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
20 changes: 14 additions & 6 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -648,19 +648,27 @@ class BacktestEngine {
// 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;
// A close-then-open SHORT whose omitted 100%-of-equity MARKET order was
// placed while LONG after a full strategy.close in the same Pine
// evaluation, and therefore reaches the fill from FLAT with zero
// deferred-flip carry. The one-shot bit queues the fill-price affordability
// pass and then one ordinary adverse-price pass on that same bar, even when
// the opening check itself is a no-op.
// An eligible omitted 100%-of-equity MARKET short opening. This covers
// both the close-then-open shape that reaches the fill from FLAT and the
// direct LONG-to-SHORT auto-reversal shape. The one-shot bit queues the
// fill-price affordability pass and then one ordinary adverse-price pass
// on that same bar, even when the opening check itself is a no-op.
bool close_then_short_opening_requires_adverse_retry_ = false;
Comment on lines +652 to 656
// Position-lifecycle provenance for a commissioned, omitted-qty,
// percent-of-equity=100 MARKET short that fills from flat at 100% short
// margin. Unlike the one-shot close-then-short opening event, this remains
// live across partial margin trims and clears when the position lifecycle
// ends or a later add changes its shape.
bool commissioned_all_in_market_short_lifecycle_ = false;
// Position-lifecycle provenance for an omitted-qty,
// percent-of-equity=100 MARKET short opened by a direct LONG-to-SHORT
// auto-reversal at 100% short margin. It carries no commission or
// flat-admission requirement. Broker margin-call reductions preserve the
// lifecycle; a script-driven reduction, add, full close, or fresh position
// clears it. At a later finite-price floor-zero margin call, this lifecycle
// selects the one-contract fallback only when the configured full-residual
// interpretation is off.
bool default_market_direct_short_reversal_lifecycle_ = false;
double opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
int64_t position_entry_time_ = 0;
Expand Down
58 changes: 53 additions & 5 deletions src/engine_fills.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,8 @@ void BacktestEngine::process_margin_call(const Bar& bar) {
if ((fee_created_floor_zero_candidate
|| opening_event_default_long_reversal
|| (opening_event_default_short_reversal
&& commissioned_all_in_market_short_lifecycle_))
&& (commissioned_all_in_market_short_lifecycle_
|| default_market_direct_short_reversal_lifecycle_)))
&& qty_step_ > 0.0
&& qty_step_ <= 1.0
&& raw_q_min > kQtyEpsilon
Expand Down Expand Up @@ -968,7 +969,10 @@ void BacktestEngine::process_margin_call(const Bar& bar) {
return;
}
bool used_one_contract_fallback = false;
if (commissioned_all_in_market_short_lifecycle_
const bool one_contract_short_lifecycle =
commissioned_all_in_market_short_lifecycle_
|| default_market_direct_short_reversal_lifecycle_;
if (one_contract_short_lifecycle
&& !margin_zero_cover_full_liquidation_
&& position_side_ == PositionSide::SHORT
&& qty_step_ <= 1.0
Expand Down Expand Up @@ -3344,6 +3348,43 @@ void BacktestEngine::apply_filled_order_to_state(
default_market_short_shape_after_fill
&& order.created_position_side == PositionSide::FLAT
&& !order.created_after_position_close_in_bar;
// A direct, default-sized strategy.entry auto-reversal has the same
// broker opening checkpoints as the already-pinned close-then-short
// shape. Re-prove the generic order/runtime topology at the fill.
const bool default_market_direct_short_reversal_after_fill =
successful_fresh_open
&& position_side_before_fill == PositionSide::LONG
&& position_side_ == PositionSide::SHORT
&& order.type == OrderType::MARKET
&& !order.is_long
&& std::isnan(order.qty)
&& order.created_position_side == PositionSide::LONG
&& !order.created_after_position_close_in_bar
&& order.tv_carry_qty > kQtyEpsilon
&& default_qty_type_ == QtyType::PERCENT_OF_EQUITY
&& std::abs(default_qty_value_ - 100.0) < 1e-12
&& std::isfinite(margin_short_)
&& std::abs(margin_short_ / 100.0 - 1.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_mark)
&& order.sizing_mark > 0.0
&& std::isfinite(order.sizing_fx)
&& order.sizing_fx > 0.0
&& slippage_ == 0
&& !process_orders_on_close_
&& !calc_on_order_fills_
&& !bar_magnifier_enabled_
&& !stream_warmup_mode_
&& stream_phase_ == StreamPhase::IDLE
&& !order.created_during_coof_recalc
&& order.created_bar < bar_index_
&& order.oca_name.empty()
&& order.oca_type == 0;
const bool default_market_long_close_then_open_after_fill =
successful_fresh_open
&& position_side_before_fill == PositionSide::FLAT
Expand Down Expand Up @@ -3388,11 +3429,14 @@ void BacktestEngine::apply_filled_order_to_state(
commissioned_all_in_market_short_lifecycle_ =
default_market_short_close_then_open_after_fill
|| default_market_flat_short_after_fill;
default_market_direct_short_reversal_lifecycle_ =
default_market_direct_short_reversal_after_fill;
} else if (accepted_additional_entry) {
// A later add changes the exact position lifecycle whose TV
// zero-cover behavior is pinned by the source tape. Fail closed
// rather than lending the original provenance to the new shape.
commissioned_all_in_market_short_lifecycle_ = false;
default_market_direct_short_reversal_lifecycle_ = false;
}
const bool positive_raw_base =
std::isfinite(fill_price) && fill_price > 0.0;
Expand All @@ -3401,7 +3445,8 @@ void BacktestEngine::apply_filled_order_to_state(
&& (successful_fresh_open || accepted_additional_entry);
const bool scoped_short_opening_fill =
(explicit_market_short_full_margin_after_fill
|| default_market_short_close_then_open_after_fill)
|| default_market_short_close_then_open_after_fill
|| default_market_direct_short_reversal_after_fill)
&& positive_raw_base;
if (successful_short_open_or_add && !scoped_short_opening_fill) {
opening_affordability_pending_ = false;
Expand All @@ -3414,7 +3459,8 @@ void BacktestEngine::apply_filled_order_to_state(
}
if ((long_full_margin_after_fill
|| explicit_market_short_full_margin_after_fill
|| default_market_short_close_then_open_after_fill)
|| default_market_short_close_then_open_after_fill
|| default_market_direct_short_reversal_after_fill)
&& positive_raw_base
&& (successful_fresh_open || accepted_additional_entry)) {
// The only exemption requires every item of provenance to agree:
Expand Down Expand Up @@ -3482,7 +3528,8 @@ void BacktestEngine::apply_filled_order_to_state(
&& std::isfinite(new_opening_commission)
&& new_opening_commission == 0.0;
close_then_short_opening_requires_adverse_retry_ =
default_market_short_close_then_open_after_fill;
default_market_short_close_then_open_after_fill
|| default_market_direct_short_reversal_after_fill;
opening_affordability_raw_fill_base_ = fill_price;
}
}
Expand Down Expand Up @@ -4093,6 +4140,7 @@ void BacktestEngine::apply_raw_order_fill(PendingOrder& order, double fill_price
opening_affordability_default_long_reversal_ = false;
close_then_short_opening_requires_adverse_retry_ = false;
commissioned_all_in_market_short_lifecycle_ = false;
default_market_direct_short_reversal_lifecycle_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
position_entry_time_ = current_bar_.timestamp;
Expand Down
3 changes: 3 additions & 0 deletions src/engine_orders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ void BacktestEngine::reset_position_state_to_flat() {
opening_affordability_default_long_reversal_ = false;
close_then_short_opening_requires_adverse_retry_ = false;
commissioned_all_in_market_short_lifecycle_ = false;
default_market_direct_short_reversal_lifecycle_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
position_entry_time_ = 0;
Expand Down Expand Up @@ -636,6 +637,7 @@ void BacktestEngine::settle_position_after_partial_exit(
&& position_side_ == PositionSide::SHORT
&& position_qty_ + kQtyEpsilon < qty_before) {
commissioned_all_in_market_short_lifecycle_ = false;
default_market_direct_short_reversal_lifecycle_ = false;
}
}
}
Expand All @@ -660,6 +662,7 @@ void BacktestEngine::open_fresh_position(PositionSide requested, double fill_pri
opening_affordability_default_long_reversal_ = false;
close_then_short_opening_requires_adverse_retry_ = false;
commissioned_all_in_market_short_lifecycle_ = false;
default_market_direct_short_reversal_lifecycle_ = false;
opening_affordability_raw_fill_base_ =
std::numeric_limits<double>::quiet_NaN();
position_entry_time_ = current_bar_.timestamp;
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ set(TEST_SOURCES
test_fills_edge
test_default_qty_signal_freeze
test_margin_admission_gate
test_direct_short_reversal_affordability
test_short_reversal_emission
test_margin_stop_admission
test_stop_entry_placement_open_qty
Expand Down
Loading
Loading