Skip to content

Isolate study faults on both market-data dispatchers and stop NaN reaching the resilience score - #77

Merged
silahian merged 1 commit into
masterfrom
fix/study-fault-isolation-and-resilience-score
Sep 17, 2026
Merged

silahian merged 1 commit into
masterfrom
fix/study-fault-isolation-and-resilience-score

Conversation

@silahian

Copy link
Copy Markdown
Collaborator

Three fixes on the market-data dispatch path and in the Market Resilience score.

1. A faulting subscriber no longer kills the process

HelperOrderBook.DispatchToSubscribers logged the exception and then rethrew.
That rethrow ran on the market connector's producer thread, at a call site that
does not guard itself: an unhandled exception on a non-UI thread terminates the
process with no dialog. It also unwound the dispatch loop, so one faulting study
starved every subscriber after it of order-book data.

HelperTrade.DispatchToSubscribers had no per-subscriber guard at all, with the
same two consequences on the trade stream.

Both now isolate each subscriber, log the fault, raise it on OnException with
the subscriber that caused it, and continue to the next subscriber. HelperTrade
gains the OnException event and a Reset() so it matches the order-book helper.

2. A study that faults on the trade stream is now stopped

BasePluginStudy subscribed its fault handler only to the order-book helper.
Raising the event on the trade helper would have surfaced nothing: a study
faulting on trades stayed at its previous status while receiving no data. The
same handler is now subscribed to and unsubscribed from both helpers, and is
renamed to reflect that it serves both.

3. The Market Resilience score can no longer be NaN or out of range

CurrentMRScore is documented and consumed as a value in [0, 1]. Four problems
could take it outside that range, and the last of them threw:

  • The spread- and depth-recovery components compute
    avgHistory / (avgHistory + duration). With an empty history the numerator is
    set to the duration, so a zero-duration recovery evaluates 0.0 / 0.0 = NaN.
    Math.Min/Math.Max propagate NaN, so the existing clamp did not sanitise it.
    A zero denominator is now treated as an absence of evidence: the component is
    omitted and the weighted normalisation reweights what remains.
  • Zero-duration samples were appended to the recovery histories, pinning the
    historical average at zero. That average is the numerator above, so every later
    genuine recovery scored lower for the rest of the session. Only a measured
    recovery joins the history now.
  • The trade component clamped 1 - z/6 at the low end only, unlike its three
    siblings. The shock trade is anchored when flagged and scored later against
    whatever the window holds at trigger time, so its z-score can be large and
    negative and the component could exceed 1. It is now clamped at both ends, and
    it is skipped when the window has no positive mean or when the dispersion is
    below a dimensionless relative epsilon of that mean, where a z-score carries no
    information.
  • (decimal) of a non-finite double throws OverflowException. The final cast is
    now guarded by double.IsFinite, which is the backstop for any future producer
    of a non-finite value in that method.

A cycle in which every component was omitted previously published 1.0, the top
of the resilience scale, at the moment liquidity vanished. It now publishes
nothing: the last measured score stands until something is actually measured.

Tests

MarketResilienceCalculatorNumericalStabilityTests covers the score invariants,
the two recovery paths under a data-driven clock, a randomised equity tape, and
four hand-computed worked examples (a normal shock and recovery, a near-constant
trade window, a zero-duration recovery against an empty history, and an
all-omitted cycle).

HelperTradeDispatchIsolationTests and BasePluginStudyHelperExceptionWiringTests
cover the dispatch guard and the study-side wiring for both streams.

tests/Unit/Studies.MarketResilience.Test gets an xunit.runner.json that
serialises the assembly, because the calculator tests drive a process-wide clock.

Verification

  • dotnet build on VisualHFT.Commons, Studies.MarketResilience,
    tests/Unit/Studies.MarketResilience.Test and tests/Unit/VisualHFT.Commons.Tests:
    0 errors.
  • Studies.MarketResilience.Test: 67 passed, 0 failed.
  • VisualHFT.Commons.Tests: 87 passed, 0 failed.

…he resilience score

Order-book dispatch no longer rethrows after logging a faulting subscriber.
The rethrow ran on the market connector's producer thread at a call site
that does not guard itself, so an unhandled exception on a non-UI thread
terminated the process, and it unwound the dispatch loop, starving every
subscriber after the faulting one of order-book data.

Trade dispatch had no per-subscriber guard at all and now gets the same
treatment, with a new OnException event and a Reset() to match the
order-book helper. The study base class subscribes its fault handler to
both helpers, so a study faulting on the trade stream is stopped and marked
failed instead of sitting at its previous status while receiving no data.

The Market Resilience score can no longer leave [0, 1] or become NaN. A
recovery component whose denominator is zero is omitted rather than
computed as 0/0; zero-duration samples no longer enter the recovery
histories, where they pulled the baseline down for the rest of the session;
the trade component is clamped at both ends and skipped when the size
dispersion is below a dimensionless relative epsilon of the mean; and the
final cast to decimal is guarded by a finiteness check, because a
non-finite quotient survives a clamp untouched and then throws. A cycle
with no usable evidence at all now publishes nothing instead of the top of
the scale.

Tests: numerical-stability facts for the calculator, including hand-computed
worked examples, plus dispatch-isolation and fault-wiring facts for the two
helpers and the study base class.
Copilot AI lite review requested due to automatic review settings September 17, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The helpers’ fire-and-forget exception publishing still risks unobserved task failures and noisy repeated faults, and should be hardened before merge.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR hardens the market-data dispatch path (order books + trades) so a faulting subscriber no longer crashes the process or starves other subscribers, and it strengthens the Market Resilience score calculation to prevent NaN/out-of-range values from being published.

Changes:

  • Isolates per-subscriber faults in HelperOrderBook and HelperTrade, raising an OnException signal instead of allowing exceptions to escape the producer thread.
  • Wires BasePluginStudy to consume exceptions from both market-data streams and stop/mark studies failed consistently.
  • Makes MarketResilienceCalculator numerically stable (omit unusable components, avoid NaN propagation, guard non-finite casts) and adds targeted unit tests + xUnit non-parallel config.
File summaries
File Description
VisualHFT.Plugins/Studies.MarketResilience/Model/MarketResilienceCalculator.cs Sanitizes MR component math (omit zero-denominator cases, clamp safely, guard non-finite results).
VisualHFT.Commons/PluginManager/BasePluginStudy.cs Subscribes to both helpers’ OnException so trade-stream faults stop the study too.
VisualHFT.Commons/Helpers/HelperTrade.cs Adds OnException, Reset(), and per-subscriber exception isolation for trade dispatch.
VisualHFT.Commons/Helpers/HelperOrderBook.cs Removes rethrow so order-book subscriber faults don’t terminate the process.
tests/Unit/VisualHFT.Commons.Tests/HelperTradeDispatchIsolationTests.cs Verifies trade dispatch isolation + OnException signaling contract.
tests/Unit/VisualHFT.Commons.Tests/BasePluginStudyHelperExceptionWiringTests.cs Verifies studies stop/mark failed for faults from both streams.
tests/Unit/Studies.MarketResilience.Test/xunit.runner.json Disables parallel execution due to process-wide clock usage in tests.
tests/Unit/Studies.MarketResilience.Test/Studies.MarketResilience.Test.csproj Copies xUnit runner config into test output.
tests/Unit/Studies.MarketResilience.Test/MarketResilienceCalculatorNumericalStabilityTests.cs Adds invariants + worked examples and randomized regression coverage for MR stability.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 75 to 79
Task.Run(() =>
{
log.Error(ex);
OnException?.Invoke(new VisualHFT.Commons.Model.ErrorEventArgs(ex, subscriber.Target));
});
Comment on lines +80 to +84
Task.Run(() =>
{
log.Error(ex);
OnException?.Invoke(new VisualHFT.Commons.Model.ErrorEventArgs(ex, subscriber.Target));
});
@silahian
silahian merged commit e885be7 into master Sep 17, 2026
1 check passed
@silahian
silahian deleted the fix/study-fault-isolation-and-resilience-score branch September 18, 2026 00:10
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