Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 44 additions & 16 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2981,26 +2981,54 @@ void BacktestEngine::apply_filled_order_to_state(
* syminfo_.pointvalue
* sizing_fx
* (margin_pct / 100.0);
// The epsilon absorbs double rounding AND one whole lot of
// notional.
// The epsilon absorbs double rounding. On the NON-reversal arms it
// additionally absorbs one whole lot of notional.
//
// The quantity was floored to the lot step, so the budget it left
// unspent is an unobservable remainder anywhere in
// The original rationale, kept because it still holds where it was
// measured: the quantity was floored to the lot step, so the budget
// it left unspent is an unobservable remainder anywhere in
// [0, qty_step * price). A decline whose margin is smaller than
// that remainder is not a decision about affordability at all —
// it is a coin flip on where the floor happened to land. On a
// continuous feed nearly half of all bars gap by exactly one
// mintick, so without this term the reversal branch resolves such
// bars by lot-floor luck: deterministic at large equity, arbitrary
// at small. Widening by one lot keeps every decline that TV's
// exports actually confirm (their margins exceed a lot of
// notional) and drops the ones no ground truth supports.
// that remainder looks like a coin flip on where the floor happened
// to land, and on a continuous feed nearly half of all bars gap by
// exactly one mintick. Widening by one lot was adopted because it
// "keeps every decline that TV's exports actually confirm (their
// margins exceed a lot of notional) and drops the ones no ground
// truth supports" — i.e. it was predicated on the ABSENCE of ground
// truth for sub-lot reversal declines.
//
// design-reversal-admission-float-guard: that premise is falsified
// ON THE REVERSAL ARM ONLY, by ground truth that did not exist when
// it was written. A pinned 13-month ETHUSDT.P parity dossier
// (percent_of_equity=100, margin 100) supplies 94
// TradingView-confirmed reversal declines against 2,325
// admits; 92 of the 94 have margins BELOW one lot of notional. The
// widening therefore does not blunt this arm's gate, it makes it
// inert: 81/81 reproducible declines AND 2,325/2,325 admits both sit
// inside [0, qty_step * admit_price), because an all-in reversal
// spends the whole equity by construction and its entire decision
// lives inside one lot-floor remainder. Measured on that tape:
// one-lot epsilon 2/94 declines caught (balanced accuracy 51.1 %);
// float-guard epsilon 86/94 caught with 6/2,325 wrongly cancelled
// (balanced accuracy 95.6 %). Board-wide the tightened arm would
// cancel 18 of 23,785 TradingView-admitted all-in reversals (0.08 %,
// only 1 of them above one lot).
//
// The lot-floor "coin flip" argument does not transfer to the
// reversal arm the way it does to the others, because there the
// frozen quantity was floored against the PREVIOUS bar's close while
// the order fills at THIS bar's open: the overshoot is an observable
// gap, not floor luck. The flat-open and same-direction-add arms
// keep the one-lot term — each is separately TV-pinned, nothing has
// falsified their premise, and the flat-open arm is undeclinable by
// the floor invariant anyway (it prices at the sizing notional).
double epsilon =
std::max(1e-9, std::abs(free_funds) * 1e-12);
epsilon = std::max(epsilon, qty_step_ * admit_price
* syminfo_.pointvalue
* sizing_fx
* (margin_pct / 100.0));
if (!reversal) {
epsilon = std::max(epsilon, qty_step_ * admit_price
* syminfo_.pointvalue
* sizing_fx
* (margin_pct / 100.0));
}
if (required_margin > free_funds + epsilon) {
// design-declined-reversal-close-leg: ONLY the reversal decline
// triggers close-leg suppression (admit_price == slipped fill,
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ set(TEST_SOURCES
test_fills_edge
test_default_qty_signal_freeze
test_margin_admission_gate
test_reversal_admission_float_guard
test_direct_short_reversal_affordability
test_short_reversal_emission
test_margin_stop_admission
Expand Down
36 changes: 29 additions & 7 deletions tests/test_margin_admission_gate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,34 @@ void test_slipped_short_reversal_zero_gap_admitted() {
CHECK(eng.position_side_ == PositionSide::SHORT); // the flip happened
}

// H. Lot-step slack: with a real qty_step the frozen qty leaves an unspent
// remainder in [0, qty_step*price). A reversal whose adverse gap costs
// LESS than one lot of notional must be ADMITTED — the decline would be
// decided by where the floor landed, not by affordability. A gap that
// costs far more than one lot is still DECLINED.
// H. Lot-step slack on the REVERSAL arm — CORRECTED 2026-07-26
// (design-reversal-admission-float-guard).
//
// This pin used to assert the opposite of H.1 below: that a reversal whose
// adverse gap costs LESS than one lot of notional must be ADMITTED, because
// the frozen qty leaves an unspent remainder in [0, qty_step*price) and the
// decline would be "decided by where the floor landed, not by
// affordability". That rationale rested on an empirical claim — that every
// decline TradingView's exports confirm has a margin exceeding one lot —
// which held only while no sub-lot ground truth existed.
//
// It does now. chartprime-power-order-blocks-chartprime (percent_of_equity
// 100, margin 100, qty_step 0.0001, 13 months of ETHUSDT.P) carries 2,419
// TradingView reversal decisions: 94 declines, and 92 of those 94 have
// margins BELOW one lot. On an all-in reversal the entire decision lives
// inside one lot-floor remainder by construction, so the widening made this
// arm's gate inert rather than merely conservative (2/94 declines caught,
// balanced accuracy 51.1 %; float guard: 86/94 with 6/2,325 false cancels,
// 95.6 %). The one-lot term is therefore gone from the reversal arm and
// retained on the flat-open and same-direction-add arms, whose premise
// nothing has falsified — see test_reversal_admission_float_guard.cpp for
// the full pin set including the scope controls.
//
// H.1 now asserts the corrected behaviour on the ORIGINAL fixture (1-tick
// adverse gap, shortfall $1.00 against a $1.0001 lot): DECLINED. H.2 is
// unchanged — a gap far above one lot was and still is DECLINED.
void test_reversal_lot_step_slack() {
std::printf("-- H: reversal within one lot of notional admitted --\n");
std::printf("-- H: reversal sub-lot gap declined (float guard) --\n");
{
Probe eng(QtyType::PERCENT_OF_EQUITY, 100.0, 1);
eng.qty_step_ = 0.01; // one lot @ ~100 = ~$1.00 notional
Expand All @@ -300,7 +321,8 @@ void test_reversal_lot_step_slack() {
mk_bar(4000, 100.01, 100.01, 100.01, 100.01),
};
eng.run(bars.data(), (int)bars.size());
CHECK(eng.position_side_ == PositionSide::SHORT); // admitted
CHECK(eng.position_side_ == PositionSide::LONG); // declined (was SHORT)
CHECK(eng.trade_count() == 0); // close leg suppressed
}
{
Probe eng(QtyType::PERCENT_OF_EQUITY, 100.0, 1);
Expand Down
Loading
Loading