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
8 changes: 8 additions & 0 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,14 @@ class BacktestEngine {
// short in ordinary historical dispatch; leveraged shapes stay fail-closed.
// Returns true when it emits a broker liquidation row.
bool process_carried_position_fx_rollover(const Bar& bar);
// finding-430: forced liquidation at the bar OPEN. A carried position
// with a finite liquidation price that already breaches the margin
// requirement at the open is sliced AT THE OPEN (quantity computed at
// the open price), before any resting order is evaluated there; the
// survivor keeps its ordinary adverse-extreme check, so one bar can book
// an open slice AND an extreme slice. Bars whose open does not breach
// are untouched. Returns true when a "Margin call" row was booked.
bool margin_call_slice_at_bar_open(const Bar& bar);

// --- Slippage helper ---
double round_to_mintick(double price) const {
Expand Down
57 changes: 55 additions & 2 deletions include/pineforge/session_time.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,72 @@ std::string normalize_timezone_for_posix(const std::string& tz);
// Pine time(timeframe, session?, timezone?) and time_close(...).
// Returns Unix milliseconds, or na<int64_t>() when the bar is outside the
// requested session (TradingView semantics for filtered sessions).
//
// `syminfo_tz` is the symbol's exchange timezone (Pine `syminfo.timezone`;
// codegen passes `syminfo_.timezone`). It is the DEFAULT zone the `session`
// window is interpreted in when `tz` is empty — Pine reads a tz-less session
// in the exchange timezone, not UTC. An explicit `tz` always wins. It does
// NOT influence the timeframe open/close computation (explicit `tz`, else
// UTC, exactly as before); an empty `syminfo_tz` reproduces the historical
// UTC session default byte-for-byte.
//
// Feature macro: generated code tests this to decide whether the trailing
// `syminfo_tz` argument exists, so one transpiler output compiles against
// both this header and older ones (`PF_PINE_TIME_SYMINFO_TZ_ARG(x)` in the
// emitted prelude expands to `, (x)` only when it is defined).
// ---------------------------------------------------------------------------

#define PF_PINE_TIME_HAS_SYMINFO_TZ 1

int64_t pine_time(int64_t bar_ms,
const std::string& tf,
const std::string& session,
const std::string& tz,
const std::string& chart_tf,
const std::string& syminfo_tz = std::string());

int64_t pine_time_close(int64_t bar_ms,
const std::string& tf,
const std::string& session,
const std::string& tz,
const std::string& chart_tf,
const std::string& syminfo_tz = std::string());

// Symbol-clock forms. `sym_tz` / `sym_session` are Pine's syminfo.timezone
// and syminfo.session (the engine's runtime overrides). For a D/W/M `tf`
// called WITHOUT a valid session argument the returned open / close is the
// SYMBOL's daily/weekly/monthly bar (17:00 ET on OANDA forex, 09:30 ET RTH
// on NASDAQ equities, 00:00 UTC on a 24x7 UTC symbol — see
// session_period_open_ms in timeframe.hpp). A valid `session` argument
// defines the day in its own `tz` exactly as the forms above do
// (TradingView rolls `time("D", "0000-2359", "America/New_York")` at New
// York midnight on a UTC symbol — measured), and its window is read in the
// explicit `tz`, else `sym_tz` (the syminfo default above), else UTC.
// Intraday tfs are unchanged.
// With sym_tz="UTC" and an empty / "24x7" sym_session these are
// bit-identical to the five-argument forms above.
//
// Feature macro: generated code tests this to decide whether the trailing
// (sym_tz, sym_session) pair exists (`PF_PINE_TIME_SESSION_DAY_ARGS(tz, s)`
// in the emitted prelude expands to `, tz, s` only when it is defined, so
// the same generated.cpp collapses to the 5-arg call on older engines).
#define PF_PINE_TIME_HAS_SESSION_DAY 1

int64_t pine_time(int64_t bar_ms,
const std::string& tf,
const std::string& session,
const std::string& tz,
const std::string& chart_tf);
const std::string& chart_tf,
const std::string& sym_tz,
const std::string& sym_session);

int64_t pine_time_close(int64_t bar_ms,
const std::string& tf,
const std::string& session,
const std::string& tz,
const std::string& chart_tf);
const std::string& chart_tf,
const std::string& sym_tz,
const std::string& sym_session);

// ---------------------------------------------------------------------------
// Low-level session helpers (exposed for engine_run.cpp, unit tests,
Expand Down
40 changes: 35 additions & 5 deletions include/pineforge/ta.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -947,26 +947,48 @@ class VWAP {
// Sum of (price^2 * volume) for running variance computation used by
// compute_bands(). Variance = cum_pv_sq_ / cum_vol_ - mean^2.
double cum_pv_sq_ = 0.0;
// Anchor day index (Unix-day = timestamp_ms / 86_400_000). On the
// first compute() call we record this from the bar timestamp; on
// every subsequent compute() the cumulator is reset whenever the
// day index advances. Pine v6 `ta.vwap(source)` defaults to a Daily
// anchor (`anchor = timeframe.change("1D")`); engine matches that.
// Anchor day index: the SESSION day of the bar (session_day_index —
// timestamp_ms / 86_400_000 on a UTC/24x7 symbol, the 17:00-ET-keyed
// trading day on forex, the 09:30 RTH day on equities). On the first
// compute() call we record this from the bar timestamp; on every
// subsequent compute() the cumulator is reset whenever the day index
// advances. Pine v6 `ta.vwap(source)` defaults to a Daily anchor
// (`anchor = timeframe.change("1D")` on the SYMBOL's daily bar); the
// engine matches that when the codegen threads syminfo tz + session.
int64_t anchor_day_ = std::numeric_limits<int64_t>::min();

// Mirror the initial committed state (see RMA::RMA) so a recompute()
// before the first compute() restores a well-defined pristine state.
double saved_cum_pv_ = 0.0, saved_cum_vol_ = 0.0, saved_cum_pv_sq_ = 0.0;
int64_t saved_anchor_day_ = std::numeric_limits<int64_t>::min();

void roll_anchor(int64_t day);

public:
VWAP() = default;
// tz-less forms key the anchor on the UTC day (the 24x7 corpus regime);
// the tz/session forms key it on the symbol's session day and reduce to
// the same integer math for tz="UTC" + empty/"24x7" session.
// Feature macro PF_VWAP_HAS_SESSION_ANCHOR (below the class) lets
// generated code compile the (tz, session) tail out on older engines.
double compute(double src, double volume, int64_t timestamp_ms);
double recompute(double src, double volume, int64_t timestamp_ms);
double compute(double src, double volume, int64_t timestamp_ms,
const std::string& tz, const std::string& session);
double recompute(double src, double volume, int64_t timestamp_ms,
const std::string& tz, const std::string& session);
VWAPBandsResult compute_bands(double src, double volume, int64_t timestamp_ms, double stdev_mult);
VWAPBandsResult recompute_bands(double src, double volume, int64_t timestamp_ms, double stdev_mult);
VWAPBandsResult compute_bands(double src, double volume, int64_t timestamp_ms, double stdev_mult,
const std::string& tz, const std::string& session);
VWAPBandsResult recompute_bands(double src, double volume, int64_t timestamp_ms, double stdev_mult,
const std::string& tz, const std::string& session);
};

// Feature macro: the emitted prelude's PF_VWAP_SESSION_ANCHOR_ARGS(tz, s)
// expands to `, tz, s` only when this is defined (see VWAP overloads above).
#define PF_VWAP_HAS_SESSION_ANCHOR 1

// --- VWAP Bands wrapper class (3-tuple form: ta.vwap(src, anchor, stdev_mult)) ---
// Wraps VWAP and routes compute/recompute to compute_bands/recompute_bands with
// a fixed stdev_mult supplied at construction time. This lets the codegen use
Expand All @@ -982,6 +1004,14 @@ class VWAPBands {
VWAPBandsResult recompute(double src, double volume, int64_t timestamp_ms) {
return vwap_.recompute_bands(src, volume, timestamp_ms, stdev_mult_);
}
VWAPBandsResult compute(double src, double volume, int64_t timestamp_ms,
const std::string& tz, const std::string& session) {
return vwap_.compute_bands(src, volume, timestamp_ms, stdev_mult_, tz, session);
}
VWAPBandsResult recompute(double src, double volume, int64_t timestamp_ms,
const std::string& tz, const std::string& session) {
return vwap_.recompute_bands(src, volume, timestamp_ms, stdev_mult_, tz, session);
}
};

// --- Statistical ---
Expand Down
41 changes: 41 additions & 0 deletions include/pineforge/timeframe.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,47 @@ bool crosses_boundary(int64_t prev_ms, int64_t curr_ms, CalendarPeriod period,
bool tf_change(int64_t prev_ms, int64_t curr_ms, const std::string& tf,
const std::string& tz, const std::string& session);

// ─── Symbol-clock D/W/M bar anchors ────────────────────────────────────────────
//
// TradingView's daily bar is the SESSION day, not the UTC (nor the local
// calendar) day: OANDA:EURUSD (America/New_York, 1700-1700) opens its daily
// bar at 17:00 ET and its week at Sunday 17:00 ET; NASDAQ:AAPL (0930-1600)
// opens at 09:30 ET Monday..Friday; a 24x7 UTC symbol opens at 00:00 UTC.
// Every chart-level consumer of "the symbol's daily bar" — ta.vwap's default
// anchor, timeframe.change("1D"), time("D")/time_close("D") — has to key on
// this clock, the same one request.security aggregation already uses
// (crosses_boundary / session_period_key above). With tz="UTC" and an
// empty / "24x7" session all of these reduce to plain epoch integer math,
// bit-identical to the UTC forms the corpus pins.
//
// Period attribution follows TV's trading-date rule: a session that wraps
// midnight (1700-1700) is the trading day of the date it CLOSES on (the
// bar opening Sunday 17:00 ET is Monday's daily bar), so forex weeks open on
// the Sunday session and a month opens on the session whose close date is
// the 1st. Equity / 24x7 sessions close on their open date, so their weeks
// stay Monday-partitioned and months calendar-partitioned exactly as before.
// W/M opens are nominal (the Monday / 1st session-day); they do not consult
// a holiday calendar.

/// Ordinal of the session day containing `ms` (days since epoch on the
/// session clock). UTC + no session: ms / kMsPerDay.
int64_t session_day_index(int64_t ms, const std::string& tz,
const std::string& session);

/// Open (Unix ms) of the symbol's D/W/M bar that contains `ms`.
/// CalendarPeriod::NONE returns `ms` unchanged.
int64_t session_period_open_ms(int64_t ms, const std::string& tz,
const std::string& session,
CalendarPeriod period);

/// Exclusive close (Unix ms) of the symbol's D/W/M bar that contains `ms`:
/// DAY -> session-day open + session length (16:00 ET on equities, the next
/// 17:00 ET on forex, next midnight on 24x7); WEEK / MONTH -> the open of
/// the next period's first session-day. CalendarPeriod::NONE returns `ms`.
int64_t session_period_close_ms(int64_t ms, const std::string& tz,
const std::string& session,
CalendarPeriod period);

// ─── TimeframeAggregator ───────────────────────────────────────────────────────

class TimeframeAggregator {
Expand Down
Loading
Loading