Skip to content

Commit 76518c6

Browse files
committed
Admit narrow all-in gaps on rounded affordable price
1 parent 331a8bc commit 76518c6

3 files changed

Lines changed: 312 additions & 3 deletions

File tree

src/engine_fills.cpp

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4364,6 +4364,9 @@ void BacktestEngine::apply_filled_order_to_state(
43644364
// reversals are admitted on their actual fill, and paired reentries may
43654365
// fill from flat despite having been placed from a live position.
43664366
bool admitted_flat_on_frozen_sizing_price = false;
4367+
// This call only: a true-flat positive gap admitted on rounded price
4368+
// still needs its existing opening-margin checkpoint after the fill.
4369+
bool admitted_flat_on_price_gap_band = false;
43674370

43684371
if (order.type == OrderType::MARKET || order.type == OrderType::ENTRY) {
43694372
PositionSide requested = order.is_long ? PositionSide::LONG : PositionSide::SHORT;
@@ -4824,6 +4827,45 @@ void BacktestEngine::apply_filled_order_to_state(
48244827
std::isfinite(order.sizing_fx) && order.sizing_fx > 0.0
48254828
? order.sizing_fx
48264829
: active_account_currency_fx();
4830+
// Round 13 taro BTC, also pinned on ETH: for ordinary zero-fee
4831+
// default 100% market orders, a positive close-to-open gap compares
4832+
// the fill price with sig10(sig10(E_s) / Q), not exact Q*fill with E_s.
4833+
// BTC offsets -.00030 admit / -.00032 drop distinguish BOTH rounds.
4834+
// Keep the existing cost decision outside this directly pinned scope;
4835+
// in particular this does not widen tv_money_scope for other rules.
4836+
const bool price_gap_scope =
4837+
order.type == OrderType::MARKET
4838+
&& std::isnan(order.qty)
4839+
&& std::abs(default_qty_value_ - 100.0) < 1e-12
4840+
&& std::isfinite(margin_pct)
4841+
&& std::abs(margin_pct - 100.0) < 1e-12
4842+
&& qty_step_ > 0.0 && qty_step_ < 1.0
4843+
&& syminfo_.pointvalue == 1.0 && sizing_fx == 1.0
4844+
&& account_currency_fx_timestamps_.empty()
4845+
&& commission_type_ == CommissionType::PERCENT
4846+
&& commission_value_ == 0.0 && slippage_ == 0
4847+
&& !process_orders_on_close_ && !calc_on_order_fills_
4848+
&& !bar_magnifier_enabled_ && !coof_scheduler_active_
4849+
&& !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE
4850+
&& !order.created_during_coof_recalc
4851+
&& !order.created_after_position_close_in_bar
4852+
&& std::isfinite(order.sizing_equity)
4853+
&& std::isfinite(order.frozen_default_qty)
4854+
&& std::isfinite(order.sizing_price)
4855+
&& std::isfinite(order.sizing_mark)
4856+
&& std::isfinite(fill_price) && fill_price > order.sizing_price
4857+
&& ((position_side_ == PositionSide::FLAT
4858+
&& order.created_position_side == PositionSide::FLAT
4859+
&& !pending_flat_market_pair_is_live(order))
4860+
|| (reversal && order.created_position_side == position_side_
4861+
&& order.created_position_cycle_seq == position_cycle_seq_
4862+
&& pyramid_entries_.size() == 1));
4863+
const auto price_gap_affordable = [&]() {
4864+
const double affordable_price = tv_money_round(
4865+
tv_money_round(order.sizing_equity) / order.frozen_default_qty);
4866+
return std::isfinite(affordable_price)
4867+
&& affordable_price >= apply_fill_slippage(fill_price, order.is_long);
4868+
};
48274869
// Gap-reject (design-cntvxiao-gap-reject, PANEL-CLEARED; widened to
48284870
// commissioned entries by the round-7 family-H market-entry-admission
48294871
// pin, below): a high-level strategy.entry with omitted qty, sized
@@ -4930,8 +4972,12 @@ void BacktestEngine::apply_filled_order_to_state(
49304972
const double float_guard =
49314973
std::max(1e-9, std::abs(order.sizing_equity) * 1e-12);
49324974
if (gap_notional > order.sizing_equity + float_guard) {
4933-
decline_and_cancel();
4934-
return;
4975+
if (price_gap_scope && price_gap_affordable()) {
4976+
admitted_flat_on_price_gap_band = true;
4977+
} else {
4978+
decline_and_cancel();
4979+
return;
4980+
}
49354981
}
49364982
}
49374983
// A same-direction add (fractional OR all-in) IS gated, against
@@ -5049,7 +5095,10 @@ void BacktestEngine::apply_filled_order_to_state(
50495095
* sizing_fx
50505096
* (margin_pct / 100.0));
50515097
}
5052-
if (required_margin > free_funds + epsilon) {
5098+
const bool price_band_admitted_reversal =
5099+
reversal && price_gap_scope && price_gap_affordable();
5100+
if (required_margin > free_funds + epsilon
5101+
&& !price_band_admitted_reversal) {
50535102
// design-declined-reversal-close-leg: ONLY the reversal decline
50545103
// triggers close-leg suppression (admit_price == slipped fill,
50555104
// MARKET). The same_dir add decline (probe65 shape) and the
@@ -5727,6 +5776,11 @@ void BacktestEngine::apply_filled_order_to_state(
57275776
&& !order.created_after_position_close_in_bar
57285777
&& position_side_before_fill == PositionSide::FLAT
57295778
&& admitted_flat_on_frozen_sizing_price
5779+
// Newly price-band-admitted positive gaps can have a real
5780+
// fill deficit on either side (BTC/ETH flat MC1 tapes).
5781+
// Use the existing event/quantizer; exact-affordable fills
5782+
// keep the historical exemption and no persistent flag.
5783+
&& !admitted_flat_on_price_gap_band
57305784
&& std::isfinite(new_opening_commission)
57315785
&& new_opening_commission == 0.0;
57325786

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ set(TEST_SOURCES
142142
test_close_percent_calltime_basis
143143
test_famag_close_first_admission
144144
test_famag_opening_money
145+
test_taro_price_gap_admission
145146
test_live_position_market_gross_admission
146147
test_lower_tf_parse_extra
147148
test_ta_ma_warmup_extra
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
* Round 13 taro BTC: nested price-scale admission at an actual gap fill.
3+
* TV tapes under state/r13-taro-audit and state/r13-taro-btc:
4+
* BTC July03 Es910872.3625532,Q8.31589,close109533.95,fill109533.96.
5+
* Offsets +.0001,0,-.0001,-.00030 admit; -.00032,-.00036,-.001 drop.
6+
* -.00032 distinguishes sig10(sig10(E)/Q) from sig10(E/Q).
7+
* ETH Apr01 Q10,close1821.47,fill1821.48: C18214.799997 admits
8+
* (MC1 at fill, remainder9 on either side); C18214.799994 drops.
9+
* Small synthetic fixtures preserve those prices and source calls. No
10+
* corpus/feed/strategy/verifier is loaded. Existing exact-affordable and
11+
* non-scope contracts must keep their old behavior.
12+
*/
13+
#include <cmath>
14+
#include <cstdio>
15+
#include <limits>
16+
#include <vector>
17+
#include <pineforge/bar.hpp>
18+
#include <pineforge/engine.hpp>
19+
20+
using namespace pineforge;
21+
static int passed = 0, failed = 0;
22+
#define CHECK(expr) do { if (expr) ++passed; else { \
23+
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); ++failed; \
24+
} } while (0)
25+
26+
namespace {
27+
constexpr double kNaN = std::numeric_limits<double>::quiet_NaN();
28+
bool near(double a, double b, double tol = 1e-7) {
29+
return std::abs(a-b) < tol;
30+
}
31+
struct Config {
32+
double capital = 910872.3625532;
33+
double step = 0.00001;
34+
double tick = 0.01;
35+
bool flat = true;
36+
bool seed_long = true;
37+
double seed_qty = 8.31589;
38+
bool is_long = false;
39+
int signal_bar = 2;
40+
bool explicit_qty = false;
41+
bool raw = false;
42+
double fee = 0.0;
43+
bool provider = false;
44+
bool pooc = false;
45+
bool coof = false;
46+
bool magnifier = false;
47+
bool close_first = false;
48+
};
49+
class Probe : public BacktestEngine {
50+
public:
51+
explicit Probe(Config config) : cfg_(config) {
52+
initial_capital_ = config.capital;
53+
default_qty_type_ = QtyType::PERCENT_OF_EQUITY;
54+
default_qty_value_ = 100;
55+
commission_type_ = CommissionType::PERCENT;
56+
commission_value_ = config.fee;
57+
margin_long_ = margin_short_ = 100;
58+
pyramiding_ = 1;
59+
slippage_ = 0;
60+
qty_step_ = config.step;
61+
syminfo_.pointvalue = 1;
62+
set_syminfo_mintick(config.tick);
63+
process_orders_on_close_ = config.pooc;
64+
calc_on_order_fills_ = config.coof;
65+
set_margin_call_enabled(true);
66+
if (config.provider) {
67+
const int64_t times[] = {1000};
68+
const double rates[] = {1};
69+
CHECK(set_account_currency_fx_series(times, rates, 1));
70+
}
71+
}
72+
void on_bar(const Bar&) override {
73+
if (!cfg_.flat && bar_index_ == 0)
74+
strategy_entry("Seed", cfg_.seed_long, kNaN, kNaN,
75+
cfg_.seed_qty, "SEED");
76+
if (bar_index_ == cfg_.signal_bar) {
77+
if (cfg_.close_first) strategy_close("Seed");
78+
if (cfg_.raw)
79+
strategy_order("Next", cfg_.is_long, kNaN);
80+
else
81+
strategy_entry("Next", cfg_.is_long, kNaN, kNaN,
82+
cfg_.explicit_qty ? 8.31589 : kNaN, "ENTRY");
83+
}
84+
if (bar_index_ == cfg_.signal_bar+1) strategy_close_all();
85+
}
86+
const std::vector<Trade>& rows() const { return trades_; }
87+
double position() const { return signed_position_size(); }
88+
bool magnifier() const { return cfg_.magnifier; }
89+
private:
90+
Config cfg_;
91+
};
92+
std::vector<Bar> btc() {
93+
return {
94+
{109393.88,109393.88,109393.88,109393.88,1,1000},
95+
{109393.88,109547.32,109382.93,109547.17,1,2000},
96+
{109547.16,109580,109471.6,109533.95,1,3000},
97+
{109533.96,109533.96,109377.57,109377.57,1,4000},
98+
{109377.57,109377.57,109377.57,109377.57,1,5000},
99+
{109377.57,109377.57,109377.57,109377.57,1,6000},
100+
};
101+
}
102+
std::vector<Bar> eth() {
103+
return {
104+
{1821.47,1821.47,1821.47,1821.47,1,1000},
105+
{1821.48,1829.36,1820.11,1826.38,1,2000},
106+
{1826.37,1826.37,1826.37,1826.37,1,3000},
107+
{1826.37,1826.37,1826.37,1826.37,1,4000},
108+
};
109+
}
110+
void run(Probe& engine, const std::vector<Bar>& bars) {
111+
if (engine.magnifier())
112+
engine.run(bars.data(), static_cast<int>(bars.size()), "1", "1", true, 4,
113+
MagnifierDistribution::ENDPOINTS);
114+
else
115+
engine.run(bars.data(), static_cast<int>(bars.size()));
116+
CHECK(engine.last_error().empty());
117+
CHECK(near(engine.position(),0));
118+
}
119+
void reversal_offsets() {
120+
struct Offset { double delta; bool admit; };
121+
const Offset cases[] = {{.0001,true},{0,true},{-.0001,true},
122+
{-.00030,true},{-.00032,false},{-.00036,false},{-.001,false}};
123+
for (const auto& c : cases) {
124+
Config cfg;
125+
cfg.flat = false;
126+
cfg.capital = 909707.5558409+c.delta;
127+
Probe engine(cfg);
128+
run(engine,btc());
129+
const auto& rows=engine.rows();
130+
CHECK(rows.size() == (c.admit ? 2u : 1u));
131+
if (rows.empty()) continue;
132+
CHECK(near(rows[0].qty,8.31589));
133+
CHECK(rows[0].exit_time == (c.admit ? 4000 : 5000));
134+
if (c.admit && rows.size()==2) {
135+
CHECK(near(rows[1].qty,8.31589));
136+
CHECK(near(rows[1].entry_price,109533.96));
137+
CHECK(rows[1].entry_time==4000);
138+
CHECK(rows[1].exit_time==5000);
139+
}
140+
}
141+
}
142+
void opening_trim(Config cfg, const std::vector<Bar>& bars,
143+
double fill, double qty) {
144+
Probe engine(cfg);
145+
// Reuse the handle: a rescued-gap event cannot survive reset or replay.
146+
for (int repeat=0;repeat<2;++repeat) {
147+
run(engine,bars);
148+
const auto& rows=engine.rows();
149+
CHECK(rows.size()==2);
150+
if (rows.size()!=2) continue;
151+
CHECK(rows[0].exit_comment=="Margin call");
152+
CHECK(near(rows[0].qty,1));
153+
CHECK(near(rows[0].entry_price,fill));
154+
CHECK(near(rows[0].exit_price,fill));
155+
CHECK(rows[0].entry_time==rows[0].exit_time);
156+
CHECK(rows[0].entry_time==(cfg.signal_bar+2)*1000);
157+
CHECK(near(rows[1].qty,qty-1));
158+
CHECK(rows[1].exit_comment!="Margin call");
159+
CHECK(rows[1].exit_time==(cfg.signal_bar+3)*1000);
160+
CHECK(near(rows[0].qty+rows[1].qty,qty));
161+
}
162+
}
163+
void flat_and_eth_controls() {
164+
for (bool is_long : {false,true}) {
165+
Config cfg; cfg.is_long=is_long;
166+
opening_trim(cfg,btc(),109533.96,8.31589);
167+
cfg.capital=18214.799997;cfg.step=.0001;cfg.signal_bar=0;
168+
opening_trim(cfg,eth(),1821.48,10);
169+
}
170+
Config drop;drop.capital=18214.799994;drop.step=.0001;drop.signal_bar=0;
171+
Probe rejected(drop);run(rejected,eth());CHECK(rejected.rows().empty());
172+
173+
// Exactly affordable flat Long stays exempt: no new fill trim.
174+
Config affordable;affordable.capital=910872.3626532;affordable.is_long=true;
175+
Probe covered(affordable);run(covered,btc());CHECK(covered.rows().size()==1);
176+
if (!covered.rows().empty()) {
177+
CHECK(covered.rows()[0].exit_comment!="Margin call");
178+
CHECK(near(covered.rows()[0].qty,8.31589));
179+
}
180+
}
181+
void opposite_reversal() {
182+
for (bool admit : {true,false}) {
183+
Config cfg;cfg.flat=false;cfg.seed_long=false;cfg.seed_qty=1;
184+
cfg.is_long=true;cfg.capital=admit?911012.4325532:911012.4322332;
185+
Probe engine(cfg);run(engine,btc());const auto& rows=engine.rows();
186+
CHECK(rows.size()==(admit?3u:1u));
187+
if (rows.empty()) continue;
188+
CHECK(near(rows[0].qty,1));
189+
CHECK(rows[0].exit_time==(admit?4000:5000));
190+
if (admit && rows.size()==3) {
191+
CHECK(rows[1].exit_comment=="Margin call");
192+
CHECK(near(rows[1].qty,1));
193+
CHECK(near(rows[1].exit_price,109533.96));
194+
CHECK(near(rows[2].qty,7.31589));
195+
}
196+
}
197+
}
198+
void scope_controls() {
199+
// These all miss the newly pinned scope and retain exact-cost decline.
200+
Config explicit_qty;explicit_qty.explicit_qty=true;
201+
Probe explicit_order(explicit_qty);run(explicit_order,btc());
202+
CHECK(explicit_order.rows().empty());
203+
Config provider;provider.provider=true;
204+
Probe converted(provider);run(converted,btc());CHECK(converted.rows().empty());
205+
Config commissioned;commissioned.fee=.000001;
206+
Probe fee(commissioned);run(fee,btc());CHECK(fee.rows().empty());
207+
Config continuous;continuous.step=0;
208+
Probe no_lot(continuous);run(no_lot,btc());CHECK(no_lot.rows().empty());
209+
Config raw;raw.flat=false;raw.capital=909707.5558409;raw.raw=true;
210+
Probe raw_close(raw);run(raw_close,btc());CHECK(raw_close.rows().size()==1);
211+
if (!raw_close.rows().empty()) CHECK(raw_close.rows()[0].exit_time==4000);
212+
Config coof;
213+
coof.coof=true;
214+
Probe recalc(coof);run(recalc,btc());CHECK(recalc.rows().empty());
215+
Config mag;
216+
mag.magnifier=true;
217+
Probe magnifier(mag);run(magnifier,btc());CHECK(magnifier.rows().empty());
218+
Config pooc;
219+
pooc.pooc=true;pooc.is_long=true;
220+
Probe at_close(pooc);run(at_close,btc());CHECK(at_close.rows().size()==1);
221+
if (!at_close.rows().empty()) CHECK(at_close.rows()[0].exit_comment!="Margin call");
222+
// An explicit source-order close-first pair keeps its existing bypass
223+
// of the reversal gap gate; it is not a price-band rescued reversal.
224+
Config cf;cf.flat=false;cf.capital=909707.5555209;cf.close_first=true;
225+
Probe close_first(cf);run(close_first,btc());CHECK(close_first.rows().size()==2);
226+
if (close_first.rows().size()==2) {
227+
CHECK(close_first.rows()[0].exit_time==4000);
228+
CHECK(close_first.rows()[1].entry_time==4000);
229+
}
230+
}
231+
void historical_eth_pins() {
232+
// famr3e-Ex010-04010000: actual all-in +1-tick gap decline remains.
233+
Config cfg;cfg.capital=999999.9634;cfg.is_long=true;cfg.step=.0001;
234+
cfg.signal_bar=0;
235+
Probe gap(cfg);run(gap,eth());CHECK(gap.rows().empty());
236+
237+
// famr3e-Eh-03312315: actual zero-gap admit548.5884 stays unchanged.
238+
cfg.capital=999999.8514;
239+
Probe flat(cfg);
240+
const std::vector<Bar> bars={
241+
{1822.86,1822.86,1822.86,1822.86,1,1000},
242+
{1822.86,1822.86,1822.86,1822.86,1,2000},
243+
{1824.93,1824.93,1824.93,1824.93,1,3000},
244+
{1824.93,1824.93,1824.93,1824.93,1,4000}};
245+
run(flat,bars);CHECK(flat.rows().size()==1);
246+
if (!flat.rows().empty()) CHECK(near(flat.rows()[0].qty,548.5884));
247+
}
248+
} // namespace
249+
int main() {
250+
reversal_offsets();flat_and_eth_controls();opposite_reversal();
251+
scope_controls();historical_eth_pins();
252+
std::printf("%d passed, %d failed\n",passed,failed);
253+
return failed?1:0;
254+
}

0 commit comments

Comments
 (0)