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
29 changes: 27 additions & 2 deletions include/pineforge/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ struct PendingOrder {
// trail predicates.
double trail_price = std::numeric_limits<double>::quiet_NaN();
double trail_offset; // NaN = not set
double profit_ticks = std::numeric_limits<double>::quiet_NaN(); // strategy.exit profit offset
double loss_ticks = std::numeric_limits<double>::quiet_NaN(); // strategy.exit loss offset
double qty; // NaN = use default sizing, else explicit qty
int qty_type; // -1 = qty is fixed contracts, else QtyType override
double qty_percent; // 100 = full position
Expand Down Expand Up @@ -488,6 +490,7 @@ class BacktestEngine {
// --- Runtime state ---
Bar current_bar_;
int bar_index_ = 0;
int bar_index_offset_ = 0;
int64_t next_order_seq_ = 1;
// TV: at most one priced ENTRY "open" event per bar; persists across
// multiple process_pending_orders calls (bar magnifier) and dual-pass
Expand Down Expand Up @@ -664,7 +667,9 @@ class BacktestEngine {
double qty_percent = 100.0,
const std::string& comment = "",
double qty = std::numeric_limits<double>::quiet_NaN(),
const std::string& oca_name = "");
const std::string& oca_name = "",
double profit_ticks = std::numeric_limits<double>::quiet_NaN(),
double loss_ticks = std::numeric_limits<double>::quiet_NaN());
void strategy_cancel(const std::string& id);
void strategy_cancel_all();
void strategy_order(const std::string& id, bool is_long, double qty,
Expand Down Expand Up @@ -837,7 +842,13 @@ class BacktestEngine {
case QtyType::FIXED:
return apply_qty_step(default_qty_value_);
case QtyType::PERCENT_OF_EQUITY: {
double equity = current_equity();
// TradingView's percent-of-equity default sizing uses
// strategy.equity, i.e. initial capital + closed PnL +
// mark-to-market open PnL. Keep current_equity() as the
// closed-equity accessor used elsewhere, but size new default
// percent orders from the live equity snapshot so pyramid adds
// and same-bar/re-entry sizing see unrealized PnL.
double equity = current_equity() + open_profit(current_bar_.close);
double cash = equity * (default_qty_value_ / 100.0) / account_currency_fx_;
// Reject (qty 0) on a non-finite / non-positive fill price — a
// degenerate $0/NaN print must NOT size as the raw % number.
Expand Down Expand Up @@ -1470,6 +1481,7 @@ class BacktestEngine {
double& trail_best_path_state,
int& exit_closed_from_bar,
bool& exit_closed_was_long);
void materialize_relative_exit_prices_for_live_position();

// Inner-loop phase split for process_pending_orders.
// The inner loop iterates `pending_orders_` and processes each via
Expand Down Expand Up @@ -1506,6 +1518,7 @@ class BacktestEngine {
double& qty_to_close_out,
bool& all_entries_match_out);
void cancel_orders_for_full_close(const std::string& id, bool closing_long);
void cancel_same_bar_market_reentries_after_full_close(bool closed_long);
// Same-bar close batching (TV one-fill-per-bar; see the field-block
// comment at close_reserved_qty_). enqueue replaces the pending
// same-bar close; flush executes the surviving one at bar close.
Expand Down Expand Up @@ -1729,6 +1742,16 @@ class BacktestEngine {
bool margin_call_enabled() const { return margin_call_enabled_; }
void set_syminfo_metadata(const std::string& key, double value) {
syminfo_metadata_[key] = value;
// Pine's public bar_index is chart-history relative. Validation feeds
// can start after TradingView's hidden first chart bar, while engine
// internals still need zero-based array indices for TA precalc and
// broker bookkeeping. This metadata key shifts only codegen-emitted
// Pine bar_index reads via pine_bar_index()/pine_last_bar_index().
if (key == "bar_index_offset") {
bar_index_offset_ = std::isfinite(value)
? static_cast<int>(std::llround(value))
: 0;
}
// "qty_step" is the per-instrument lot increment used by the forced-
// liquidation quantizer. Route it onto the dedicated member so the
// codegen run(const Bar*, int) path (which never overwrites it) keeps
Expand Down Expand Up @@ -1776,6 +1799,8 @@ class BacktestEngine {
// Returns the script's active timeframe string (e.g. "15" for 15-minute,
// "D" for daily). Backs timeframe.main_period in generated Pine v6 code.
const std::string& main_period() const { return script_tf_; }
int pine_bar_index() const { return bar_index_ + bar_index_offset_; }
int pine_last_bar_index() const { return last_bar_index_ + bar_index_offset_; }

// Toggle volume-weighted per-sub-bar sampling inside run_magnified_bar.
// Has no effect unless bar magnifier is enabled.
Expand Down
5 changes: 5 additions & 0 deletions include/pineforge/session_time.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

namespace pineforge {

// TradingView accepts fixed-offset names like "GMT+1" / "UTC-5" with the
// human-readable sign convention. POSIX TZ strings use the opposite sign, so
// engine code that calls setenv("TZ", ...) must normalize through this helper.
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
Expand Down
6 changes: 4 additions & 2 deletions include/pineforge/ta.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,11 @@ class HMA {

class StdDev {
int length_;
bool biased_;
std::deque<double> buffer_;

public:
explicit StdDev(int length);
explicit StdDev(int length, bool biased = true);
double compute(double src);
double recompute(double src);
};
Expand Down Expand Up @@ -542,10 +543,11 @@ class RCI {

class Variance {
int length_;
bool biased_;
std::deque<double> buffer_;

public:
explicit Variance(int length);
explicit Variance(int length, bool biased = true);
double compute(double src);
double recompute(double src);
};
Expand Down
6 changes: 5 additions & 1 deletion scripts/regen_validation_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ def _verify_probe(strategy_dir: Path) -> dict:
"tier": tier,
"tv": len(tv_cmp), "eng": len(eng_cmp), "matched": 0,
"count_delta": vc.relative_max(len(tv_gate), len(eng_gate)),
"count_abs_delta": abs(len(tv_gate) - len(eng_gate)),
"entry_p90": 0.0, "exit_p90": 0.0, "pnl_p90": 0.0,
"profile": profile,
"notes": meta.get("notes", "no aligned trades"),
}

count_abs_delta = abs(len(tv_gate) - len(eng_gate))
count_delta = vc.relative_max(len(tv_gate), len(eng_gate))
entry_deltas = [vc.relative_max(t.entry_price, e.entry_price) for t, e in gating_matched]
exit_deltas = [vc.relative_max(t.exit_price, e.exit_price) for t, e in gating_matched]
Expand All @@ -140,7 +142,7 @@ def _verify_probe(strategy_dir: Path) -> dict:
exit_p90 = vc.percentile(exit_deltas, 0.90)
pnl_p90 = vc.percentile(pnl_deltas, 0.90) if pnl_deltas else 0.0

count_ok = count_delta < thresh["count"]
count_ok = count_abs_delta == 0
entry_ok = entry_p90 < thresh["entry"]
exit_ok = exit_p90 < thresh["exit"]
pnl_ok = pnl_p90 < thresh["pnl"]
Expand All @@ -149,6 +151,7 @@ def _verify_probe(strategy_dir: Path) -> dict:
tier = "excellent"
elif (
len(gating_matched) / max(len(tv_gate), 1) >= 0.99
and count_abs_delta <= 1
and count_delta < vc.STRONG_COUNT_DELTA
and entry_p90 < vc.STRONG_ENTRY_DELTA
and exit_p90 < vc.STRONG_EXIT_DELTA
Expand Down Expand Up @@ -190,6 +193,7 @@ def _verify_probe(strategy_dir: Path) -> dict:
"eng": len(eng_cmp),
"matched": len(matched),
"count_delta": count_delta,
"count_abs_delta": count_abs_delta,
"entry_p90": entry_p90,
"exit_p90": exit_p90,
"pnl_p90": pnl_p90,
Expand Down
8 changes: 7 additions & 1 deletion scripts/run_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,9 @@ def main() -> int:
help="Emit all trades from the full OHLCV input, including warmup trades.")
ap.add_argument("--disable-trading-before-window", action="store_true",
help="Warm indicators on pre-window bars but ignore strategy order commands until the emit window starts.")
ap.add_argument("--allow-trading-before-window", action="store_true",
help="When tv_trades_csv defines an emit window, keep broker order execution active before that window. "
"This matches TV exports that carry positions opened before the displayed date range.")
ap.add_argument("--inputs-json", type=Path, default=None,
help="Use this inputs.json instead of strategy_dir/inputs.json. "
"Lets ad-hoc validation runs override strategy properties "
Expand Down Expand Up @@ -1208,7 +1211,10 @@ def main() -> int:
tv_window_used = emit_window is not None
if emit_window is None:
emit_window = _load_window_ms(REFERENCE_OHLCV)
trade_start_ms = emit_window[0] if (emit_window is not None and (tv_window_used or args.disable_trading_before_window)) else None
trade_start_ms = None
if emit_window is not None and not args.allow_trading_before_window:
if tv_window_used or args.disable_trading_before_window:
trade_start_ms = emit_window[0]

if args.runner == "docker":
if args.trace_json is not None:
Expand Down
Loading
Loading