From 78acb0de0fe901235e38cd2dc15fe4a7e5a75376 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Wed, 8 Jul 2026 16:09:44 +0800 Subject: [PATCH] fix(sizing): reserve entry commission when sizing percent_of_equity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TradingView sizes a percent_of_equity order so that notional + entry_fee stays within the target equity fraction: qty = equity*pct / (price * pointvalue * (1 + commRate)), commRate = commission_value/100 for a percent commission. The engine omitted the (1+commRate) divisor and sized slightly large, drifting the compounding equity/position path. Proven from TV exports (clean-room probes at pct=10 AND pct=100, percent commission): TV reserves the commission for BOTH fractional AND all-in sizing — first-entry qty = equity/(price*(1+commRate)) to the lot step, 16/16. So the reservation is not gated on headroom; the fix is global. New helper reserve_percent_commission() applied to both PERCENT_OF_EQUITY sizing branches (calc_qty + calc_qty_for_type). Percent commission only; FIXED/CASH qty types and commission_value==0 are exact no-ops. Gate (full R7): ctest 79/79; run_corpus 239 exc/12 strong/1 anomaly, byte-identical (corpus has no percent-commission percent_of_equity, so no-op there); data sweep net-0: one strategy strong->excellent (its true tier once sized correctly), one excellent->strong. The latter is a TRUTHFUL re-grade, not a regression: it is percent_of_equity=100 all-in (a margin-cascade strategy) whose prior excellent was an artifact of the under-sizing bug coincidentally matching TV's proprietary liquidation path; under correct sizing its honest tier is strong. Improves per-trade sizing accuracy for the ~50 percent-commission strategies in the dataset. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/pineforge/engine.hpp | 15 ++++++++++++++- src/engine_orders.cpp | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 810bcae..8d20d80 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -837,6 +837,19 @@ class BacktestEngine { return floored < qty ? floored : qty; } + // TradingView reserves the entry commission when sizing percent_of_equity: + // it sizes the notional so that notional + entry_fee <= equity*pct, i.e. + // divides the sizing cash by (1 + commRate). Proven from TV exports for + // BOTH fractional (pct=10) and all-in (pct=100) sizing — the reservation is + // not gated on headroom (KI-52 probes: ki52-pct-equity-commission-{frac,allin}, + // first-entry qty = equity/(price*(1+commRate)) to the lot step, 16/16). Only + // percent commission reserves; cash-per-order/contract and a zero rate are + // exact no-ops, so FIXED/CASH qty types and commission_value_==0 are unchanged. + double reserve_percent_commission(double cash) const { + return (commission_type_ == CommissionType::PERCENT && commission_value_ > 0.0) + ? cash / (1.0 + commission_value_ / 100.0) : cash; + } + double calc_qty(double fill_price) const { switch (default_qty_type_) { case QtyType::FIXED: @@ -849,7 +862,7 @@ class BacktestEngine { // percent orders from the live equity snapshot so pyramid adds // and same-bar/re-entry sizing see unrealized PnL. double equity = current_equity() + open_profit(current_bar_.close); - double cash = equity * (default_qty_value_ / 100.0) / account_currency_fx_; + double cash = reserve_percent_commission(equity * (default_qty_value_ / 100.0)) / account_currency_fx_; // Reject (qty 0) on a non-finite / non-positive fill price — a // degenerate $0/NaN print must NOT size as the raw % number. return (std::isfinite(fill_price) && fill_price > 0) diff --git a/src/engine_orders.cpp b/src/engine_orders.cpp index e642623..b280940 100644 --- a/src/engine_orders.cpp +++ b/src/engine_orders.cpp @@ -31,7 +31,7 @@ double BacktestEngine::calc_qty_for_type(double fill_price, double qty_value, in } if (qty_type == static_cast(QtyType::PERCENT_OF_EQUITY)) { double equity = current_equity() + open_profit(current_bar_.close); - double cash = equity * (qty_value / 100.0); + double cash = reserve_percent_commission(equity * (qty_value / 100.0)); // Reject (qty 0) on a non-finite / non-positive fill price — a degenerate // $0/NaN print must NOT size as the raw % number (silent wrong-qty bug). // One contract's currency exposure is fill_price × pointvalue (1.0 for