Repository navigation
Make candle commands time-aware and order Android option lists (PRO-4970) #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,54 @@ public class HTKLineContainerView extends RelativeLayout { | |
| private ThemedReactContext reactContext; | ||
|
|
||
| public HTKLineConfigManager configManager = new HTKLineConfigManager(); | ||
| // Option lists are parsed off the UI thread, in order, per chart. | ||
| public final java.util.concurrent.ExecutorService optionListExecutor = | ||
| java.util.concurrent.Executors.newSingleThreadExecutor(); | ||
| // Written on the UI thread, read by the option-list worker. | ||
| public volatile int optionListGeneration = 0; | ||
| private int appliedOptionListGeneration = 0; | ||
| // Bars delivered by live commands while an option list is still being parsed. | ||
| // The parsed snapshot predates them, so they are merged back when it lands. | ||
| private final List<KLineEntity> barsSinceOptionList = new ArrayList<>(); | ||
|
|
||
| /** UI thread: a new option list was handed to the parser. */ | ||
| public int onOptionListScheduled() { | ||
| barsSinceOptionList.clear(); | ||
| return ++optionListGeneration; | ||
| } | ||
|
|
||
| /** UI thread: the parsed option list has replaced the native dataset. */ | ||
| public void onOptionListApplied(int generation) { | ||
| appliedOptionListGeneration = generation; | ||
| synchronized (configManager.modelArray) { | ||
| for (KLineEntity bar : barsSinceOptionList) { | ||
| mergeBar(bar); | ||
| } | ||
| } | ||
| barsSinceOptionList.clear(); | ||
| reloadConfigManager(); | ||
| } | ||
|
|
||
| private void rememberForPendingOptionList(KLineEntity bar) { | ||
| if (appliedOptionListGeneration != optionListGeneration) { | ||
| barsSinceOptionList.add(bar); | ||
| } | ||
| } | ||
|
|
||
| /** Same time replaces the last bar, a newer bar is appended, an older bar is ignored. */ | ||
| private void mergeBar(KLineEntity bar) { | ||
| if (configManager.modelArray.isEmpty()) { | ||
| configManager.modelArray.add(bar); | ||
| return; | ||
| } | ||
| int lastIndex = configManager.modelArray.size() - 1; | ||
| long lastTime = configManager.modelArray.get(lastIndex).timestamp; | ||
| if (bar.timestamp <= 0 || lastTime <= 0 || bar.timestamp == lastTime) { | ||
| configManager.modelArray.set(lastIndex, bar); | ||
| } else if (bar.timestamp > lastTime) { | ||
| configManager.modelArray.add(bar); | ||
| } | ||
| } | ||
|
|
||
| public KLineChartView klineView; | ||
|
|
||
|
|
@@ -334,19 +382,23 @@ public void updateLastCandlestick(Map<String, Object> candlestickData) { | |
|
|
||
| // Only preserve indicator lists if the new data doesn't contain them | ||
| android.util.Log.d("HTKLineContainerView", "Using new indicator data from React Native"); | ||
| if (newEntity.maList.isEmpty()) { | ||
| // Indicator values belong to one bar: reuse them only when this update is | ||
| // for that same bar, never for a newer bar that will be appended. | ||
| boolean sameBar = newEntity.timestamp <= 0 || existingEntity.timestamp <= 0 | ||
| || newEntity.timestamp == existingEntity.timestamp; | ||
| if (sameBar && newEntity.maList.isEmpty()) { | ||
| newEntity.maList = existingEntity.maList; | ||
| } | ||
| if (newEntity.maVolumeList.isEmpty()) { | ||
| if (sameBar && newEntity.maVolumeList.isEmpty()) { | ||
| newEntity.maVolumeList = existingEntity.maVolumeList; | ||
| } | ||
| if (newEntity.rsiList.isEmpty()) { | ||
| if (sameBar && newEntity.rsiList.isEmpty()) { | ||
| newEntity.rsiList = existingEntity.rsiList; | ||
| } | ||
| if (newEntity.wrList.isEmpty()) { | ||
| if (sameBar && newEntity.wrList.isEmpty()) { | ||
| newEntity.wrList = existingEntity.wrList; | ||
| } | ||
| if (newEntity.selectedItemList.isEmpty()) { | ||
| if (sameBar && newEntity.selectedItemList.isEmpty()) { | ||
| newEntity.selectedItemList = existingEntity.selectedItemList; | ||
| } | ||
|
|
||
|
|
@@ -363,7 +415,22 @@ public void updateLastCandlestick(Map<String, Object> candlestickData) { | |
| return; | ||
| } | ||
|
|
||
| configManager.modelArray.set(lastIndex, newEntity); | ||
| // Commands are time-aware: an option-list replacement can land before or | ||
| // after a live command, so the native array may be behind the JS data. | ||
| // Never overwrite an older bar with a newer one, and ignore stale bars. | ||
| KLineEntity currentLast = configManager.modelArray.get(configManager.modelArray.size() - 1); | ||
| long incomingTime = newEntity.timestamp; | ||
| long currentTime = currentLast != null ? currentLast.timestamp : 0; | ||
| if (incomingTime > 0 && currentTime > 0 && incomingTime < currentTime) { | ||
| android.util.Log.d("HTKLineContainerView", "Ignoring stale last-candlestick update"); | ||
| return; | ||
| } | ||
| if (incomingTime > 0 && currentTime > 0 && incomingTime > currentTime) { | ||
| configManager.modelArray.add(newEntity); | ||
|
Comment on lines
+428
to
+429
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 New candles inherit stale indicators When newer Learn moreThe existing fallback copies empty indicator lists from the current last candle because this command previously replaced that same candle. The new timestamp branch changes the operation to an append after those copies have already happened. The iOS branch follows the same sequence in updateLastCandlestick. Values derived for one candle are therefore attached to a different timestamp. Example: The 10:00 candle has MA5 100 and RSI 45. A 10:01 update contains OHLCV but no Recommended fix: Decide whether the command will replace or append before preserving indicators. Preserve missing lists only for equal timestamps. For a newer timestamp, leave missing lists empty or calculate them from the expanded dataset on both platforms. Was this helpful? React with 👍 or 👎 to provide feedback.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid, fixed in bda3095 on both platforms. The indicator fallback now runs only when the update is for the same bar ( |
||
| } else { | ||
| configManager.modelArray.set(configManager.modelArray.size() - 1, newEntity); | ||
| } | ||
| rememberForPendingOptionList(newEntity); | ||
| android.util.Log.d("HTKLineContainerView", "Updated last candlestick at index: " + lastIndex); | ||
| } | ||
|
|
||
|
|
@@ -447,8 +514,23 @@ public void addCandlesticksAtTheEnd(ReadableArray candlesticksArray) { | |
|
|
||
| // Add new entities to the end of the array with synchronization | ||
| synchronized (configManager.modelArray) { | ||
| configManager.modelArray.addAll(newEntities); | ||
| android.util.Log.d("HTKLineContainerView", "Added " + newEntities.size() + " new candlesticks to the end"); | ||
| // Skip bars the native array already has (same time replaces the last bar). | ||
| long lastTime = configManager.modelArray.isEmpty() ? 0 | ||
| : configManager.modelArray.get(configManager.modelArray.size() - 1).timestamp; | ||
| List<KLineEntity> fresh = new ArrayList<>(); | ||
| for (KLineEntity entity : newEntities) { | ||
| if (entity.timestamp <= 0 || lastTime <= 0 || entity.timestamp > lastTime) { | ||
| fresh.add(entity); | ||
| if (entity.timestamp > 0) lastTime = entity.timestamp; | ||
| } else if (entity.timestamp == lastTime && fresh.isEmpty()) { | ||
| configManager.modelArray.set(configManager.modelArray.size() - 1, entity); | ||
| } | ||
| } | ||
| configManager.modelArray.addAll(fresh); | ||
| for (KLineEntity entity : fresh) { | ||
| rememberForPendingOptionList(entity); | ||
| } | ||
| android.util.Log.d("HTKLineContainerView", "Added " + fresh.size() + " of " + newEntities.size() + " candlesticks to the end"); | ||
| android.util.Log.d("HTKLineContainerView", "Total candlesticks now: " + configManager.modelArray.size()); | ||
| android.util.Log.d("HTKLineContainerView", "Was at end before adding: " + wasAtEnd); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Late option replacement drops live candle
When
updateLastCandlestickappends before pendingoptionListwork finishes, the replacement overwrites that candle.optionListGenerationtracks only option calls, so the live candle disappears on both platforms.Learn more
Option parsing runs asynchronously, while candle commands mutate the current native array immediately. The generation check rejects an option result only when another option call supersedes it. A candle command does not change the generation, so an older snapshot can still replace
modelArrayafter that command. Android performs the replacement in reloadOptionList, and iOS has the same replacement behavior in reloadOptionList. Timestamp checks inside the commands only protect the opposite ordering, where the replacement lands first.Example: The native array ends at 10:00. An option snapshot ending at 10:00 starts parsing, then
updateLastCandlestickappends 10:01. The pending snapshot subsequently replaces the array, removing 10:01 even though no newer option call changed the generation.Recommended fix: Parse off-thread, but serialize dataset application and candle commands on the UI thread. Before replacing
modelArray, reconcile bars added or updated since the snapshot was scheduled by timestamp. Apply the equivalent ordering or reconciliation on iOS.Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid for Android, fixed in bda3095. A bar delivered by a live command while an option list is still being parsed is now recorded per chart and merged back by time on the UI thread when that snapshot lands (
onOptionListScheduled/onOptionListApplied/mergeBar), so a late replacement can no longer remove it. Only bars delivered after the latest option list was scheduled are replayed, so bars from a previous market or timeframe are never carried into a new dataset. Not applicable on iOS:optionListis applied synchronously in the prop setter there, so there is no pending window between a command and a replacement.