fix(MarketResilience): require the depleted side to come back at its price, not just its size - #80
Merged
Conversation
…price, not just its size Immediacy-weighted depth is measured from the touch of the frame being measured, so the reference price moves with the book. A bid that was swept and rebuilt three ticks lower therefore read as a full recovery: the size was back, but the price that had been bid for was gone. The market moved down and the tile reported nothing. A depleted side now counts as recovered only when both hold inside the window: its depth is back to 90% of the way from its trough to its baseline, and its best price is back within one spread baseline of where it was quoted on the frame before the depletion was detected. The anchor is the frame before the depletion, and neither of the two obvious alternatives. Not the depletion frame itself: on a sweep that frame already shows the moved touch, so the event would be measured against its own damage. Not the anchoring print: a print admits events for the whole shock window, five seconds by default, so that price can be seconds stale, and ordinary drift in between would read as a failure to redeploy and publish a directional arrow from nothing. The tolerance is one spread baseline, inclusive, taken from the event itself. It is the instrument's own typical distance, so it carries no tick size and no price scale. Routine requoting inside the spread still counts as a recovery; a touch that moved further than the instrument's own spread does not. Zero tolerance fires on ordinary quote competition, a fraction of a spread is a second constant with no basis, and a tick count is a hardcoded multiplier. The test is one-sided: a side that came back at a better price came back, and the tile reports which side failed. Also aligns ActivateDepthEvent's cold-start spread baseline with the one IsLOBDepleted already uses. It floored at 1.0 regardless of the book, which measured the event on a different scale than the detector that admitted it and, on an instrument quoting below 1.0, is a tolerance larger than the whole price. Both tile tooltips are corrected for the new rule, for the arrow holding its last reading between the hysteresis levels rather than resetting to neutral, and for the first-recovery rule, which had lost the qualifier that a first-of-session non-recovery does publish. The plugin Description strings are deliberately untouched: they are hashed into the id that keys saved settings.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The documented “skip anchor check when no measurable spread baseline” behavior isn’t achievable with the current state model because the anchor tolerance is conflated with the depth-normalization baseline.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Updates the Market Resilience study so a depleted side is only credited as “recovered” when depth returns and the best price returns close enough to the pre-depletion touch, preventing false recoveries after repricing.
Changes:
- Anchor the recovery price check to the previous book frame (pre-depletion) and require the touch to return within one spread-baseline.
- Align cold-start spread-baseline behavior between depletion detection and depth-event activation.
- Update both tiles’ tooltip text to reflect the new recovery rule and clarified hysteresis behavior.
| File | Description |
|---|---|
| VisualHFT.Plugins/Studies.MarketResilience/Model/MarketResilienceCalculator.cs | Adds previous-touch anchoring and enforces price-return gating for depth recovery. |
| VisualHFT.Plugins/Studies.MarketResilience/MarketResilienceStudy.cs | Updates Market Resilience tooltip to describe price-return requirement. |
| VisualHFT.Plugins/Studies.MarketResilience/MarketResilienceBiasStudy.cs | Updates Bias tile tooltip to reflect price-aware recovery and hysteresis persistence. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| // The touch on the frame before the depletion was detected: the price each side has to | ||
| // come back to. Null when no earlier frame was seen. | ||
| public decimal? AnchorBid, AnchorAsk; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The defect
Immediacy-weighted depth is measured from the touch of the frame being measured, so the reference price moves with the book. A bid that was swept and rebuilt three ticks lower therefore read as a full recovery — the size was back, but the price that had been bid for was gone.
The score published
0.3125and the bias tile said nothing, on a book that had just given way.The rule
A depleted side now counts as recovered only when both hold inside the window:
Where the anchor comes from
The frame before the depletion — not either of the two obvious alternatives:
Why one spread baseline
It is the only distance unit already in the file, it is the instrument's own typical distance, and it carries no tick size and no price scale — this plugin serves crypto, equities, futures and FX. Routine requoting inside the spread still counts as a recovery; a touch that moved further than the instrument's own spread does not.
The alternatives were each rejected for a concrete reason: zero tolerance fires on ordinary quote competition, a fraction of a spread is a second constant with no basis, and a tick count is a hardcoded multiplier.
The test is one-sided. A side that came back at a better price came back, and is credited. The tile reports which side FAILED, and nothing failed there.
Skipped, crediting the recovery
When there is no anchor (the print arrived before any book) or no measurable spread baseline. Neither can support the comparison, and silence is the safe direction for a tile that only speaks about failures.
Also in this change
ActivateDepthEvent's cold-start spread baseline floored at1.0regardless of the book, whileIsLOBDepletedprefers the book's own spread when it has one. That measured the event on a different scale than the detector that admitted it, and on an instrument quoting below 1.0 it is a tolerance larger than the whole price. The two now agree.Both tile tooltips are corrected: for the new rule, for the arrow holding its last reading between the hysteresis levels rather than resetting to neutral, and for the first-recovery rule, which had lost the qualifier that a first-of-session non-recovery does publish.
Deliberately NOT changed
The plugin
Descriptionstrings. They are hashed into the plugin id that keys saved settings, so editing one orphans every existing user's symbol and provider selection and leaves the tile dead on launch. Verified byte-unchanged in this PR. The corrected wording lives in the tooltip, which is not hashed.Evidence
dotnet test tests/Unit/Studies.MarketResilience.Test→ 68 passed, 0 failed, with no test modified by this PR.dotnet build VisualHFT.Plugins/Studies.MarketResilience→ 0 errors.The rule was mutation-checked three ways before this port:
abs(touch - anchor) <= toleranceReviewer note
The zero-tests problem called out in #79 is gone — this test project now executes and reports its 68 results normally.