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
6 changes: 6 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,11 @@ class BacktestEngine {
// fee-created, floor-zero one-contract fallback. Reversals, adds,
// explicit/priced/RAW orders and zero/CASH commission stay outside it.
bool opening_affordability_commissioned_default_flat_long_ = false;
// The queued event came from a successful omitted-qty, 100%-of-equity
// MARKET reversal from SHORT to LONG at 100% long margin with zero opening
// commission. TV applies a one-contract post-fill affordability trim when
// this exact reversal has a positive but sub-lot restore amount.
bool opening_affordability_default_long_reversal_ = false;
double opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
int64_t position_entry_time_ = 0;
Expand Down Expand Up @@ -2528,6 +2533,7 @@ class BacktestEngine {
void flip_market_position_to(const std::string& id, bool is_long,
double fill_price, double explicit_qty,
int explicit_qty_type,
bool explicit_qty_prequantized,
bool close_only,
uint64_t entry_incarnation);
void sequential_same_tick_reversal_fill(const std::string& id, bool is_long,
Expand Down
28 changes: 27 additions & 1 deletion src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -667,11 +667,14 @@ void BacktestEngine::process_margin_call(const Bar& bar) {
const bool opening_event_eligible = opening_affordability_eligible_;
const bool opening_event_commissioned_default_flat_long =
opening_affordability_commissioned_default_flat_long_;
const bool opening_event_default_long_reversal =
opening_affordability_default_long_reversal_;
const double opening_event_raw_fill_base =
opening_affordability_raw_fill_base_;
opening_affordability_pending_ = false;
opening_affordability_eligible_ = false;
opening_affordability_commissioned_default_flat_long_ = false;
opening_affordability_default_long_reversal_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();

Expand Down Expand Up @@ -875,7 +878,8 @@ void BacktestEngine::process_margin_call(const Bar& bar) {
double opening_floor_zero_fallback =
std::numeric_limits<double>::quiet_NaN();
if (opening_affordability && q_min <= kQtyEpsilon) {
if (fee_created_floor_zero_candidate
if ((fee_created_floor_zero_candidate
|| opening_event_default_long_reversal)
&& qty_step_ > 0.0
&& qty_step_ <= 1.0
&& raw_q_min > kQtyEpsilon
Expand Down Expand Up @@ -3213,6 +3217,7 @@ void BacktestEngine::apply_filled_order_to_state(
opening_affordability_pending_ = false;
opening_affordability_eligible_ = false;
opening_affordability_commissioned_default_flat_long_ = false;
opening_affordability_default_long_reversal_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
}
Expand Down Expand Up @@ -3265,6 +3270,26 @@ void BacktestEngine::apply_filled_order_to_state(
&& commission_value_ > 0.0
&& std::isfinite(new_opening_commission)
&& new_opening_commission > 0.0;
opening_affordability_default_long_reversal_ =
successful_fresh_open
&& position_side_before_fill == PositionSide::SHORT
&& order.created_position_side == PositionSide::SHORT
&& !order.created_after_position_close_in_bar
&& order.type == OrderType::MARKET
&& order.is_long
&& std::isnan(order.qty)
&& default_qty_type_ == QtyType::PERCENT_OF_EQUITY
&& std::abs(default_qty_value_ - 100.0) < 1e-12
&& std::isfinite(order.frozen_default_qty)
&& order.frozen_default_qty > kQtyEpsilon
&& std::isfinite(order.sizing_equity)
&& order.sizing_equity > 0.0
&& std::isfinite(order.sizing_price)
&& order.sizing_price > 0.0
&& std::isfinite(order.sizing_fx)
&& order.sizing_fx > 0.0
&& std::isfinite(new_opening_commission)
&& new_opening_commission == 0.0;
opening_affordability_raw_fill_base_ = fill_price;
}
}
Expand Down Expand Up @@ -3872,6 +3897,7 @@ void BacktestEngine::apply_raw_order_fill(PendingOrder& order, double fill_price
opening_affordability_pending_ = false;
opening_affordability_eligible_ = false;
opening_affordability_commissioned_default_flat_long_ = false;
opening_affordability_default_long_reversal_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
position_entry_time_ = current_bar_.timestamp;
Expand Down
13 changes: 12 additions & 1 deletion src/engine_orders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ void BacktestEngine::execute_market_entry(const std::string& id, bool is_long, d
// transaction whose whole broker movement is consumed by the close. Both
// close the live opposite position without opening their own leg.
flip_market_position_to(id, is_long, fill_price, explicit_qty, explicit_qty_type,
explicit_qty_prequantized,
/*close_only=*/close_only_opposite,
entry_incarnation);
}
Expand Down Expand Up @@ -580,6 +581,7 @@ void BacktestEngine::reset_position_state_to_flat() {
opening_affordability_pending_ = false;
opening_affordability_eligible_ = false;
opening_affordability_commissioned_default_flat_long_ = false;
opening_affordability_default_long_reversal_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
position_entry_time_ = 0;
Expand Down Expand Up @@ -633,6 +635,7 @@ void BacktestEngine::open_fresh_position(PositionSide requested, double fill_pri
opening_affordability_pending_ = false;
opening_affordability_eligible_ = false;
opening_affordability_commissioned_default_flat_long_ = false;
opening_affordability_default_long_reversal_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
position_entry_time_ = current_bar_.timestamp;
Expand Down Expand Up @@ -881,6 +884,7 @@ void BacktestEngine::close_opposite_then_enter(const std::string& id, bool is_lo
void BacktestEngine::flip_market_position_to(const std::string& id, bool is_long,
double fill_price, double explicit_qty,
int explicit_qty_type,
bool explicit_qty_prequantized,
bool close_only,
uint64_t entry_incarnation) {
// For the close we need exit slippage based on closing direction.
Expand Down Expand Up @@ -918,7 +922,14 @@ void BacktestEngine::flip_market_position_to(const std::string& id, bool is_long
return;
}

double new_qty = calc_qty_for_type(fill_price, explicit_qty, explicit_qty_type);
// Default-sized MARKET quantities are frozen and exchange-quantized at
// signal time. Every sibling dispatch path preserves that provenance;
// ordinary flips must not feed the frozen contracts through qty_step a
// second time (binary64 can turn 1.3410 into 1.3409 on the second floor).
// Explicit/FIXED quantities keep their normal single fill-side floor.
double new_qty = explicit_qty_prequantized
? explicit_qty
: calc_qty_for_type(fill_price, explicit_qty, explicit_qty_type);
Comment on lines +925 to +932
PositionSide requested = is_long ? PositionSide::LONG : PositionSide::SHORT;
open_fresh_position(requested, fill_price, new_qty, id, entry_incarnation);
}
Expand Down
85 changes: 85 additions & 0 deletions tests/test_default_qty_signal_freeze.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,89 @@ void test_explicit_true_flat_market_keeps_single_floor() {
CHECK_NEAR(eng.id_ledger_qty("BOUNDARY"), 6.2790, 1e-12);
}

// J/K. The same binary boundary must survive an ordinary in-position MARKET
// reversal. The default-sized long is quantized when it is placed; the later
// short->long flip must consume that frozen contracts value directly. The
// adjacent explicit-qty reversal remains un-frozen and therefore still gets
// exactly one ordinary fill-side floor.
Comment on lines +456 to +460
class FrozenReversalBoundaryProbe : public BacktestEngine {
public:
explicit FrozenReversalBoundaryProbe(bool explicit_reversal)
: explicit_reversal_(explicit_reversal) {
initial_capital_ = 6279.0000001;
default_qty_type_ = QtyType::PERCENT_OF_EQUITY;
default_qty_value_ = 100.0;
commission_value_ = 0.0;
qty_step_ = 0.0001;
margin_call_enabled_ = false;
}

void on_bar(const Bar& /*bar*/) override {
if (bar_index_ == 0) {
strategy_entry("SEED", false, kNaN, kNaN, 1.0);
} else if (bar_index_ == 1) {
if (explicit_reversal_) {
strategy_entry("BOUNDARY", true, kNaN, kNaN,
6.2790000001);
} else {
strategy_entry("BOUNDARY", true);
}
}
}

double one_floor_qty() const { return apply_qty_step(6.2790000001); }
double two_floor_qty() const { return apply_qty_step(one_floor_qty()); }
PositionSide position_side() const { return position_side_; }
double position_qty() const { return position_qty_; }
int live_lot_count() const { return static_cast<int>(pyramid_entries_.size()); }
double live_lot_qty() const {
return pyramid_entries_.empty() ? 0.0 : pyramid_entries_.front().qty;
}
double id_ledger_qty(const std::string& id) const {
const auto it = id_unclosed_qty_.find(id);
return it == id_unclosed_qty_.end() ? 0.0 : it->second;
}

private:
bool explicit_reversal_;
};

static std::vector<Bar> frozen_reversal_boundary_bars() {
return {
mk_bar(1000, 1000.0, 1000.0, 1000.0, 1000.0),
mk_bar(2000, 1000.0, 1000.0, 1000.0, 1000.0),
mk_bar(3000, 1000.0, 1000.0, 1000.0, 1000.0),
};
}

void test_frozen_market_reversal_is_not_refloored() {
std::printf("-- J: frozen MARKET reversal is not re-floored --\n");
FrozenReversalBoundaryProbe eng(/*explicit_reversal=*/false);
auto bars = frozen_reversal_boundary_bars();
eng.run(bars.data(), static_cast<int>(bars.size()));

CHECK_NEAR(eng.one_floor_qty(), 6.2790, 1e-12);
CHECK_NEAR(eng.two_floor_qty(), 6.2789, 1e-12);
CHECK(eng.position_side() == PositionSide::LONG);
CHECK_NEAR(eng.position_qty(), 6.2790, 1e-12);
CHECK(eng.live_lot_count() == 1);
CHECK_NEAR(eng.live_lot_qty(), 6.2790, 1e-12);
CHECK_NEAR(eng.id_ledger_qty("BOUNDARY"), 6.2790, 1e-12);
}

void test_explicit_market_reversal_keeps_single_floor() {
std::printf("-- K: explicit MARKET reversal keeps one qty floor --\n");
FrozenReversalBoundaryProbe eng(/*explicit_reversal=*/true);
auto bars = frozen_reversal_boundary_bars();
eng.run(bars.data(), static_cast<int>(bars.size()));

CHECK(eng.position_side() == PositionSide::LONG);
CHECK_NEAR(eng.position_qty(), 6.2790, 1e-12);
CHECK(eng.live_lot_count() == 1);
CHECK_NEAR(eng.live_lot_qty(), 6.2790, 1e-12);
CHECK_NEAR(eng.id_ledger_qty("BOUNDARY"), 6.2790, 1e-12);
}

} // namespace

int main() {
Expand All @@ -466,6 +549,8 @@ int main() {
test_reversal_bracket_binding_survives_freeze();
test_frozen_true_flat_market_dispatch_is_not_refloored();
test_explicit_true_flat_market_keeps_single_floor();
test_frozen_market_reversal_is_not_refloored();
test_explicit_market_reversal_keeps_single_floor();
std::printf("\n=== Results: %d passed, %d failed ===\n",
tests_passed, tests_failed);
return tests_failed == 0 ? 0 : 1;
Expand Down
94 changes: 94 additions & 0 deletions tests/test_margin_call.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,98 @@ static void test_reversal_captures_fresh_opening_state() {
CHECK(std::isnan(eng.opening_raw_base()));
}

// A frozen 100%-equity MARKET reversal can pass the signal-time admission
// check yet become microscopically underfunded after the carried short is
// realized at the next-open fill. TV restores this positive sub-step deficit
// by closing exactly one whole contract, not by treating it as dust. The
// numbers pin a source-faithful omitted-quantity reversal event and also
// exercise the frozen-quantity no-refloor path: 5.2798 must survive placement
// and flip.
class DefaultLongReversalFloorZeroProbe : public MCEngine {
public:
explicit DefaultLongReversalFloorZeroProbe(bool explicit_reversal)
: explicit_reversal_(explicit_reversal) {
initial_capital_ = 10000.0;
default_qty_type_ = QtyType::PERCENT_OF_EQUITY;
default_qty_value_ = 100.0;
commission_value_ = 0.0;
margin_long_ = 100.0;
margin_short_ = 100.0;
process_orders_on_close_ = false;
pyramiding_ = 1;
qty_step_ = 0.0001;
syminfo_mintick_ = 0.01;
}

void on_bar(const Bar& /*bar*/) override {
if (bar_index_ != 0) return;

// Seed the already-partially-liquidated short immediately before the
// TV-pinned reversal signal. Its open mark at 1841.70 freezes the new
// long at 5.2798; filling at 1841.71 realizes the remaining short loss
// and leaves a positive restore amount below the 0.0001 lot step.
position_side_ = PositionSide::SHORT;
position_cycle_seq_ = next_position_cycle_seq_++;
position_entry_price_ = 1821.96;
position_entry_time_ = current_bar_.timestamp - 1000;
position_qty_ = 5.2524;
position_entry_count_ = 1;
position_open_bar_ = -1;
trail_best_price_ = position_entry_price_;
net_profit_sum_ = -172.449012;
pyramid_entries_.clear();
id_unclosed_qty_.clear();
pyramid_entries_.push_back(
{position_entry_price_, position_entry_time_, position_qty_,
"SEED", -1});
pyramid_entries_.back().entry_incarnation = 1;
snapshot_entry_commission(pyramid_entries_.back());
id_unclosed_qty_["SEED"] = position_qty_;

if (explicit_reversal_) {
strategy_entry("L", true, kNaN, kNaN, 5.2798000001);
} else {
strategy_entry("L", true);
}
}

private:
bool explicit_reversal_;
};

static std::vector<Bar> default_long_reversal_floor_zero_bars() {
return {
mk_bar(1000, 1841.70, 1841.70, 1841.70, 1841.70, 1.0),
mk_bar(2000, 1841.71, 1841.71, 1841.71, 1841.71, 1.0),
};
}

static void test_default_long_reversal_floor_zero_closes_one_contract() {
std::printf("test_default_long_reversal_floor_zero_closes_one_contract\n");
DefaultLongReversalFloorZeroProbe eng(/*explicit_reversal=*/false);
auto bars = default_long_reversal_floor_zero_bars();
eng.run(bars.data(), static_cast<int>(bars.size()));

CHECK(eng.trade_count() == 2); // seed close + same-fill long MC trim
CHECK(margin_call_rows(eng) == 1);
CHECK(eng.exit_comment(1) == std::string("Margin call"));
CHECK(near(eng.entry_price(1), 1841.71));
CHECK(near(eng.exit_price(1), 1841.71));
CHECK(near(eng.trade_size(1), 1.0));
CHECK(near(eng.position_size(), 4.2798));
}

static void test_explicit_long_reversal_floor_zero_remains_dust() {
std::printf("test_explicit_long_reversal_floor_zero_remains_dust\n");
DefaultLongReversalFloorZeroProbe eng(/*explicit_reversal=*/true);
auto bars = default_long_reversal_floor_zero_bars();
eng.run(bars.data(), static_cast<int>(bars.size()));

CHECK(eng.trade_count() == 1); // seed close only; no broker trim
CHECK(margin_call_rows(eng) == 0);
CHECK(near(eng.position_size(), 5.2798));
}

// A reused engine handle must clear the per-position state before on_bar of
// the next run. Run 1 deliberately ends with an open position whose one-shot
// event was consumed; run 2 observes a clean state before opening a new RAW
Expand Down Expand Up @@ -1938,6 +2030,8 @@ int main() {
test_zero_qty_add_does_not_duplicate_cash_per_order_fee();
test_flat_clears_and_raw_fresh_reuses_state();
test_reversal_captures_fresh_opening_state();
test_default_long_reversal_floor_zero_closes_one_contract();
test_explicit_long_reversal_floor_zero_remains_dust();
test_run_reuse_clears_opening_state();
test_long_leveraged_margin_call();

Expand Down
Loading