Skip to content

fix(security): tighten TF validation, split error messages by failure mode - #3

Merged
luisleo526 merged 2 commits into
mainfrom
fix/security-tf-validation
May 10, 2026
Merged

luisleo526 merged 2 commits into
mainfrom
fix/security-tf-validation

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

Summary

Hardens `validate_security_timeframes` (`src/engine_security.cpp`) for all `request.security[_lower_tf]` calls.

Invariants now enforced

  • `request.security` requested TF must be `>= input_tf` (HTF or same). Reject finer TFs with hint to use `security_lower_tf`.
  • `request.security_lower_tf` requested TF must be `< input_tf` AND `input_tf % requested_tf == 0` (integer divisor).
  • TF literals must parse to `> 0` seconds (rejects `"abc"`, `"0"`, etc.).
  • `script_tf >= input_tf` already enforced at `engine_run.cpp:199`; documented in validate path.

Pre-existing leak fixed: `request.security("X","5",...)` with `input_tf="15"` was silently going through LTF emulation path. Now correctly gated on `lower_tf_array_requested`.

Test plan

  • 21/21 ctest pass (incl. new `test_security_tf_validation.cpp`)
  • Corpus parity unchanged (163/200 strict-excellent, matches main baseline)
  • Existing `test_request_security.cpp` updated for new error messages

Split the previously-conflated lower-TF diagnostics in
validate_security_timeframes into four distinct failure modes:

- invalid timeframe literal (e.g. "abc") names the offending input
- request.security with a finer-than-input TF is now rejected even
  when the ratio is an integer divisor; the error directs callers
  to request.security_lower_tf
- request.security_lower_tf with a non-integer-divisor ratio reports
  the actual ratio and explains why bars cannot be subdivided
- request.security_lower_tf with an HTF-or-equal target is rejected
  with a message naming the lower-TF API requirement

Document that the script_tf >= input_tf invariant is enforced
separately in BacktestEngine::run() (engine_run.cpp ~line 199).

Update existing test_request_security harnesses that relied on the
old behavior of regular request.security silently emulating LTF.
Add test_security_tf_validation covering the seven invariants:
HTF accept, finer-than-input request.security reject (with hint to
LTF API), LTF non-divisor reject, LTF integer-divisor accept, LTF
HTF-target reject, invalid TF literal reject, same-TF accept.
@luisleo526
luisleo526 merged commit d34a9a2 into main May 10, 2026
4 checks passed
@luisleo526
luisleo526 deleted the fix/security-tf-validation branch May 10, 2026 16:48
luisleo526 added a commit that referenced this pull request Sep 16, 2026
…tion and carry the entry-bar masks (R4-D L10b)

The legacy owner (ab9714b pine_scheduler.cpp: process_pending_orders ->
update_per_trade_extremes -> process_margin_call) sampled every open lot's
high/low/close for the bar before liquidating it, so a lot flattened by a
margin call on its first extreme still owns the rest of the bar's favorable
excursion. On the switched route the generic excursion fold stops when the
lot closes, so corpus/anomaly-equity-mirror-strategy-equity-01 trade #3
reported favorableUsd 21.28 where the owner reports 27.34 (final-sweep
class H16, 271 probes, 262 of them short entries). The adapter now re-runs
the owner's per-lot sampler at margin-call submit (full bar on the
scheduled path; the traversed prefix only under process_orders_on_close,
pine_fills.cpp:2009-2023), and the source host sets the owner's entry-bar
masks (pine_fills.cpp:42) on the lot of a priced entry applied mid-bar so
that sampler never credits pre-fill range.

tests/test_l10b_excursion_parity.cpp pins the 04-21 margin-call bars and a
short percent-of-equity same-bar shape with the owner's literals.

Executor: Grok (brief W14a); verified by root (27/27, 455 ctest, hash
coverage OK; full corpus replay unchanged: the corpus lane has no
margin-call divergence). Kernel untouched.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ADabocUiCAh8YsUEsVNspC
luisleo526 added a commit that referenced this pull request Sep 16, 2026
…ip-prep sibling fill first (R4-D L10e)

After a same-pass close flattened the position, the native route forced the
following opposite stop entry to the close's fill price (the open print)
and let a later same-direction stop consume the from-flat carry. The legacy
owner (ab9714b pine_fills.cpp:8002-8018, 3687-3788) books such a stop at
its stop level unless the opening print already gapped through it, and when
an older flip-prep sibling is also touched on the bar that older stop fills
first. Corpus scenarios order-opposite-entry-close-same-pass-01 and
pyramid-flip-stop-pyramiding-2-01 now replay identically to the owner
(trade #3 short at 1800.00, not 1803.22); the local corpus replay goes from
259 to 261 identical scenarios with no new difference.

tests/test_l10e_opposite_entry_close_same_pass.cpp pins both scenarios'
trade #3 with the owner's literals on the exact bars.

Executor: Grok (brief W15b); verified by root (36/36, 455 ctest, hash
coverage OK). Kernel untouched.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ADabocUiCAh8YsUEsVNspC
luisleo526 added a commit that referenced this pull request Sep 17, 2026
…sidual lot (R4-D L10y)

yukozb-gold-ny-orb-v19-close-20-00 #2/#3 and ahtisham-ee-decoded-
volatility-expansion-ahtisham #126/#127: with default_qty_value=100 the
legacy owner admits the entry and books it as a main lot plus a residual
margin-call lot closed again on the entry bar; the switched route refused
the whole entry.  The adapter now sizes and admits the full-equity entry
the way the owner's fill-time affordability and margin-call slice do, and
samples the residual lot's open extremes like the owner.  Both population
reproductions replay identically to the owner.  Pinned with embedded bars
in tests/test_l10y_full_equity_entry_split.cpp (W21c, qwen3.8-max).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RKCCnMS51GLmRbA3AHtBDb
luisleo526 added a commit that referenced this pull request Sep 26, 2026
…blings its leg had (R5 lane INT26, round 2: the gate sweep's leaver)

INT26's gate sweep (exp-r5-int26-20260926 against rebaseline e69aa1f7)
refused the merge on one leaver,
scrapper:data-BINANCE-BTCUSDT/standard/vasudevshenoy-manoj-betrayed-me
(BTCUSDT 15m, process_orders_on_close, pyramiding 0; stop entries
'Buy'/'Sell' with strategy.exit stop/limit brackets, a pending entry
cancelled when the next bar does not trigger it): 821 of TradingView's 821
trades on main 0d76a09, 820 on ebe6748 (canonical 100 -> 99.9).

Bisect (the probe's own feed, spark, one build per pick boundary): a6e6a00
and 2922ade book 821, e1d7a98 -- R5 lane PAR-ORDERS, H-MEASURE Finding 6b,
a process_orders_on_close stop entry its placing close already reached
fills at that close -- books 820, and so does every later pick.

Mechanism, on 2025-08-03 10:00 UTC. The 09:45 close cancels its pending
'Buy', places it again (stop 113689.29) and re-issues 'Buy Exit' (limit
113771.41); the re-issued legs keep their older book slot, so on the 10:00
bar the pre-script drain settles the limit on its parent's touch bar --
after that bar's receipt read. The 10:00 bar closes at its high 113771.47;
its body cancels 'Buy' again and places it at that high with a fresh 'Buy
Exit' (limit 114004.53), and since e1d7a98 the close pass fills that
re-entry at the close, as TradingView does (#274), so the fresh legs are
working when 10:15 opens. The first limit's terminal receipt is read only
then, and observe_terminal_receipts' sibling cancel withdrew every working
leg of the same exit id, from_entry and origin -- the fresh bracket's
included, both legs at origin 0. The re-entry ran on to the 15:15 session
close and swallowed TradingView's #275 (11:45). Before e1d7a98 the
re-entry rested to 10:15, its legs still staged, so the late read found no
sibling to take. The pick is right; the sibling cancel was not.

Fix, adapter only: cancel_bracket_siblings takes the ordinal the terminal
receipt executed at when the observer reads it, and skips a matching leg of
a later strategy.exit call (another command_sequence) that the kernel
accepted after that ordinal (its Birth::acceptance_ordinal, read through
native_working_requests() only when such a leg is among the matches): that
leg is the same id's later order. A leg of the filled leg's own call is its
sibling however late it was accepted -- the drain submits a bracket's legs
one at a time, executing each, so a stop can be accepted after its limit
filled -- and goes as before (a review of the first form, which skipped
every leg accepted after the fill, found that case). The close passes that
call it synchronously pass nothing and are unchanged. No state is added.

The probe (spark, generated.cpp of codegen a4259656, the lane feed
6b54c44a): 821 trades, canonical rubric excellent, countAbsDelta 0 (820 /
strong before). TradingView's #273-#275 exact: 10:00 113689.29 -> 10:00
113771.41; 10:00 113771.47 -> 11:00 114004.53; 11:45 114005.84 -> 15:15
113844.70. 819 of 821 trades exact to the minute and cent on both legs
(main: 771; the two others, #1 and #3, differ on main too).

Witness: tests/test_pooc_stop_reentry_bracket_tapes.cpp replays three `lab
tv --no-note` tapes of synthetic scripts of the same shape on NYSE:F 15m
(tests/fixtures/pooc_stop_reentry_bracket; tv_trades e0447b53 / b7e89ce1 /
efbd25e3): three flat signal closes that each cancel and place again a stop
entry with its bracket, 10c beyond the close, then 1c (the next bar fills
the entry and then the limit and closes flat), then at that close (filled
there), long on bars 39-41 and short on bars 44-46; TradingView books the
same four rows on the chart and under the magnifier. Every row is compared
field by field, with the end book and its working bracket legs.
- int26-reentry-bracket-chart: all four rows, flat; the short is the
  leaver's shape.
- int26-reentry-bracket-mag records a divergence the fix does not touch
  (the same before and after): there the drain leaves the short's retained
  limit to the flush below the body, whose book still holds the short, so
  the re-entry is never placed -- rows 1-3 exact, both books flat.
- int26-reentry-noexit-chart, the control: the short's re-entry places no
  strategy.exit (TradingView's row 4 still open at the end); the short
  bracket's stop, accepted after its limit filled, must still be withdrawn.
Fail-before: against ebe6748 2 of 27 checks fail (the chart tape: 3 rows
against 4, short 100 against flat); against the first form of the rule 1 of
28 (the control's stop left working); after, 28 of 28.

Floors: one source-bound row. RELEASE_MIN_TESTS 702 -> 703,
EXCLUDED_REGISTERED_MIN 683/683/692 -> 684/684/693 (and
test_ci_verify's expectation, docs/ci.md). KERNEL_MIN_TESTS unchanged.
docs/ci.md's sentence "The PR registration floors at INT26 are 683 for
Debug and sanitizers and 692 for native" is rewritten with the new counts.

Docs: the design page's PAR-ORDERS paragraph records the rule and the
magnified divergence; the CHANGELOG 1.0.0 entry's process_orders_on_close
item adds it for a user. Every doc anchor carried onto this tree by the
exact line map from ebe6748 (135 moved); the three citations of
ci_verify.py's RELEASE_MIN_TESTS line (CONTRIBUTING.md, contributing-llm.md
twice) set by hand, 390 -> 393.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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.

1 participant