Skip to content

Fix MultimarketPower: bar-0 forward-fill guard and thread-safe trade buffers - #173

Open
AlbertoAmadorBelchistim wants to merge 3 commits into
AtasPlatform:Developfrom
AlbertoAmadorBelchistim:fix/multimarketpower-robustness
Open

AlbertoAmadorBelchistim wants to merge 3 commits into
AtasPlatform:Developfrom
AlbertoAmadorBelchistim:fix/multimarketpower-robustness

Conversation

@AlbertoAmadorBelchistim

@AlbertoAmadorBelchistim AlbertoAmadorBelchistim commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Two small robustness fixes for MultiMarketPower, in separate commits.

1. Guard OnCalculate forward-fill against bar 0

OnCalculate copies the previous bar's value into a zero bar:
vds[bar] = vds[bar - 1]. The guard only checked bar != CurrentBar - 1, so on a chart with a single bar (CurrentBar == 1, bar == 0) it reads vds[-1].
Added || bar == 0 to the early return.

2. Synchronize the realtime trade buffers

_ticks / _trades are appended from the market-data callbacks (OnNewTrade, OnCumulativeTrade, OnUpdateCumulativeTrade) while OnFinishRecalculate clears them and CalculateHistory iterates them, with no synchronization - even though a _locker field is already declared. This races on reload under live data (the existing catch (NullReferenceException) //on reset exception ignored in CalculateHistory is a symptom). All buffer access is now wrapped in lock (_locker), and the replay iterates a snapshot taken under the lock instead of the live list.

No behavior change in the steady state; only removes the race and the bar-0 edge case.

@Stig4all

Copy link
Copy Markdown
Collaborator

Thanks for the update. The direction makes sense, but we cannot merge this version yet.
The main concern is the handoff between history replay and live buffering. CalculateHistory now takes a snapshot of _ticks / _trades, but _bigTradesIsReceived is set only after that work completes. That leaves a window where new live trades can still be appended to the original buffers and then cleared in finally, which means some data may be lost during the transition.
So the race on enumeration is improved, but the buffer handoff is still not fully safe. We would need an additional drain/replay step after the snapshot, or another synchronization approach that guarantees trades arriving during the transition are not dropped.
Because of that, we cannot accept this PR in its current form.

@AlbertoAmadorBelchistim

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review - you were right about the handoff window. Pushed a follow-up commit that closes it: trade handlers now check the flag inside the lock (buffer-or-live is atomic), and after the historical replay a drain loop repeatedly swaps out the buffers under the lock and replays each batch, flipping _bigTradesIsReceived inside the lock only once both buffers are empty. The finally-block clear is gone, so trades arriving during the transition are always replayed rather than dropped. Zero-history responses now also drain instead of returning early

@Stig4all

Stig4all commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

The drain loop addresses the previously reported handoff issue, but another update-loss window remains.
DrainBufferedData copies and clears _trades while realtime processing is still disabled. If OnUpdateCumulativeTrade arrives while that batch is being replayed, it sees an empty _trades list and returns without retaining the update. The trade in the detached batch can therefore be processed with an outdated volume.
Please preserve cumulative-trade updates across batch boundaries and replay them with the correct update semantics. A focused test covering an update arriving between buffer extraction and completion of replay would help verify the fix.
The bar-zero guard looks suitable to merge separately.

…ng realtime

The previous version snapshotted _ticks/_trades under the lock, but
_bigTradesIsReceived was only set after CalculateHistory returned. Trades
arriving between the snapshot and the flag flip were appended to the
original buffers and then discarded by the finally-block clear.

Close the handoff window:
- Trade handlers now check the flag inside the lock: a trade is either
  buffered under the lock or processed as realtime, never dropped.
- CalculateHistory no longer clears the buffers in finally. After the
  historical replay it calls DrainBufferedData, which repeatedly swaps
  out the buffers under the lock and replays each batch, and flips
  _bigTradesIsReceived inside the lock only once both buffers are empty,
  so no trade can be buffered after the flip.
- The flag becomes volatile (it is still read lock-free in OnCalculate)
  and is reset inside the lock in OnFinishRecalculate.
- Zero-history responses now also drain buffered live trades instead of
  returning early.
…tches

Buffered updates used to overwrite the last buffered trade. When an update
arrived after DrainBufferedData had detached the batch, the buffer was empty
and the update was dropped, so the trade was replayed with an outdated volume.

Buffer new trades and updates as ordered events and replay each one through
CalculateTrade with its own update flag. An update for a trade that is not the
last processed one (for example, a trade already contained in the history
response) is skipped as before, so its volume is not counted twice.
_lastTrade is reset together with the accumulated deltas.
@AlbertoAmadorBelchistim
AlbertoAmadorBelchistim force-pushed the fix/multimarketpower-robustness branch from df49174 to 3d7d0f9 Compare September 20, 2026 09:37
@AlbertoAmadorBelchistim

AlbertoAmadorBelchistim commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed analysis. Updated:

  • Buffered cumulative-trade events are now kept as an ordered list of (trade, isUpdate) entries. An update no longer overwrites the last buffered trade; it is appended and replayed through CalculateTrade(trade, isUpdate: true, …). An update that arrives after DrainBufferedData has detached a batch is therefore buffered and replayed in the next loop iteration instead of being dropped.
  • An update for a trade that is not the last processed one (e.g. a trade already contained in the history response) is skipped, as before, so its volume is not counted twice. _lastTrade is reset together with the deltas.
  • The bar-zero guard has been moved to a separate PR (fix(MultiMarketPower): guard OnCalculate forward-fill against bar 0 #181 ).

Test: the repository has no test project, so I verified it with a small console harness that compiles the actual OnCumulativeTrade, OnUpdateCumulativeTrade, CalculateTrade and DrainBufferedData from the file against stubs. It covers an update fired while its trade is being replayed (the lock is not held at that point), updates buffered before the drain, an orphan update, realtime after the drain, and a stress run of 200 × 2,000 trades with random updates from a producer thread. The previous version fails the first case (volume 5 instead of 8) and the stress run; the new one passes all of them. I can add the harness to the PR if you want it in the repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants