fix(engine): freeze default percent_of_equity/cash market-order sizing at the signal bar#79
Merged
Merged
Conversation
…g at the signal bar
TradingView sizes a DEFAULT-qty (qty=na) market order at the SIGNAL bar, not at
the fill:
equity_S = initial_capital + realized net profit + open_profit(close(S))
sizing_price = close(S) + slippage*mintick*(+1 buy / -1 sell)
qty = floor_step(reserve_percent_commission(equity_S * pct/100)
/ fx / (sizing_price * pointvalue))
The order then fills at the next bar's open carrying that frozen quantity.
Evaluating the same expression at FILL time — the previous behaviour — was wrong
three separable ways on a reversal or a gapped bar:
1. double count: flip_market_position_to() books emit_close_trade() (which
adds the closing lot's PnL to net_profit_sum_) and only then calls
calc_qty_for_type(), while position_* still holds the lot just closed, so
open_profit() re-adds that PnL as a phantom mark;
2. look-ahead: open_profit() marked at current_bar_.close = the FILL bar's
close, a price unknown when the order fills at that bar's open;
3. wrong divisor: it divided by fill_price = open(S+1) rather than close(S).
49.9% of 15m bars in the reference window have open != prev close.
Freezing at placement removes all three at once: current_bar_ IS the signal bar
there, so open_profit() marks at close(S) and the divisor is the argument.
The frozen quantity is stored in a NEW PendingOrder::frozen_default_qty field
rather than in ``qty``. ``qty`` doubles as the "an explicit qty was provided"
flag, and several sites recover "was this default-sized?" from
std::isnan(o.qty) — reduce_oca_group's default-sized cancel, the
pending-reversal-entry binding, the OCA fully-filled heuristic, and the
partial-vs-full exit classification. Writing into ``qty`` silently flips all of
them; ``qty`` stays NaN and the frozen value is read only where a quantity is
actually computed.
Scope: MARKET (strategy.entry) and RAW market (strategy.order) placements with
default PERCENT_OF_EQUITY / CASH sizing. FIXED default sizing needs no freeze
(same value at fill). Priced (limit/stop) entries keep the legacy fill-time
sizing: TV's basis for an order armed bars before its fill is not established.
Known residual, deliberately NOT addressed here: for an entry sized while a
position is open with commission_type=percent, TV may additionally deduct the
entry fee already charged on the open lot. Reconstructions of TV's equity chain
disagree and the end-to-end evidence is confounded; an in-code comment warns
against re-adding it by algebra. It needs a clean-room TV probe.
Gates: ctest 80/80 (adds tests/test_default_qty_signal_freeze.cpp, which pins
the reversal, gap-down and gap-up flat entries, process_orders_on_close, and
CASH); corpus run_corpus.sh 252/252 -> excellent=239 strong=12 anomaly=1
(unchanged baseline); scraper data sweep of 409 strategies -> 5 tier upgrades,
0 downgrades.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
TradingView sizes a DEFAULT-qty (
qty=na) market order at the signal bar, not at the fill:The order then fills at the next bar's open carrying that frozen quantity. Evaluating the same expression at fill time — the previous behaviour — was wrong three separable ways on a reversal or a gapped bar:
flip_market_position_to()booksemit_close_trade()(adding the closing lot's PnL tonet_profit_sum_) and only then callscalc_qty_for_type(), whileposition_*still holds the lot just closed, soopen_profit()re-adds that PnL as a phantom mark.open_profit()marked atcurrent_bar_.close, i.e. the fill bar's close, a price unknown when the order fills at that bar's open.fill_price = open(S+1)rather thanclose(S). 49.9% of 15m bars in the reference window haveopen != prev close.Freezing at placement removes all three at once:
current_bar_is the signal bar there.Evidence
A flat entry isolates the divisor (no position, so the double count cannot apply). Corpus golden
analyzer-parity-percent-of-equity-sizing-01entry #1:close(S)=1807.83,open(S+1)=1807.82, capital 1e6, pct 99 → TVSize(qty) = 547.6178 = 990000/1807.83. Base engine emitted547.6209 = 990000/1807.82. Patched engine emits547.6180.The buggy fill-time formula predicts 1739/1739 reversal quantities in one exercised strategy;
close(S)predicts 370/370 gapped reversals and 507/507 gapped flat entries whereopen(S+1)predicts 0.Why a new field
The frozen quantity is stored in a new
PendingOrder::frozen_default_qty, not inqty.qtydoubles as the "an explicit qty was provided" flag, and several sites recover "was this default-sized?" fromstd::isnan(o.qty):reduce_oca_group's default-sized cancel, the pending-reversal-entry binding, the OCA fully-filled heuristic, and the partial-vs-full exit classification. An earlier revision wrote intoqtyand silently flipped all of them — it disabled reversal binding for exactly the orders this change targets, and cost 5 downward tier moves.qtynow stays NaN; the frozen value is read only where a quantity is actually computed.Scope
MARKET (
strategy.entry) and RAW market (strategy.order) placements with defaultPERCENT_OF_EQUITY/CASHsizing.FIXEDdefault sizing needs no freeze (identical value at fill). Priced (limit/stop) entries keep the legacy fill-time sizing — TV's basis for an order armed bars before its fill is not established.Known residual (deliberately not addressed)
For an entry sized while a position is open with
commission_type=percent, TV may additionally deduct the entry fee already charged on the open lot. Independent reconstructions of TV's equity chain disagree, and the end-to-end evidence is confounded by an unrelated order-admission gap. An in-code comment warns against re-adding it by algebra; it needs a clean-room TV probe.Test plan
ctest— 80/80 (addstests/test_default_qty_signal_freeze.cpp, pinning the reversal, gap-down and gap-up flat entries,process_orders_on_close, and CASH; a gap must neither re-size nor decline a flat, fully-affordable entry)scripts/run_corpus.sh— 252/252 ran, excellent=239 strong=12 anomaly=1, the unchanged baseline🤖 Generated with Claude Code