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
11 changes: 7 additions & 4 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1520,8 +1520,10 @@ class BacktestEngine {
// their first bucket). Whether the bucket was in progress is read from
// the auxiliary 1m feed (did it trade between the bucket's nominal open
// and the first chart bar? the NSE week whose Monday was a holiday opens
// on Tuesday and is kept), so only split-feed runs apply the cut; a
// single-feed run keeps its feed-start series. Lower-TF evaluators are
// on Tuesday and is kept). Historical intraday single-feed forex/cfd D
// requests also omit their partial first session, using its actual trading
// open rather than its label. Other single-feed series keep their feed-start
// behavior; native feeds retain their own rules. Lower-TF evaluators are
// untouched (their slices begin at the first chart bar anyway), and the
// flag above keeps its explicit epoch plus the EMA na-warmup semantics.
int64_t security_first_chart_bar_ms_ = 0;
Expand Down Expand Up @@ -3380,8 +3382,9 @@ class BacktestEngine {
// `input_ts` belongs to an HTF bucket that opened before the cut --
// security_range_start_ms_ under the flag, else the run's first chart
// bar for a coarser-than-chart / chart-timeframe evaluator
// (security_first_chart_bar_ms_, split-feed runs, the auxiliary feed
// proving the bucket traded before it; false for lower-TF evaluators). The
// (security_first_chart_bar_ms_, split-feed runs with the auxiliary feed
// proving prior trading, or single-feed historical intraday forex/cfd D
// requests keyed to their session's actual open; false for lower TFs). The
// progressive feed and the historical lookahead projection builder must
// agree on this predicate so projected child indexes line up with the
// per-state feed cursor.
Expand Down
20 changes: 10 additions & 10 deletions src/engine_run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1604,16 +1604,16 @@ void BacktestEngine::prepare_historical_security_lookahead_projections(
return input_bars[child].timestamp;
};
int projection_begin = 0;
if (security_range_start_na_warmup_
|| (security_first_chart_bar_ms_ > 0 && aux_security_feed_enabled())) {
while (projection_begin < n_input
&& security_input_precedes_range_start(
state, child_instant_ms(projection_begin))) {
++projection_begin;
}
if (projection_begin >= n_input) {
continue;
}
// The shared predicate also covers the single-feed OTC daily cut.
// It is false for excluded evaluators, so no separate mode guard may
// let the producer retain children the consumer will discard.
while (projection_begin < n_input
&& security_input_precedes_range_start(
state, child_instant_ms(projection_begin))) {
++projection_begin;
}
if (projection_begin >= n_input) {
continue;
}
const int projection_count = n_input - projection_begin;

Expand Down
22 changes: 22 additions & 0 deletions src/engine_security.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,28 @@ bool BacktestEngine::security_input_precedes_range_start(
return state.aggregator.bucket_open_ms(input_ts) < security_range_start_ms_;
}
#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
// R19 OTC daily pins: a historical intraday chart beginning mid-session
// has no partial first D bar in request.security, even when this run has
// only the chart feed. XAUUSD from Apr1 00:00Z reads D time/close as na
// that day and ATR14[1] first becomes numeric Apr23; counting the partial
// Mar31 session seeds ATR a day early. Restrict the no-aux inference to
// the pinned daily OTC clock. W/M need trading-history evidence to retain
// a holiday-open period, and native/aux feeds keep their existing rules.
if (!aux_security_feed_enabled() && native_security_feeds_.empty()
&& security_first_chart_bar_ms_ > 0
&& script_tf_seconds_ > 0 && script_tf_seconds_ < 86400
&& (state.tf == "D" || state.tf == "1D")
&& !state.lower_tf_requested && !state.lower_tf_array_requested
&& (syminfo_.type == "forex" || syminfo_.type == "cfd")
&& !stream_warmup_mode_ && stream_phase_ == StreamPhase::IDLE) {
const int64_t stamp = session_period_open_ms(
input_ts, syminfo_.timezone, syminfo_.session, CalendarPeriod::DAY);
// Metals stamp the D bar at 17:00 ET but first trade at 18:00.
// Starting at that actual open keeps the complete first session.
const int64_t trading_open = session_covered_instant_ms(
stamp, syminfo_.timezone, syminfo_.session);
return trading_open < security_first_chart_bar_ms_;
}
// Default cut (round 8, family P), split-feed runs only: a coarser-than-
// chart or chart-timeframe series starts at the first bucket that OPENS at
// or after the run's first chart bar -- the deep-backtest range start --
Expand Down
155 changes: 151 additions & 4 deletions tests/test_security_range_start_bucket_gating.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

#include <pineforge/engine.hpp>
#include <pineforge/na.hpp>
#include <pineforge/ta.hpp>
#include <pineforge/timeframe.hpp>

#include <cstdint>
Expand Down Expand Up @@ -170,10 +171,11 @@ class BucketGateHarness : public BacktestEngine {
std::vector<double> completed_close[3];

explicit BucketGateHarness(const char* input_tf,
const char* tf0, const char* tf1, const char* tf2) {
register_security_eval(0, tf0, input_tf, false, false);
register_security_eval(1, tf1, input_tf, false, false);
register_security_eval(2, tf2, input_tf, false, false);
const char* tf0, const char* tf1, const char* tf2,
bool lookahead = false) {
register_security_eval(0, tf0, input_tf, lookahead, false);
register_security_eval(1, tf1, input_tf, lookahead, false);
register_security_eval(2, tf2, input_tf, lookahead, false);
}
void evaluate_security(int sec_id, const Bar& bar, bool is_complete) override {
if (!is_complete || sec_id < 0 || sec_id > 2) return;
Expand Down Expand Up @@ -613,6 +615,148 @@ static void test_flag_epoch_1d_chart_weekly_from_chart_dailies() {
}
#endif

#ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1
// R19 covered TV pins on XAUUSD: starting at Apr1 00:00 UTC omits the
// in-progress daily bar. time/close are na on Apr1 and ATR14[1] first reads
// Apr23. This holds without an auxiliary feed and for both lookahead modes.
// Keep W/M's existing no-history-evidence behavior outside the daily repair.
static void test_single_feed_otc_daily_partial_bucket() {
int before = failures;
const auto full = make_forex_15m_feed();
const int64_t start = utc_ms(2025, 4, 1);
std::vector<Bar> bars;
for (const Bar& bar : full) if (bar.timestamp >= start) bars.push_back(bar);
for (const std::string& kind : {std::string("forex"), std::string("cfd")}) {
for (bool lookahead : {false, true}) {
BucketGateHarness h("15", "D", "W", "M", lookahead);
h.set_syminfo_timezone(NY);
h.set_syminfo_session(FX);
h.set_syminfo_type(kind);
h.run(bars.data(), static_cast<int>(bars.size()), "15", "15");
CHECK(h.last_error().empty(), "single-feed OTC partial-day run succeeds");
CHECK(!h.completed[0].empty(), "single-feed daily series completes");
if (!h.completed[0].empty()) {
CHECK_EQ_MS(h.completed[0].front(), utc_ms(2025, 4, 1, 21),
"single-feed D starts at the first whole session");
}
for (int i=1; i<3; ++i) {
CHECK(!h.completed[i].empty(), "single-feed W/M completes");
if (!h.completed[i].empty()) CHECK_EQ_MS(h.completed[i].front(),
utc_ms(2025,3,31,21),
"single-feed W/M retains its existing feed-start series");
}
}
}
std::printf("test_single_feed_otc_daily_partial_bucket: %s\n",
failures > before ? "FAIL" : "ok");
}

// OANDA's daily label is 17:00 ET but trading starts at 18:00. A feed
// beginning at that actual open contains the whole bar and must keep it.
static void test_single_feed_cfd_actual_open_is_not_partial() {
int before = failures;
const int64_t begin=utc_ms(2025,4,1,22), end=utc_ms(2025,4,4,21);
std::vector<Bar> bars;
for (int64_t t=begin;t<end;t+=900000) {
const int64_t hour=(t/3600000)%24;
if (hour==21) continue; // the 17:00..18:00 EDT daily break
bars.push_back({100,101,99,100,1,t});
}
BucketGateHarness h("15", "D", "W", "M");
h.set_syminfo_timezone(NY);
h.set_syminfo_session("1800-1700");
h.set_syminfo_type("cfd");
h.run(bars.data(),static_cast<int>(bars.size()),"15","15");
CHECK(h.last_error().empty(), "single-feed cfd aligned run succeeds");
CHECK(!h.completed[0].empty(), "single-feed cfd aligned D completes");
if (!h.completed[0].empty()) CHECK_EQ_MS(h.completed[0].front(),
utc_ms(2025,4,1,21), "keep whole cfd day whose stamp precedes trading");
std::printf("test_single_feed_cfd_actual_open_is_not_partial: %s\n",
failures > before ? "FAIL" : "ok");
}

// Equivalent to request.security("D", ta.atr(14)[1]) plus D close. The
// synthetic bars have true range 8; a deliberately extreme initial partial
// day must never enter either the ATR seed or the projected daily roster.
class OtcDailyAtrHarness : public BacktestEngine {
public:
ta::ATR atr{14};
std::vector<double> atr_history;
struct Read { int64_t time; double close; double previous_atr; };
std::vector<Read> reads;
double visible_close=na<double>(), previous_atr=na<double>();
int64_t first_projected_child=0;
explicit OtcDailyAtrHarness(bool lookahead) {
register_security_eval(0,"D","15",lookahead,false);
set_syminfo_metadata("historical_security_lookahead_projection",1);
}
void evaluate_security(int,const Bar& bar,bool) override {
if (security_series_slot_is_new(0)) {
atr_history.push_back(atr.compute(bar.high,bar.low,bar.close));
} else if (!atr_history.empty()) {
atr_history.back()=atr.recompute(bar.high,bar.low,bar.close);
}
previous_atr=atr_history.size()>1
? atr_history[atr_history.size()-2] : na<double>();
visible_close=bar.close;
}
void on_bar(const Bar& bar) override {
if (first_projected_child==0
&& !security_eval_states_[0].historical_projections.empty()) {
first_projected_child=
security_eval_states_[0].historical_projections.front().first_child_ms;
}
reads.push_back({bar.timestamp,visible_close,previous_atr});
}
};

static void test_single_feed_projection_and_previous_atr() {
int before=failures;
for (bool cfd : {false,true}) {
const char* session=cfd ? "1800-1700" : "1700-1700";
std::vector<Bar> bars={
{999,1999,0,999,1,utc_ms(2025,4,1)},
{999,1999,0,999,1,utc_ms(2025,4,1,6)},
{999,1999,0,999,1,utc_ms(2025,4,1,20,45)}};
const int trading_days[]={2,3,4,7,8,9,10,11,14,15,16,17,18,21,22,23,24,25,28,29,30};
int index=0;
for (int day : trading_days) {
if (cfd && day==18) continue; // XAU Good Friday; EUR trades
double base=100+index++;
bars.push_back({base,base+4,base-4,base,1,
utc_ms(2025,4,day-1,cfd?22:21)});
bars.push_back({base,base+4,base-4,base+1,1,utc_ms(2025,4,day,6)});
bars.push_back({base,base+4,base-4,base+2,1,utc_ms(2025,4,day,20,45)});
}
for (bool lookahead : {false,true}) {
OtcDailyAtrHarness h(lookahead);
h.set_syminfo_timezone(NY);h.set_syminfo_session(session);
h.set_syminfo_type(cfd?"cfd":"forex");
h.run(bars.data(),static_cast<int>(bars.size()),"15","15");
CHECK(h.last_error().empty(),"OTC previous ATR run succeeds");
if (lookahead) CHECK_EQ_MS(h.first_projected_child,
utc_ms(2025,4,1,cfd?22:21),"projection roster starts at first whole day");
const int numeric_day=cfd ? (lookahead?23:24) : (lookahead?22:23);
for (const auto& read : h.reads) {
if (read.time==utc_ms(2025,4,1,6))
CHECK(is_na(read.close),"partial first daily close is absent");
if (read.time==utc_ms(2025,4,2,6))
CHECK(lookahead ? read.close==102 : is_na(read.close),
"first daily close leaks on first child only with lookahead");
if (read.time==utc_ms(2025,4,3,6) && !lookahead)
CHECK(read.close==102,"lookahead-off exposes first completed daily close");
if (read.time==utc_ms(2025,4,numeric_day-1,6))
CHECK(is_na(read.previous_atr),"ATR14[1] remains absent one day before seed");
if (read.time==utc_ms(2025,4,numeric_day,6))
CHECK(read.previous_atr==8,"ATR14[1] reads 8 after fourteen whole daily bars");
}
}
}
std::printf("test_single_feed_projection_and_previous_atr: %s\n",
failures>before?"FAIL":"ok");
}
#endif

int main() {
test_bucket_open_utc_grid();
test_bucket_open_forex_session();
Expand All @@ -626,6 +770,9 @@ int main() {
test_default_cut_nse_holiday_monday_keeps_the_week();
test_default_cut_oanda_1d_weekly();
test_flag_epoch_1d_chart_weekly_from_chart_dailies();
test_single_feed_otc_daily_partial_bucket();
test_single_feed_cfd_actual_open_is_not_partial();
test_single_feed_projection_and_previous_atr();
#endif
if (failures) {
std::printf("%d check(s) FAILED\n", failures);
Expand Down
Loading