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
13 changes: 9 additions & 4 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ enum class PositionSide { FLAT, LONG, SHORT };
// (entry-leg admission), log-20260905t180249z-10358e84 (margin-call
// trigger)): TradingView's broker carries MONEY at TEN SIGNIFICANT DIGITS,
// half-up — 3 decimals at >= 1e6, 4 decimals below. Five consequences are
// pinned and implemented, all scoped by tv_money_scope (below):
// pinned and implemented, with the per-rule scopes described below:
// 1. SIZING — a default percent_of_equity order is floored from the
// ROUNDED equity (calc_qty): the lot count flips one lot early/late
// when the exact equity is within half a money unit of a lot boundary
Expand Down Expand Up @@ -62,7 +62,7 @@ enum class PositionSide { FLAT, LONG, SHORT };
// the every-bar sensors have E_s < round(cost), 0/3086 admissions do.
// 3. MARGIN CALL — a margin-100 LONG is liquidated (one contract, the
// broker's minimum) at the first bar path point where the exact equity
// is <= the position value ROUNDED to 10 significant digits
// is below the position value ROUNDED to 10 significant digits
// (process_margin_call): revL L18..L25 16/16, taro 6/6 one-unit trims.
// Where equity ~ 1e6 and a lot is worth ~0.011 (EURUSD: qty step 0.01 at
// price ~1.1) the 0.0005 rounding crosses a lot boundary on ~9 % of all-in
Expand All @@ -73,8 +73,10 @@ enum class PositionSide { FLAT, LONG, SHORT };
// 4583, the float-accumulated ledger's 4584.000000000001 or a 1e-6-nudged
// floor gives 4584, one share the account cannot pay at the 238.78 fill), so
// rule 1 sizes every lot-stepped instrument (tv_money_lot_sizing); rules
// 2, 3 and 5 stay scoped by tv_money_scope — outside it the exact fill-price
// admission already decides (1094521.681 -> Q 4584 dropped on AAPL).
// 2 and 5 stay scoped by tv_money_scope — outside it the exact fill-price
// admission already decides (1094521.681 -> Q 4584 dropped on AAPL). Rule 3
// additionally covers ordinary MARKET-opened, fee/slippage-free single-position
// fractional unit-pointvalue/same-currency books with lot value >=1 (R21 pins).
// 5. WHOLE-ORDER DROP (round 9 family R follow-up, campaign notes
// log-20260905t205824z-af397c83 and log-20260905t210117z-ab914192;
// the residual of log-20260905t180249z-4bd857ad): once the rounded-cost
Expand Down Expand Up @@ -199,6 +201,9 @@ struct PyramidEntry {
// physical lot by FIFO. Its logical slot cannot later be released merely
// because an owner-bound bracket closes the last physical remainder.
bool bracket_slot_shadowed = false;
// Exact ordinary MARKET fill at the next bar open. Priced/RAW entries
// cannot infer this provenance from an equal numeric entry price.
bool ordinary_market_open = false;
};

struct Trade {
Expand Down
34 changes: 32 additions & 2 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,12 @@ void BacktestEngine::process_margin_call(const Bar& bar) {
// C 1.17653, 09-17 09:45Z C 1.18457, 10-03 01:45Z H 1.17293, 11-25 06:45Z H
// 1.15217, 12-30 06:15Z H 1.17798); 0 false fires over 612 every-bar sensor
// longs (their equity sits below 1e6). One broker event per bar, plain
// close-calc dispatch only (the pinned tapes), scoped by tv_money_scope.
// close-calc dispatch only (the pinned tapes). R21 also pins fee/slippage-free
// single fractional lots opened by an ordinary MARKET entry, whose minimum
// lot is worth >=1 account unit:
// BTC Q10.68387 at105380.96, cash0.0003048999 fires1 at low105355.26;
// +0.0001cash does not. BTC Q10.68388 and XAU Q300.01 with0.00001cash
// fire at the opening price itself. Other money/admission scopes stay fixed.
// Shorts keep the finite-price cascade: the same rounding moves their
// liquidation price by ~1e-10, below a tick.
//
Expand Down Expand Up @@ -1555,7 +1560,16 @@ bool BacktestEngine::tv_money_long_margin_call(const Bar& bar,
|| stream_warmup_mode_ || stream_phase_ != StreamPhase::IDLE) {
return false;
}
if (!std::isfinite(bar.close) || !tv_money_scope(bar.close)) return false;
const bool legacy_money_scope = tv_money_scope(bar.close);
const bool high_value_fractional_scope = !legacy_money_scope
&& qty_step_ > 0.0 && qty_step_ < 1.0
&& !process_orders_on_close_ && commission_value_ == 0.0 && slippage_ == 0
&& syminfo_.pointvalue == 1.0 && active_account_currency_fx() == 1.0
&& pyramiding_ >= 0 && pyramiding_ <= 1
&& position_entry_count_ == 1 && pyramid_entries_.size() == 1
&& pyramid_entries_.front().ordinary_market_open;
if (!std::isfinite(bar.close) || bar.close <= 0.0
|| !(legacy_money_scope || high_value_fractional_scope)) return false;
// Pinned on same-currency accounts only. A converted (quote -> account
// FX series) ledger is cent-rounded in TradingView's export and already
// carries its own sub-half-cent tolerance on the opening path; the
Expand Down Expand Up @@ -1584,6 +1598,11 @@ bool BacktestEngine::tv_money_long_margin_call(const Bar& bar,
int seg = static_cast<int>(std::floor(fill_pos + internal::kPathPosEps));
if (seg < 0) seg = 0;
start = seg + 1;
// The newly covered high-value fractional pins include an immediate
// post-entry valuation at O. Only a fill actually at O can inspect it;
// the exact MARKET-origin marker excludes priced and RAW entries.
if (high_value_fractional_scope && fill_pos == 0.0
&& position_entry_price_ == round_to_mintick(bar.open)) start = 0;
}
double fire_price = std::numeric_limits<double>::quiet_NaN();
double deficit = 0.0;
Expand Down Expand Up @@ -5724,6 +5743,17 @@ void BacktestEngine::apply_filled_order_to_state(
}

if (primary_fill_applied) {
if (order.type == OrderType::MARKET
&& !process_orders_on_close_ && !calc_on_order_fills_
&& !bar_magnifier_enabled_ && !coof_scheduler_active_
&& !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE
&& order.created_bar == bar_index_ - 1
&& pyramid_entries_.size() == 1
&& pyramid_entries_.front().entry_incarnation == order.incarnation
&& pyramid_entries_.front().entry_bar_index == bar_index_
&& position_entry_price_ == round_to_mintick(bar.open)) {
pyramid_entries_.front().ordinary_market_open = true;
}
++broker_fill_event_seq_;
}

Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ set(TEST_SOURCES
test_metrics
test_drawing
test_margin_call
test_high_value_fractional_money_margin
test_margin_call_intrabar_chronology
test_margin_call_trail_exit_chronology
test_margin_call_1x_long_entry_fill
Expand Down
137 changes: 137 additions & 0 deletions tests/test_high_value_fractional_money_margin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// R21 covered TV capital controls: fractional lots worth more than one unit
// still receive rounded-money margin calls. Synthetic three-bar fixtures pin
// the BTC low waypoint and XAU opening valuation without any corpus execution.
#include <pineforge/engine.hpp>
#include <cmath>
#include <cstdio>
#include <limits>
#include <vector>

using namespace pineforge;
namespace {
constexpr double qnan=std::numeric_limits<double>::quiet_NaN();
int failures=0,passed=0;
#define CHECK(x) do {if(x)++passed;else{++failures;std::printf("FAIL %d %s\n",__LINE__,#x);}}while(0)
bool near(double a,double b){return std::abs(a-b)<1e-7;}
class Probe : public BacktestEngine {
public:
double explicit_qty=qnan;
double entry_limit=qnan, entry_stop=qnan;
bool raw_order=false;
Probe(double capital,double step,double tick) {
initial_capital_=capital;default_qty_type_=QtyType::PERCENT_OF_EQUITY;
default_qty_value_=100;qty_step_=step;syminfo_mintick_=tick;
commission_value_=0;slippage_=0;
}
void fixed_default(){default_qty_type_=QtyType::FIXED;default_qty_value_=1;}
void small_fee(){commission_type_=CommissionType::CASH_PER_ORDER;commission_value_=0.000001;}
void one_tick_slippage(){slippage_=1;}
void constant_fx(){account_currency_fx_=2;}
void double_point_value(){syminfo_.pointvalue=2;}
void larger_pyramid_cap(){pyramiding_=2;}
void on_bar(const Bar&) override {
if(bar_index_==0){
if(raw_order)strategy_order("L",true,explicit_qty,entry_limit,entry_stop);
else strategy_entry("L",true,entry_limit,entry_stop,explicit_qty);
}
if(bar_index_==1)strategy_close("L");
}
int margin_count()const{
int n=0;for(const auto&t:trades_)if(t.exit_comment=="Margin call")++n;return n;
}
const Trade* margin()const{
for(const auto&t:trades_)if(t.exit_comment=="Margin call")return &t;return nullptr;
}
double final_position()const{return signed_position_size();}
};
std::vector<Bar> btc(){return {
{105157.56,105481.78,105157.56,105380.95,100,1000},
{105380.96,105504.67,105355.26,105496.54,100,2000},
{105496.54,105737.84,105494.72,105737.82,100,3000}};}
std::vector<Bar> xau(){return {
{3145.45,3146.31,3130.63,3132.08,100,1000},
{3132.085,3132.88,3126.665,3130.84,100,2000},
{3130.83,3138.26,3130.15,3136.37,100,3000}};}
void run(Probe&p,const std::vector<Bar>&bars){p.run(bars.data(),static_cast<int>(bars.size()));}
void check_margin(Probe&p,double price,int expected){
CHECK(p.margin_count()==expected);CHECK(near(p.final_position(),0));
if(expected&&p.margin()){
CHECK(near(p.margin()->qty,1));CHECK(near(p.margin()->exit_price,price));
CHECK(p.margin()->exit_time==2000);
}
}
void test_btc_cash_boundary(){
for(double offset : {-0.0001,0.0,0.0001,0.001}){
Probe p(1125876.4774201+offset,0.00001,0.01);
run(p,btc());check_margin(p,105355.26,offset<=0?1:0);
}
Probe fixed(1125876.4774201,0.00001,0.01);
fixed.fixed_default();fixed.explicit_qty=10.68387;
run(fixed,btc());check_margin(fixed,105355.26,1);
run(fixed,btc());check_margin(fixed,105355.26,1); // reuse
}
void test_xau_opening_cash_boundary(){
for(double cash : {0.00001,0.001}){
Probe p(939656.82085+cash,0.01,0.001);
p.fixed_default();p.explicit_qty=300.01;
run(p,xau());check_margin(p,3132.085,cash<0.0001?1:0);
}
}
void test_btc_opening_valuation(){
Probe p(1125877.5309348,0.00001,0.01);
p.fixed_default();p.explicit_qty=10.68388;
run(p,btc());check_margin(p,105380.96,1);
}
void test_continuous_and_integer_excluded(){
Probe continuous(1125876.4774201,0,0.01);
continuous.fixed_default();continuous.explicit_qty=10.68387;
run(continuous,btc());check_margin(continuous,0,0);
Probe integer(1125876.4774201,1,0.01);
integer.fixed_default();integer.explicit_qty=10;
run(integer,btc());check_margin(integer,0,0);
}
void test_other_money_paths_preserved(){
Probe fee(1125876.4774211,0.00001,0.01);
fee.fixed_default();fee.explicit_qty=10.68387;fee.small_fee();
run(fee,btc());check_margin(fee,0,0);
Probe slipped(1125876.5842588,0.00001,0.01);
slipped.fixed_default();slipped.explicit_qty=10.68387;slipped.one_tick_slippage();
run(slipped,btc());check_margin(slipped,0,0);
for(bool fx : {false,true}){
Probe converted(2251752.9542404,0.00001,0.01);
converted.fixed_default();converted.explicit_qty=10.68387;
if(fx)converted.constant_fx();else converted.double_point_value();
run(converted,btc());check_margin(converted,0,0);
}
Probe cap(1125876.4774201,0.00001,0.01);
cap.fixed_default();cap.explicit_qty=10.68387;cap.larger_pyramid_cap();
run(cap,btc());check_margin(cap,0,0);
}
void test_priced_entries_do_not_enter_market_extension(){
for(bool stop : {false,true}){
Probe p(1125876.4774201,0.00001,0.01);
p.fixed_default();p.explicit_qty=10.68387;
if(stop)p.entry_stop=105380.95;else p.entry_limit=105380.97;
run(p,btc());check_margin(p,0,0);
}
Probe p(1129689.1229734,0.00001,0.01);
p.fixed_default();p.explicit_qty=10.68387;
p.entry_stop=105737.83;p.entry_limit=105737.82;
const std::vector<Bar> bars={
{105496.54,105737.84,105494.72,105737.82,100,1000},
{105737.82,105737.83,105458.83,105476.19,100,2000},
{105476.19,105551.68,105439.73,105490.68,100,3000}};
run(p,bars);check_margin(p,0,0);
Probe raw(1125876.4774201,0.00001,0.01);
raw.fixed_default();raw.explicit_qty=10.68387;raw.raw_order=true;
run(raw,btc());check_margin(raw,0,0);
}
}
int main(){
test_btc_cash_boundary();test_xau_opening_cash_boundary();
test_btc_opening_valuation();
test_continuous_and_integer_excluded();
test_other_money_paths_preserved();
test_priced_entries_do_not_enter_market_extension();
std::printf("%d passed, %d failed\n",passed,failures);return failures?1:0;
}
Loading