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
28 changes: 28 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,14 @@ struct PendingOrder {
// flag is never set, so the fix is inert. See suppress_declined_reversal_
// close_legs (engine_fills.cpp).
bool suppress_as_declined_reversal_close = false;
// finding-311 (bracket lifecycle on declined reversal): a standing exit
// bracket of the live position goes DORMANT when an in-position opposite
// entry is declined at its fill re-check (the tradeless reversal). A
// dormant bracket never matches a fill. It revives with ORIGINAL prices
// when a margin-call partial re-registers the surviving position's exits,
// or is replaced wholesale by a fresh same-(id,from_entry) strategy.exit
// call (which arms the NEW call's prices, the ordinary re-issue path).
bool dormant_bracket = false;
// Qty this deferred close debited from id_unclosed_qty_[<bare id>] in
// compute_close_target_qty's default-FIFO branch at strategy.close CALL
// time. On the false->true suppression transition it is re-credited to that
Expand Down Expand Up @@ -1324,6 +1332,17 @@ class BacktestEngine {
// then fills the reduced remainder.
bool margin_call_slice_before_priced_exit(const Bar& bar,
double exit_fill_price);
// finding-325 (1x-long entry-fill affordability chronology): TV runs the
// 1x-long (margin_long=100) opening-affordability check AT THE ENTRY
// FILL, chronologically before the same bar's intrabar exits. When a
// priced exit of a just-opened 1x long is about to fill on the entry's
// own bar and the floor-sized opening cost exceeds post-close equity,
// the one-shot opening event books its trim FIRST — the ordinary
// floor-before-4x quantity (including the sub-lot one-contract
// fallback), filled at the RAW matched entry base, tagged "Margin call"
// — and the exit then closes the reduced remainder. Consumes the
// pending opening event; returns true when a slice was booked.
bool margin_call_1x_long_opening_slice_before_priced_exit(const Bar& bar);
// A timestamped FX rollover is a broker-open event, not an end-of-bar
// adverse-price check. Cell A1 supports carried 1x full-margin long and
// short in ordinary historical dispatch; leveraged shapes stay fail-closed.
Expand Down Expand Up @@ -2437,6 +2456,15 @@ class BacktestEngine {
// side (see PendingOrder::suppress_as_declined_reversal_close), re-crediting
// each flagged close's consumed id-ledger exactly once.
void suppress_declined_reversal_close_legs(const PendingOrder& declined_entry);
// finding-311: mark the live position's standing strategy.exit brackets
// dormant when an in-position reversal entry is declined at fill.
void mark_position_brackets_dormant_on_declined_reversal();
// finding-311: a margin-call partial re-registers the surviving
// position's exit brackets (revive with original prices). When the
// margin-call event price makes a revived bracket marketable, the whole
// remaining position closes at that price through the bracket's id.
void revive_position_brackets_after_margin_call_partial(
double margin_call_event_price);
// Per-OrderType fill kernels. Called only after risk + intraday
// gates pass; each updates the engine's position/trade state and
// any per-type out-parameters the post-fill bookkeeping needs.
Expand Down
277 changes: 272 additions & 5 deletions src/engine_fills.cpp

Large diffs are not rendered by default.

43 changes: 21 additions & 22 deletions src/ta_extremes_volume.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ Highest::Highest(int length)
: length(length) {}

double Highest::compute(double src) {
if (is_na(src)) {
return na<double>();
}

// TV positional-window semantics (findings 331/332 oracle, KI-55 family):
// the lookback window covers the last `length` BARS — an na input advances
// the window as a gap instead of being dropped, the extremum is taken over
// the non-na members, and the result is na only while the bar window has
// not fully formed or contains no non-na member. On gap-free series this
// is byte-identical to the previous last-N-non-na buffer.
buffer.push_back(src);
while ((int)buffer.size() > length) {
buffer.pop_front();
Expand All @@ -41,9 +43,9 @@ double Highest::compute(double src) {
return na<double>();
}

double hi = buffer[0];
for (int i = 1; i < (int)buffer.size(); i++) {
if (buffer[i] > hi) hi = buffer[i];
double hi = na<double>();
for (int i = 0; i < (int)buffer.size(); i++) {
if (!is_na(buffer[i]) && (is_na(hi) || buffer[i] > hi)) hi = buffer[i];
}
return hi;
}
Expand All @@ -54,10 +56,7 @@ Lowest::Lowest(int length)
: length(length) {}

double Lowest::compute(double src) {
if (is_na(src)) {
return na<double>();
}

// TV positional-window semantics — see Highest::compute above.
buffer.push_back(src);
while ((int)buffer.size() > length) {
buffer.pop_front();
Expand All @@ -67,9 +66,9 @@ double Lowest::compute(double src) {
return na<double>();
}

double lo = buffer[0];
for (int i = 1; i < (int)buffer.size(); i++) {
if (buffer[i] < lo) lo = buffer[i];
double lo = na<double>();
for (int i = 0; i < (int)buffer.size(); i++) {
if (!is_na(buffer[i]) && (is_na(lo) || buffer[i] < lo)) lo = buffer[i];
}
return lo;
}
Expand Down Expand Up @@ -533,12 +532,12 @@ double Highest::recompute(double src) {
if (buffer.empty()) return compute(src);
buffer.back() = src;

if (is_na(src)) return na<double>();
if ((int)buffer.size() < length) return na<double>();

double hi = buffer[0];
for (int i = 1; i < (int)buffer.size(); i++) {
if (buffer[i] > hi) hi = buffer[i];
// Mirrors Highest::compute — positional window, na members skipped.
double hi = na<double>();
for (int i = 0; i < (int)buffer.size(); i++) {
if (!is_na(buffer[i]) && (is_na(hi) || buffer[i] > hi)) hi = buffer[i];
}
return hi;
}
Expand All @@ -548,12 +547,12 @@ double Lowest::recompute(double src) {
if (buffer.empty()) return compute(src);
buffer.back() = src;

if (is_na(src)) return na<double>();
if ((int)buffer.size() < length) return na<double>();

double lo = buffer[0];
for (int i = 1; i < (int)buffer.size(); i++) {
if (buffer[i] < lo) lo = buffer[i];
// Mirrors Lowest::compute — positional window, na members skipped.
double lo = na<double>();
for (int i = 0; i < (int)buffer.size(); i++) {
if (!is_na(buffer[i]) && (is_na(lo) || buffer[i] < lo)) lo = buffer[i];
}
return lo;
}
Expand Down
10 changes: 8 additions & 2 deletions src/ta_oscillators.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ double Stoch::compute(double src, double high, double low) {

double range = hi - lo;
if (range == 0.0) {
return 50.0; // Avoid division by zero; midpoint when flat
// Pine: 100 * (src - lowest) / (highest - lowest) divides by zero on a
// flat window; division by zero is na in Pine, and the finding-331 TV
// oracle pins exactly this at the first live stochRSI bar of a
// range-local security context (single-value window -> hi == lo -> na,
// NOT a 50 midpoint: a midpoint would wake %D one bar early and flip
// the 2025-04-19 readout).
return na<double>();
}

return (src - lo) / range * 100.0;
Expand Down Expand Up @@ -618,7 +624,7 @@ double Stoch::recompute(double src, double high, double low) {
}

double range = hi - lo;
if (range == 0.0) return 50.0;
if (range == 0.0) return na<double>(); // Pine division by zero (see compute)
return (src - lo) / range * 100.0;
}

Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ set(TEST_SOURCES
test_close_all_coqueued_entry
test_same_bar_add_exit_coverage
test_declined_reversal_close_leg
test_bracket_lifecycle_declined_reversal
test_dual_entry_placement_sizing
test_default_flat_market_gross_admission
test_live_position_market_gross_admission
Expand All @@ -107,6 +108,7 @@ set(TEST_SOURCES
test_drawing
test_margin_call
test_margin_call_intrabar_chronology
test_margin_call_1x_long_entry_fill
test_percent_equity_open_entry_fee
test_streaming
test_calc_on_order_fills
Expand Down
Loading
Loading