From 4bd86c2ba39d64b97add2e45fb65e679545fc8e4 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 6 Sep 2026 23:04:33 +0800 Subject: [PATCH 1/2] fix: decline underfunded integer-lot budget ties --- src/engine_fills.cpp | 37 +++++++++ tests/CMakeLists.txt | 1 + tests/test_integer_flat_budget_tie.cpp | 104 +++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 tests/test_integer_flat_budget_tie.cpp diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index d5a13ec..716d125 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -4980,6 +4980,43 @@ void BacktestEngine::apply_filled_order_to_state( } } } + // Round17 ADXAE F: rule1 sizes from ten-digit rounded equity, so an + // integer-lot exact-budget tie need not satisfy the raw-equity floor + // invariant. At the zero-gap Apr28 open, Q768 * P12.31 is 9454.08 but + // frozen E is 9454.0799999999981; TV drops the entry. The general float + // allowance must not donate that missing budget in this pinned shape. + // Keep fractional lots, reversals, fees, other execution modes and all + // non-tie/gap admission rules on their established paths. + if (order.type == OrderType::MARKET && order.is_long && std::isnan(order.qty) + && !order.affordability_close_only && !order.sbmt_member + && position_side_ == PositionSide::FLAT + && order.created_position_side == PositionSide::FLAT + && !order.created_after_position_close_in_bar + && !order.created_by_same_id_replacement + && order.created_bar == bar_index_ - 1 && pending_orders_.size() == 1 + && default_qty_type_ == QtyType::PERCENT_OF_EQUITY + && default_qty_value_ == 100 && margin_long_ == 100 && pyramiding_ == 0 + && qty_step_ == 1 && syminfo_.pointvalue == 1 + && account_currency_fx_ == 1 && account_currency_fx_timestamps_.empty() + && order.sizing_fx == 1 && slippage_ == 0 && commission_value_ == 0 + && !process_orders_on_close_ && !calc_on_order_fills_ + && !coof_scheduler_active_ && !bar_magnifier_enabled_ + && !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE + && max_intraday_filled_orders_ == 0 + && risk_max_intraday_loss_ == 0 && risk_max_drawdown_ == 0 + && risk_max_cons_loss_days_ == 0 + && std::isfinite(order.sizing_equity) && order.sizing_equity > 0 + && std::isfinite(order.frozen_default_qty) && order.frozen_default_qty > 0 + && std::isfinite(order.sizing_price) && order.sizing_price >= 1 + && round_to_mintick(fill_price) == order.sizing_price) { + const double cost = order.frozen_default_qty * order.sizing_price; + if (std::isfinite(cost) && cost == tv_money_round(order.sizing_equity) + && cost > order.sizing_equity) { + decline_and_cancel(); + return; + } + } + // A reversal reduced to its closing leg (family R above, or the placement // half) needs no opening admission: the KI-54 / gap gates below judge an // OPENING quantity, and declining the close leg here would turn TV's diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fe4c1a2..0c07776 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -144,6 +144,7 @@ set(TEST_SOURCES test_famag_close_first_admission test_famag_opening_money test_taro_price_gap_admission + test_integer_flat_budget_tie test_taro_mc_close_residue test_live_position_market_gross_admission test_lower_tf_parse_extra diff --git a/tests/test_integer_flat_budget_tie.cpp b/tests/test_integer_flat_budget_tie.cpp new file mode 100644 index 0000000..bb8226e --- /dev/null +++ b/tests/test_integer_flat_budget_tie.cpp @@ -0,0 +1,104 @@ +// R17 ADXAE F: the Cloud Run receipt at 2026-04-28 14:15Z has frozen +// equity 9454.0799999999981, qty768, sizing/fill price12.31. The displayed +// decimal budget is9454.08, but actual E-Q*P=-1.8189894035458565e-12. +// TV skips that entry. This synthetic fixture isolates that rounded-sizing +// exact-budget shortfall, not a general change to admission float guards. +#include +#include +#include +#include +#include + +using namespace pineforge; +static int passed=0,failed=0; +#define CHECK(x) do { if(x) ++passed; else { ++failed; \ + std::printf("FAIL %s:%d: %s\n",__FILE__,__LINE__,#x); } } while(0) + +namespace { +constexpr double N=std::numeric_limits::quiet_NaN(); +enum class Mode { Default, Short, Explicit, Cash, Fixed, Half, Pooc, Coof, + Fee, Slip, Fx, Competing, Replacement, Fractional }; +class BudgetProbe : public BacktestEngine { +public: + BudgetProbe(double equity,Mode mode=Mode::Default):mode_(mode) { + initial_capital_=equity; + default_qty_type_=QtyType::PERCENT_OF_EQUITY; + default_qty_value_=mode==Mode::Half?50:100; + if(mode==Mode::Cash) {default_qty_type_=QtyType::CASH;default_qty_value_=9454.08;} + if(mode==Mode::Fixed) {default_qty_type_=QtyType::FIXED;default_qty_value_=768;} + margin_long_=margin_short_=100; + pyramiding_=0; + qty_step_=mode==Mode::Fractional?.01:1; + syminfo_.pointvalue=1; + set_syminfo_mintick(.01); + commission_type_=CommissionType::PERCENT; + commission_value_=mode==Mode::Fee?.01:0; + slippage_=mode==Mode::Slip?1:0; + process_orders_on_close_=mode==Mode::Pooc; + calc_on_order_fills_=mode==Mode::Coof; + account_currency_fx_=mode==Mode::Fx?2:1; + } + void on_bar(const Bar&) override { + if(bar_index_==0) { + if(mode_==Mode::Competing) strategy_order("Idle",true,1,N,1000); + if(mode_==Mode::Replacement) strategy_entry("E",true); + strategy_entry("E",mode_!=Mode::Short,N,N,mode_==Mode::Explicit?768:N); + } + if(bar_index_==1 && position_side_!=PositionSide::FLAT) + strategy_close("E"); + } + uint64_t fills() const { return broker_fill_event_seq_; } + double remaining() const { return signed_position_size(); } +private: + Mode mode_; +}; +const Bar bars[]={ + {12.27,12.315,12.25,12.305,1,1000}, + {12.31,12.32,12.31,12.315,1,2000}, + {12.4,12.4,12.4,12.4,1,3000}, +}; +void boundary(double equity,bool should_fill) { + BudgetProbe p(equity); + for(int repeat=0;repeat<2;++repeat) { + p.run(bars,3); + CHECK(p.last_error().empty()); + CHECK(p.trade_count()==(should_fill?1:0)); + CHECK(p.fills()==(should_fill?2:0)); + CHECK(p.remaining()==0); + if(should_fill && p.trade_count()==1) { + CHECK(p.get_trade(0).entry_bar_index==1); + CHECK(p.get_trade(0).exit_bar_index==2); + CHECK(p.get_trade(0).qty==768); + CHECK(std::abs(p.get_trade(0).entry_price-12.31)<1e-12); + CHECK(p.get_trade(0).commission==0); + } + } +} +void guards() { + const double e=std::nextafter(9454.08,0.0); + for(Mode m:{Mode::Short,Mode::Explicit,Mode::Cash,Mode::Fixed,Mode::Half, + Mode::Pooc,Mode::Coof,Mode::Fee,Mode::Slip,Mode::Fx, + Mode::Competing,Mode::Replacement,Mode::Fractional}) { + BudgetProbe p(e,m);p.run(bars,3);CHECK(p.last_error().empty()); + std::printf("guard %d trades %d fills %llu remaining %.9f",static_cast(m), + p.trade_count(),static_cast(p.fills()),p.remaining()); + for(int j=0;j::infinity()),true); + boundary(9455.08,true); + } + guards(); + std::printf("%d passed, %d failed\n",passed,failed); + return failed?1:0; +} From bfb8e0fb18ec720520f1f020b036786e084be5aa Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 6 Sep 2026 23:17:00 +0800 Subject: [PATCH 2/2] fix: honor default single-entry config in budget tie guard --- src/engine_fills.cpp | 5 ++++- tests/test_integer_flat_budget_tie.cpp | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 716d125..0d3740e 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -4995,7 +4995,10 @@ void BacktestEngine::apply_filled_order_to_state( && !order.created_by_same_id_replacement && order.created_bar == bar_index_ - 1 && pending_orders_.size() == 1 && default_qty_type_ == QtyType::PERCENT_OF_EQUITY - && default_qty_value_ == 100 && margin_long_ == 100 && pyramiding_ == 0 + && default_qty_value_ == 100 && margin_long_ == 100 + // Omitted Pine pyramiding retains the engine's single-entry default1; + // explicit0 has the same first-opening shape. Adds remain out of scope. + && pyramiding_ >= 0 && pyramiding_ <= 1 && qty_step_ == 1 && syminfo_.pointvalue == 1 && account_currency_fx_ == 1 && account_currency_fx_timestamps_.empty() && order.sizing_fx == 1 && slippage_ == 0 && commission_value_ == 0 diff --git a/tests/test_integer_flat_budget_tie.cpp b/tests/test_integer_flat_budget_tie.cpp index bb8226e..843f7ea 100644 --- a/tests/test_integer_flat_budget_tie.cpp +++ b/tests/test_integer_flat_budget_tie.cpp @@ -20,14 +20,16 @@ enum class Mode { Default, Short, Explicit, Cash, Fixed, Half, Pooc, Coof, Fee, Slip, Fx, Competing, Replacement, Fractional }; class BudgetProbe : public BacktestEngine { public: - BudgetProbe(double equity,Mode mode=Mode::Default):mode_(mode) { + BudgetProbe(double equity,Mode mode=Mode::Default,int pyramiding=0):mode_(mode) { initial_capital_=equity; default_qty_type_=QtyType::PERCENT_OF_EQUITY; default_qty_value_=mode==Mode::Half?50:100; if(mode==Mode::Cash) {default_qty_type_=QtyType::CASH;default_qty_value_=9454.08;} if(mode==Mode::Fixed) {default_qty_type_=QtyType::FIXED;default_qty_value_=768;} margin_long_=margin_short_=100; - pyramiding_=0; + // -1 leaves the inherited default untouched, as the real Pine source + // and its generated constructor do when pyramiding is omitted. + if(pyramiding>=0) pyramiding_=pyramiding; qty_step_=mode==Mode::Fractional?.01:1; syminfo_.pointvalue=1; set_syminfo_mintick(.01); @@ -57,8 +59,8 @@ const Bar bars[]={ {12.31,12.32,12.31,12.315,1,2000}, {12.4,12.4,12.4,12.4,1,3000}, }; -void boundary(double equity,bool should_fill) { - BudgetProbe p(equity); +void boundary(double equity,bool should_fill,int pyramiding) { + BudgetProbe p(equity,Mode::Default,pyramiding); for(int repeat=0;repeat<2;++repeat) { p.run(bars,3); CHECK(p.last_error().empty()); @@ -92,11 +94,11 @@ void guards() { } } // namespace int main(int argc,char**) { - if(argc==1) { - boundary(std::nextafter(9454.08,0.0),false); - boundary(9454.08,true); - boundary(std::nextafter(9454.08,std::numeric_limits::infinity()),true); - boundary(9455.08,true); + if(argc==1) for(int pyramiding:{0,-1}) { + boundary(std::nextafter(9454.08,0.0),false,pyramiding); + boundary(9454.08,true,pyramiding); + boundary(std::nextafter(9454.08,std::numeric_limits::infinity()),true,pyramiding); + boundary(9455.08,true,pyramiding); } guards(); std::printf("%d passed, %d failed\n",passed,failed);