From bc507c6522de276f7c946486b4e0ce98750c2b53 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 7 Sep 2026 12:23:09 +0800 Subject: [PATCH 1/4] fix: expose opening short liquidations before script evaluation --- include/pineforge/engine.hpp | 3 + src/engine_fills.cpp | 40 +++++ src/engine_run.cpp | 1 + tests/CMakeLists.txt | 1 + ...test_opening_short_margin_script_state.cpp | 154 ++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 tests/test_opening_short_margin_script_state.cpp diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index d827568..7a9b393 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -1891,6 +1891,9 @@ class BacktestEngine { // adverse-price liquidation; only an eligible one-shot post-fill // affordability event can trim it. void process_margin_call(const Bar& bar); + // Ordinary fresh sub-contract shorts with no resting orders expose their + // opening-bar liquidation to the close-time script (R23 TV controls). + void process_opening_short_margin_before_script(const Bar& bar); // finding-308: chronological pre-exit forced-liquidation slice. Called // from the process_pending_orders fill loop immediately BEFORE a priced // exit of the live position is applied. Fires only when (a) no margin diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index a04a896..5a558d8 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -997,6 +997,46 @@ bool BacktestEngine::entry_bar_post_fill_adverse(const Bar& bar, return true; } +// R23 BTC Rhyme17: the 13:45 opening short (0.08733 @ 115842.33) is fully +// liquidated at H=115852.95 before the script places a replacement. The +// script therefore reads position_size=0 and position_avg_price=na. Running +// this checkpoint after the script instead creates a bracket from the dead +// entry's average, which then closes the replacement one bar too early. +// +// Covered TV controls also expose a partial's reduced size (-0.08729) to a +// 50% close, keep a funded short, and preserve an explicit bracket issued for +// the pending replacement. Reuse the existing broker arithmetic and settle +// it before the script in this bounded, interaction-free opening topology. +void BacktestEngine::process_opening_short_margin_before_script(const Bar& bar) { + if (!margin_call_enabled_ || position_side_ != PositionSide::SHORT + || !entry_bar_margin_path_scope() + || bar.timestamp != current_bar_.timestamp + || !pending_orders_.empty() + || !(position_qty_ > 0.0 && position_qty_ <= 1.0) + || !(qty_step_ > 0.0 && qty_step_ < 1.0) + || pyramiding_ < 0 || pyramiding_ > 1 + || position_entry_count_ != 1 || pyramid_entries_.size() != 1 + || !pyramid_entries_.front().ordinary_market_open + || pyramid_entries_.front().entry_bar_index != bar_index_ + || commission_value_ != 0.0 || slippage_ != 0 + || margin_short_ != 100.0 || syminfo_.pointvalue != 1.0 + || active_account_currency_fx() != 1.0 + || !account_currency_fx_timestamps_.empty() + || max_intraday_filled_orders_ > 0 + || risk_max_intraday_loss_ != 0.0 || risk_max_drawdown_ != 0.0 + || risk_max_cons_loss_days_ > 0 + || last_margin_call_event_bar_ == bar_index_) { + return; + } + const std::size_t trades_before = trades_.size(); + process_margin_call(bar); + if (trades_.size() != trades_before) { + // The opening checkpoint and its adverse retry have both completed. + // A surviving partial must not revisit that high after the script. + intrabar_exit_margin_call_bar_ = bar_index_; + } +} + void BacktestEngine::process_margin_call(const Bar& bar) { // Consume first, including on disabled/degenerate paths. This is an event // attached to the just-completed fill cycle, never durable per-position diff --git a/src/engine_run.cpp b/src/engine_run.cpp index 76c6b00..a464038 100644 --- a/src/engine_run.cpp +++ b/src/engine_run.cpp @@ -76,6 +76,7 @@ double BacktestEngine::active_account_currency_fx() const { // evaluation. The latter installs its own scope around every security // evaluator dispatch and restores the prior thread-local value on return. void BacktestEngine::invoke_chart_on_bar(const Bar& bar) { + process_opening_short_margin_before_script(bar); struct ChartEmaNaWarmupScope { bool previous; explicit ChartEmaNaWarmupScope(bool enabled) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6fe391d..61bb531 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,5 @@ set(TEST_SOURCES + test_opening_short_margin_script_state test_ringbuffer test_timeframe test_magnifier diff --git a/tests/test_opening_short_margin_script_state.cpp b/tests/test_opening_short_margin_script_state.cpp new file mode 100644 index 0000000..e8b1115 --- /dev/null +++ b/tests/test_opening_short_margin_script_state.cpp @@ -0,0 +1,154 @@ +// R23 TradingView controls: a full opening-bar short liquidation is visible +// to the close-time script; a replacement may receive its own explicit bracket. +// Compact command fixtures use synthetic timestamps and fixed exit distances. +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace { +constexpr double qnan = std::numeric_limits::quiet_NaN(); +int passed = 0, failed = 0; +#define CHECK(x) do { if (x) ++passed; else { ++failed; std::printf("FAIL %d %s\n", __LINE__, #x); } } while (0) +bool near(double a, double b) { return std::abs(a - b) < 1e-7; } + +enum class Mode { DYNAMIC, EXPLICIT_BRACKET, DIFFERENT_ID, EXPLICIT_QTY, FIXED, PARTIAL_CLOSE }; +class ScriptView : public BacktestEngine { +public: + Mode mode; + double visible_first = qnan, visible_second = qnan; + double first_equity = qnan; + std::size_t first_closed = 0; + ScriptView(Mode value, double capital = 10117.291322) : mode(value) { + initial_capital_ = capital; + default_qty_type_ = mode == Mode::FIXED ? QtyType::FIXED : QtyType::PERCENT_OF_EQUITY; + default_qty_value_ = mode == Mode::FIXED ? 0.08733 : 100.0; + qty_step_ = 0.00001; + syminfo_mintick_ = 0.01; + syminfo_.pointvalue = 1.0; + margin_long_ = margin_short_ = 100.0; + commission_value_ = 0.0; + slippage_ = 0; + pyramiding_ = 0; + } + void on_bar(const Bar&) override { + if (bar_index_ == 1) { + visible_first = signed_position_size(); + first_equity = current_equity(); + first_closed = trades_.size(); + } + if (bar_index_ == 2) visible_second = signed_position_size(); + if (bar_index_ == 0 || (bar_index_ == 1 && mode != Mode::PARTIAL_CLOSE)) { + const std::string id = mode == Mode::DIFFERENT_ID && bar_index_ == 0 ? "First" : "Short"; + const double qty = mode == Mode::EXPLICIT_QTY ? (bar_index_ == 0 ? 0.08733 : 0.08739) : qnan; + strategy_entry(id, false, qnan, qnan, qty); + } + if (mode == Mode::EXPLICIT_BRACKET) { + if (bar_index_ == 1) strategy_exit("Short Exit", "Short", 115639.51, 115944.61); + } else { + const double average = signed_position_size() == 0.0 ? qnan : position_entry_price_; + const double distance = bar_index_ <= 1 ? 101.40652319727 : 109.08; + strategy_exit("Short Exit", "Short", average - 2 * distance, average + distance); + } + if (bar_index_ == 1 && mode == Mode::PARTIAL_CLOSE) { + strategy_close("Short", "half", qnan, 50.0); + } + if (bar_index_ == 3) strategy_close_all(); + } + const std::vector& rows() const { return trades_; } +}; + +const std::vector bars = { + {115842.32, 115842.32, 115842.32, 115842.32, 1, 1000}, + {115842.33, 115852.95, 115621.65, 115761.05, 1, 2000}, + {115761.06, 115812.71, 115603.98, 115688.35, 1, 3000}, + {115688.35, 115950.00, 115688.34, 115905.88, 1, 4000}, + {115905.88, 115916.73, 115800.00, 115854.00, 1, 5000}, +}; + +void test_full_liquidation_and_replacement() { + for (Mode mode : {Mode::DYNAMIC, Mode::DIFFERENT_ID, Mode::EXPLICIT_QTY}) { + ScriptView engine(mode); + engine.run(bars.data(), static_cast(bars.size())); + CHECK(near(engine.visible_first, 0.0)); + CHECK(engine.first_closed == 1); + CHECK(near(engine.first_equity, 10117.291322 - 0.9274446)); + CHECK(near(engine.visible_second, -0.08711)); + CHECK(engine.rows().size() == 3); + if (engine.rows().size() != 3) continue; + CHECK(engine.rows()[0].exit_time == 2000); + CHECK(engine.rows()[0].exit_id == "__margin_call__"); + CHECK(near(engine.rows()[0].qty, 0.08733)); + CHECK(near(engine.rows()[0].exit_price, 115852.95)); + CHECK(engine.rows()[1].exit_time == 3000); + CHECK(engine.rows()[1].exit_id == "__margin_call__"); + CHECK(near(engine.rows()[1].qty, 0.00028)); + CHECK(engine.rows()[2].exit_time == 4000); + CHECK(engine.rows()[2].exit_id == "Short Exit"); + CHECK(near(engine.rows()[2].qty, 0.08711)); + CHECK(near(engine.rows()[2].exit_price, 115870.14)); + } +} + +void test_explicit_bracket_survives() { + ScriptView engine(Mode::EXPLICIT_BRACKET); + engine.run(bars.data(), static_cast(bars.size())); + CHECK(near(engine.visible_first, 0.0)); + CHECK(engine.rows().size() == 3); + if (engine.rows().size() != 3) return; + CHECK(engine.rows()[2].exit_time == 3000); + CHECK(engine.rows()[2].exit_id == "Short Exit"); + CHECK(near(engine.rows()[2].exit_price, 115639.51)); + CHECK(near(engine.rows()[2].qty, 0.08711)); +} + +void test_partial_and_funded() { + ScriptView partial(Mode::DYNAMIC, 10116.7); + partial.run(bars.data(), static_cast(bars.size())); + CHECK(near(partial.visible_first, -0.08729)); + CHECK(partial.first_closed == 1); + CHECK(partial.rows().size() == 2); + if (partial.rows().size() == 2) { + CHECK(partial.rows()[0].exit_id == "__margin_call__"); + CHECK(near(partial.rows()[0].qty, 0.00004)); + CHECK(near(partial.rows()[1].qty, 0.08729)); + CHECK(partial.rows()[1].exit_time == 3000); + } + ScriptView funded(Mode::FIXED, 10200.0); + funded.run(bars.data(), static_cast(bars.size())); + CHECK(near(funded.visible_first, -0.08733)); + CHECK(funded.first_closed == 0); + CHECK(funded.rows().size() == 1); + if (funded.rows().size() == 1) { + CHECK(funded.rows()[0].exit_id == "Short Exit"); + CHECK(near(funded.rows()[0].qty, 0.08733)); + CHECK(funded.rows()[0].exit_time == 3000); + } +} + +void test_partial_close_reads_reduced_quantity() { + ScriptView engine(Mode::PARTIAL_CLOSE, 10116.7); + engine.run(bars.data(), static_cast(bars.size())); + CHECK(near(engine.visible_first, -0.08729)); + CHECK(engine.rows().size() == 3); + if (engine.rows().size() != 3) return; + CHECK(engine.rows()[0].exit_id == "__margin_call__"); + CHECK(near(engine.rows()[0].qty, 0.00004)); + CHECK(engine.rows()[1].exit_comment == "half"); + CHECK(near(engine.rows()[1].qty, 0.04364)); + CHECK(near(engine.rows()[1].exit_price, 115761.06)); + CHECK(engine.rows()[2].exit_id == "Short Exit"); + CHECK(near(engine.rows()[2].qty, 0.04365)); + CHECK(near(engine.rows()[2].exit_price, 115639.51)); +} +} +int main() { + test_full_liquidation_and_replacement(); + test_explicit_bracket_survives(); + test_partial_and_funded(); + test_partial_close_reads_reduced_quantity(); + std::printf("%d passed, %d failed\n", passed, failed); + return failed ? 1 : 0; +} From 59c2ff3067d14f4f1b6b7d15157bb4bd83b4d85f Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 7 Sep 2026 12:49:06 +0800 Subject: [PATCH 2/4] fix: settle carried short liquidation before script state reads --- include/pineforge/engine.hpp | 12 +- src/engine_fills.cpp | 46 ++- src/engine_run.cpp | 2 +- tests/CMakeLists.txt | 2 +- ...test_opening_short_margin_script_state.cpp | 154 -------- tests/test_short_margin_script_state.cpp | 332 ++++++++++++++++++ 6 files changed, 377 insertions(+), 171 deletions(-) delete mode 100644 tests/test_opening_short_margin_script_state.cpp create mode 100644 tests/test_short_margin_script_state.cpp diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 7a9b393..046df00 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -1346,10 +1346,10 @@ class BacktestEngine { // extreme (on the engine's own OHLC path) must let a pre-fill deficit // slice first. ``last_margin_call_event_bar_`` records the last // bar_index_ on which ANY margin-call trade row was booked (FX broker- - // open rollover, end-of-bar cascade, or the pre-exit slice); the + // open rollover, pre-script/end-of-bar cascade, or the pre-exit slice); the // pre-exit hook consults it so at most one forced-liquidation event - // fires per bar. ``intrabar_exit_margin_call_bar_`` is set ONLY by the - // pre-exit slice and tells the end-of-bar process_margin_call that this + // fires per bar. ``intrabar_exit_margin_call_bar_`` is set by a pre-exit + // slice or the scoped pre-script checkpoint and tells the later call that this // bar's adverse-extreme event was already consumed chronologically (the // surviving remainder is re-checked from the next bar on, preserving // TV's one-nibble-per-bar cascade). @@ -1891,9 +1891,9 @@ class BacktestEngine { // adverse-price liquidation; only an eligible one-shot post-fill // affordability event can trim it. void process_margin_call(const Bar& bar); - // Ordinary fresh sub-contract shorts with no resting orders expose their - // opening-bar liquidation to the close-time script (R23 TV controls). - void process_opening_short_margin_before_script(const Bar& bar); + // Ordinary sub-contract shorts expose completed liquidation to the + // close-time script (R23 opening and carried-position TV controls). + void process_short_margin_before_script(const Bar& bar); // finding-308: chronological pre-exit forced-liquidation slice. Called // from the process_pending_orders fill loop immediately BEFORE a priced // exit of the live position is applied. Fires only when (a) no margin diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 5a558d8..2eac08a 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -906,8 +906,9 @@ bool BacktestEngine::process_carried_position_fx_rollover(const Bar& bar) { // TradingView force-liquidation (margin call). // -// Run once per script bar (end of dispatch_bar / magnifier bar) after all -// order processing. Finite liquidation-price positions use the bar's ADVERSE +// The end-of-bar dispatcher retains the general checkpoint. Scoped pre-exit +// and pre-script sites settle earlier events and mark their consumed adverse +// check so the end-of-bar call cannot repeat it. Finite-price positions use the bar's ADVERSE // extreme (bar HIGH for shorts, bar LOW for leveraged longs). A long at // margin_long=100 has no adverse-price liquidation; it can only receive the // one-shot affordability event queued by a successful opening/add fill: @@ -1006,18 +1007,24 @@ bool BacktestEngine::entry_bar_post_fill_adverse(const Bar& bar, // Covered TV controls also expose a partial's reduced size (-0.08729) to a // 50% close, keep a funded short, and preserve an explicit bracket issued for // the pending replacement. Reuse the existing broker arithmetic and settle -// it before the script in this bounded, interaction-free opening topology. -void BacktestEngine::process_opening_short_margin_before_script(const Bar& bar) { +// it before the script in this bounded topology. The carried-position pins +// reproduce Ycelestine July 6: a full liquidation before the script permits +// its flat-gated Long entry. A resting own bracket that did not fill does not +// postpone that margin event; after a full close it belongs to the old cycle. +void BacktestEngine::process_short_margin_before_script(const Bar& bar) { if (!margin_call_enabled_ || position_side_ != PositionSide::SHORT - || !entry_bar_margin_path_scope() + || process_orders_on_close_ || calc_on_order_fills_ + || bar_magnifier_enabled_ || coof_scheduler_active_ + || stream_warmup_mode_ || stream_phase_ != StreamPhase::IDLE + || position_open_bar_ < 0 || position_open_bar_ > bar_index_ || bar.timestamp != current_bar_.timestamp - || !pending_orders_.empty() + || pending_orders_.size() > 1 || !(position_qty_ > 0.0 && position_qty_ <= 1.0) || !(qty_step_ > 0.0 && qty_step_ < 1.0) || pyramiding_ < 0 || pyramiding_ > 1 || position_entry_count_ != 1 || pyramid_entries_.size() != 1 || !pyramid_entries_.front().ordinary_market_open - || pyramid_entries_.front().entry_bar_index != bar_index_ + || pyramid_entries_.front().entry_bar_index != position_open_bar_ || commission_value_ != 0.0 || slippage_ != 0 || margin_short_ != 100.0 || syminfo_.pointvalue != 1.0 || active_account_currency_fx() != 1.0 @@ -1028,12 +1035,33 @@ void BacktestEngine::process_opening_short_margin_before_script(const Bar& bar) || last_margin_call_event_bar_ == bar_index_) { return; } + for (const auto& order : pending_orders_) { + // Pending entries/closes, foreign or global brackets, and dormant or + // trailing lifecycles retain their established scheduling. The order + // kernel has already evaluated this ordinary own priced bracket over + // the bar; if it filled, the resulting position is what we see here. + if (order.type != OrderType::EXIT + || order.from_entry != pyramid_entries_.front().entry_id + || order.dormant_bracket || order.dormant_reissue_pending + || !std::isnan(order.trail_points) + || !std::isnan(order.trail_price) + || (!std::isfinite(order.limit_price) + && !std::isfinite(order.stop_price))) { + return; + } + } const std::size_t trades_before = trades_.size(); process_margin_call(bar); if (trades_.size() != trades_before) { - // The opening checkpoint and its adverse retry have both completed. - // A surviving partial must not revisit that high after the script. + // All checkpoints in this call have completed. A surviving partial + // must not revisit that high after the script. intrabar_exit_margin_call_bar_ = bar_index_; + if (position_side_ == PositionSide::FLAT) { + // No pending parent entry passed the scope check above. Retire the + // old cycle's bracket now; the upcoming script can independently + // attach an explicit bracket to a newly placed replacement. + purge_exit_orders(); + } } } diff --git a/src/engine_run.cpp b/src/engine_run.cpp index a464038..061a009 100644 --- a/src/engine_run.cpp +++ b/src/engine_run.cpp @@ -76,7 +76,7 @@ double BacktestEngine::active_account_currency_fx() const { // evaluation. The latter installs its own scope around every security // evaluator dispatch and restores the prior thread-local value on return. void BacktestEngine::invoke_chart_on_bar(const Bar& bar) { - process_opening_short_margin_before_script(bar); + process_short_margin_before_script(bar); struct ChartEmaNaWarmupScope { bool previous; explicit ChartEmaNaWarmupScope(bool enabled) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 61bb531..5790a95 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,5 @@ set(TEST_SOURCES - test_opening_short_margin_script_state + test_short_margin_script_state test_ringbuffer test_timeframe test_magnifier diff --git a/tests/test_opening_short_margin_script_state.cpp b/tests/test_opening_short_margin_script_state.cpp deleted file mode 100644 index e8b1115..0000000 --- a/tests/test_opening_short_margin_script_state.cpp +++ /dev/null @@ -1,154 +0,0 @@ -// R23 TradingView controls: a full opening-bar short liquidation is visible -// to the close-time script; a replacement may receive its own explicit bracket. -// Compact command fixtures use synthetic timestamps and fixed exit distances. -#include -#include -#include -#include -#include - -using namespace pineforge; -namespace { -constexpr double qnan = std::numeric_limits::quiet_NaN(); -int passed = 0, failed = 0; -#define CHECK(x) do { if (x) ++passed; else { ++failed; std::printf("FAIL %d %s\n", __LINE__, #x); } } while (0) -bool near(double a, double b) { return std::abs(a - b) < 1e-7; } - -enum class Mode { DYNAMIC, EXPLICIT_BRACKET, DIFFERENT_ID, EXPLICIT_QTY, FIXED, PARTIAL_CLOSE }; -class ScriptView : public BacktestEngine { -public: - Mode mode; - double visible_first = qnan, visible_second = qnan; - double first_equity = qnan; - std::size_t first_closed = 0; - ScriptView(Mode value, double capital = 10117.291322) : mode(value) { - initial_capital_ = capital; - default_qty_type_ = mode == Mode::FIXED ? QtyType::FIXED : QtyType::PERCENT_OF_EQUITY; - default_qty_value_ = mode == Mode::FIXED ? 0.08733 : 100.0; - qty_step_ = 0.00001; - syminfo_mintick_ = 0.01; - syminfo_.pointvalue = 1.0; - margin_long_ = margin_short_ = 100.0; - commission_value_ = 0.0; - slippage_ = 0; - pyramiding_ = 0; - } - void on_bar(const Bar&) override { - if (bar_index_ == 1) { - visible_first = signed_position_size(); - first_equity = current_equity(); - first_closed = trades_.size(); - } - if (bar_index_ == 2) visible_second = signed_position_size(); - if (bar_index_ == 0 || (bar_index_ == 1 && mode != Mode::PARTIAL_CLOSE)) { - const std::string id = mode == Mode::DIFFERENT_ID && bar_index_ == 0 ? "First" : "Short"; - const double qty = mode == Mode::EXPLICIT_QTY ? (bar_index_ == 0 ? 0.08733 : 0.08739) : qnan; - strategy_entry(id, false, qnan, qnan, qty); - } - if (mode == Mode::EXPLICIT_BRACKET) { - if (bar_index_ == 1) strategy_exit("Short Exit", "Short", 115639.51, 115944.61); - } else { - const double average = signed_position_size() == 0.0 ? qnan : position_entry_price_; - const double distance = bar_index_ <= 1 ? 101.40652319727 : 109.08; - strategy_exit("Short Exit", "Short", average - 2 * distance, average + distance); - } - if (bar_index_ == 1 && mode == Mode::PARTIAL_CLOSE) { - strategy_close("Short", "half", qnan, 50.0); - } - if (bar_index_ == 3) strategy_close_all(); - } - const std::vector& rows() const { return trades_; } -}; - -const std::vector bars = { - {115842.32, 115842.32, 115842.32, 115842.32, 1, 1000}, - {115842.33, 115852.95, 115621.65, 115761.05, 1, 2000}, - {115761.06, 115812.71, 115603.98, 115688.35, 1, 3000}, - {115688.35, 115950.00, 115688.34, 115905.88, 1, 4000}, - {115905.88, 115916.73, 115800.00, 115854.00, 1, 5000}, -}; - -void test_full_liquidation_and_replacement() { - for (Mode mode : {Mode::DYNAMIC, Mode::DIFFERENT_ID, Mode::EXPLICIT_QTY}) { - ScriptView engine(mode); - engine.run(bars.data(), static_cast(bars.size())); - CHECK(near(engine.visible_first, 0.0)); - CHECK(engine.first_closed == 1); - CHECK(near(engine.first_equity, 10117.291322 - 0.9274446)); - CHECK(near(engine.visible_second, -0.08711)); - CHECK(engine.rows().size() == 3); - if (engine.rows().size() != 3) continue; - CHECK(engine.rows()[0].exit_time == 2000); - CHECK(engine.rows()[0].exit_id == "__margin_call__"); - CHECK(near(engine.rows()[0].qty, 0.08733)); - CHECK(near(engine.rows()[0].exit_price, 115852.95)); - CHECK(engine.rows()[1].exit_time == 3000); - CHECK(engine.rows()[1].exit_id == "__margin_call__"); - CHECK(near(engine.rows()[1].qty, 0.00028)); - CHECK(engine.rows()[2].exit_time == 4000); - CHECK(engine.rows()[2].exit_id == "Short Exit"); - CHECK(near(engine.rows()[2].qty, 0.08711)); - CHECK(near(engine.rows()[2].exit_price, 115870.14)); - } -} - -void test_explicit_bracket_survives() { - ScriptView engine(Mode::EXPLICIT_BRACKET); - engine.run(bars.data(), static_cast(bars.size())); - CHECK(near(engine.visible_first, 0.0)); - CHECK(engine.rows().size() == 3); - if (engine.rows().size() != 3) return; - CHECK(engine.rows()[2].exit_time == 3000); - CHECK(engine.rows()[2].exit_id == "Short Exit"); - CHECK(near(engine.rows()[2].exit_price, 115639.51)); - CHECK(near(engine.rows()[2].qty, 0.08711)); -} - -void test_partial_and_funded() { - ScriptView partial(Mode::DYNAMIC, 10116.7); - partial.run(bars.data(), static_cast(bars.size())); - CHECK(near(partial.visible_first, -0.08729)); - CHECK(partial.first_closed == 1); - CHECK(partial.rows().size() == 2); - if (partial.rows().size() == 2) { - CHECK(partial.rows()[0].exit_id == "__margin_call__"); - CHECK(near(partial.rows()[0].qty, 0.00004)); - CHECK(near(partial.rows()[1].qty, 0.08729)); - CHECK(partial.rows()[1].exit_time == 3000); - } - ScriptView funded(Mode::FIXED, 10200.0); - funded.run(bars.data(), static_cast(bars.size())); - CHECK(near(funded.visible_first, -0.08733)); - CHECK(funded.first_closed == 0); - CHECK(funded.rows().size() == 1); - if (funded.rows().size() == 1) { - CHECK(funded.rows()[0].exit_id == "Short Exit"); - CHECK(near(funded.rows()[0].qty, 0.08733)); - CHECK(funded.rows()[0].exit_time == 3000); - } -} - -void test_partial_close_reads_reduced_quantity() { - ScriptView engine(Mode::PARTIAL_CLOSE, 10116.7); - engine.run(bars.data(), static_cast(bars.size())); - CHECK(near(engine.visible_first, -0.08729)); - CHECK(engine.rows().size() == 3); - if (engine.rows().size() != 3) return; - CHECK(engine.rows()[0].exit_id == "__margin_call__"); - CHECK(near(engine.rows()[0].qty, 0.00004)); - CHECK(engine.rows()[1].exit_comment == "half"); - CHECK(near(engine.rows()[1].qty, 0.04364)); - CHECK(near(engine.rows()[1].exit_price, 115761.06)); - CHECK(engine.rows()[2].exit_id == "Short Exit"); - CHECK(near(engine.rows()[2].qty, 0.04365)); - CHECK(near(engine.rows()[2].exit_price, 115639.51)); -} -} -int main() { - test_full_liquidation_and_replacement(); - test_explicit_bracket_survives(); - test_partial_and_funded(); - test_partial_close_reads_reduced_quantity(); - std::printf("%d passed, %d failed\n", passed, failed); - return failed ? 1 : 0; -} diff --git a/tests/test_short_margin_script_state.cpp b/tests/test_short_margin_script_state.cpp new file mode 100644 index 0000000..6059926 --- /dev/null +++ b/tests/test_short_margin_script_state.cpp @@ -0,0 +1,332 @@ +// R23 TradingView controls: a full opening-bar short liquidation is visible +// to the close-time script; a replacement may receive its own explicit bracket. +// Compact command fixtures use synthetic timestamps and fixed exit distances. +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace { +constexpr double qnan = std::numeric_limits::quiet_NaN(); +int passed = 0, failed = 0; +#define CHECK(x) do { if (x) ++passed; else { ++failed; std::printf("FAIL %d %s\n", __LINE__, #x); } } while (0) +bool near(double a, double b) { return std::abs(a - b) < 1e-7; } + +enum class Mode { DYNAMIC, EXPLICIT_BRACKET, DIFFERENT_ID, EXPLICIT_QTY, FIXED, PARTIAL_CLOSE }; +class ScriptView : public BacktestEngine { +public: + Mode mode; + double visible_first = qnan, visible_second = qnan; + double first_equity = qnan; + std::size_t first_closed = 0; + ScriptView(Mode value, double capital = 10117.291322) : mode(value) { + initial_capital_ = capital; + default_qty_type_ = mode == Mode::FIXED ? QtyType::FIXED : QtyType::PERCENT_OF_EQUITY; + default_qty_value_ = mode == Mode::FIXED ? 0.08733 : 100.0; + qty_step_ = 0.00001; + syminfo_mintick_ = 0.01; + syminfo_.pointvalue = 1.0; + margin_long_ = margin_short_ = 100.0; + commission_value_ = 0.0; + slippage_ = 0; + pyramiding_ = 0; + } + void on_bar(const Bar&) override { + if (bar_index_ == 1) { + visible_first = signed_position_size(); + first_equity = current_equity(); + first_closed = trades_.size(); + } + if (bar_index_ == 2) visible_second = signed_position_size(); + if (bar_index_ == 0 || (bar_index_ == 1 && mode != Mode::PARTIAL_CLOSE)) { + const std::string id = mode == Mode::DIFFERENT_ID && bar_index_ == 0 ? "First" : "Short"; + const double qty = mode == Mode::EXPLICIT_QTY ? (bar_index_ == 0 ? 0.08733 : 0.08739) : qnan; + strategy_entry(id, false, qnan, qnan, qty); + } + if (mode == Mode::EXPLICIT_BRACKET) { + if (bar_index_ == 1) strategy_exit("Short Exit", "Short", 115639.51, 115944.61); + } else { + const double average = signed_position_size() == 0.0 ? qnan : position_entry_price_; + const double distance = bar_index_ <= 1 ? 101.40652319727 : 109.08; + strategy_exit("Short Exit", "Short", average - 2 * distance, average + distance); + } + if (bar_index_ == 1 && mode == Mode::PARTIAL_CLOSE) { + strategy_close("Short", "half", qnan, 50.0); + } + if (bar_index_ == 3) strategy_close_all(); + } + const std::vector& rows() const { return trades_; } +}; + +const std::vector bars = { + {115842.32, 115842.32, 115842.32, 115842.32, 1, 1000}, + {115842.33, 115852.95, 115621.65, 115761.05, 1, 2000}, + {115761.06, 115812.71, 115603.98, 115688.35, 1, 3000}, + {115688.35, 115950.00, 115688.34, 115905.88, 1, 4000}, + {115905.88, 115916.73, 115800.00, 115854.00, 1, 5000}, +}; + +void test_full_liquidation_and_replacement() { + for (Mode mode : {Mode::DYNAMIC, Mode::DIFFERENT_ID, Mode::EXPLICIT_QTY}) { + ScriptView engine(mode); + engine.run(bars.data(), static_cast(bars.size())); + CHECK(near(engine.visible_first, 0.0)); + CHECK(engine.first_closed == 1); + CHECK(near(engine.first_equity, 10117.291322 - 0.9274446)); + CHECK(near(engine.visible_second, -0.08711)); + CHECK(engine.rows().size() == 3); + if (engine.rows().size() != 3) continue; + CHECK(engine.rows()[0].exit_time == 2000); + CHECK(engine.rows()[0].exit_id == "__margin_call__"); + CHECK(near(engine.rows()[0].qty, 0.08733)); + CHECK(near(engine.rows()[0].exit_price, 115852.95)); + CHECK(engine.rows()[1].exit_time == 3000); + CHECK(engine.rows()[1].exit_id == "__margin_call__"); + CHECK(near(engine.rows()[1].qty, 0.00028)); + CHECK(engine.rows()[2].exit_time == 4000); + CHECK(engine.rows()[2].exit_id == "Short Exit"); + CHECK(near(engine.rows()[2].qty, 0.08711)); + CHECK(near(engine.rows()[2].exit_price, 115870.14)); + } +} + +void test_explicit_bracket_survives() { + ScriptView engine(Mode::EXPLICIT_BRACKET); + engine.run(bars.data(), static_cast(bars.size())); + CHECK(near(engine.visible_first, 0.0)); + CHECK(engine.rows().size() == 3); + if (engine.rows().size() != 3) return; + CHECK(engine.rows()[2].exit_time == 3000); + CHECK(engine.rows()[2].exit_id == "Short Exit"); + CHECK(near(engine.rows()[2].exit_price, 115639.51)); + CHECK(near(engine.rows()[2].qty, 0.08711)); +} + +void test_partial_and_funded() { + ScriptView partial(Mode::DYNAMIC, 10116.7); + partial.run(bars.data(), static_cast(bars.size())); + CHECK(near(partial.visible_first, -0.08729)); + CHECK(partial.first_closed == 1); + CHECK(partial.rows().size() == 2); + if (partial.rows().size() == 2) { + CHECK(partial.rows()[0].exit_id == "__margin_call__"); + CHECK(near(partial.rows()[0].qty, 0.00004)); + CHECK(near(partial.rows()[1].qty, 0.08729)); + CHECK(partial.rows()[1].exit_time == 3000); + } + ScriptView funded(Mode::FIXED, 10200.0); + funded.run(bars.data(), static_cast(bars.size())); + CHECK(near(funded.visible_first, -0.08733)); + CHECK(funded.first_closed == 0); + CHECK(funded.rows().size() == 1); + if (funded.rows().size() == 1) { + CHECK(funded.rows()[0].exit_id == "Short Exit"); + CHECK(near(funded.rows()[0].qty, 0.08733)); + CHECK(funded.rows()[0].exit_time == 3000); + } +} + +void test_partial_close_reads_reduced_quantity() { + ScriptView engine(Mode::PARTIAL_CLOSE, 10116.7); + engine.run(bars.data(), static_cast(bars.size())); + CHECK(near(engine.visible_first, -0.08729)); + CHECK(engine.rows().size() == 3); + if (engine.rows().size() != 3) return; + CHECK(engine.rows()[0].exit_id == "__margin_call__"); + CHECK(near(engine.rows()[0].qty, 0.00004)); + CHECK(engine.rows()[1].exit_comment == "half"); + CHECK(near(engine.rows()[1].qty, 0.04364)); + CHECK(near(engine.rows()[1].exit_price, 115761.06)); + CHECK(engine.rows()[2].exit_id == "Short Exit"); + CHECK(near(engine.rows()[2].qty, 0.04365)); + CHECK(near(engine.rows()[2].exit_price, 115639.51)); +} + +class CarriedView : public BacktestEngine { +public: + bool resting_bracket, partial_close; + double carried_partial_view = qnan, full_close_view = qnan; + bool old_bracket_at_full_close = false; + CarriedView(bool resting, bool partial) : resting_bracket(resting), partial_close(partial) { + initial_capital_ = 10294.985534; + default_qty_type_ = QtyType::PERCENT_OF_EQUITY; + default_qty_value_ = 100.0; + qty_step_ = 0.00001; + syminfo_mintick_ = 0.01; + syminfo_.pointvalue = 1.0; + commission_value_ = 0.0; + slippage_ = 0; + pyramiding_ = 0; + } + void on_bar(const Bar&) override { + if (bar_index_ == 0) strategy_entry("Short", false, qnan, qnan, 0.09525); + if (bar_index_ == 2) carried_partial_view = signed_position_size(); + if (bar_index_ == 3) { + full_close_view = signed_position_size(); + for (const auto& order : pending_orders_) { + if (order.id == "Short Exit") old_bracket_at_full_close = true; + } + } + if (resting_bracket && signed_position_size() < 0.0) { + strategy_exit("Short Exit", "Short", 107000.0, 110000.0); + } + if (partial_close && bar_index_ == 2) strategy_close("Short", "part", qnan, 10.0); + if (bar_index_ == 3 && signed_position_size() == 0.0) { + strategy_entry("Long", true); + strategy_exit("Long Exit", "Long", 110000.0, 108033.74); + } + if (bar_index_ == 5) strategy_close_all(); + } + const std::vector& rows() const { return trades_; } +}; + +void test_carried_liquidation_script_state() { + const std::vector carry_bars = { + {108078.08, 108078.08, 108078.08, 108078.08, 1, 1000}, + {108078.07, 108110.77, 108053.30, 108092.00, 1, 2000}, + {108092.00, 108216.22, 108070.00, 108161.00, 1, 3000}, + {108218.25, 108267.53, 108183.40, 108250.00, 1, 4000}, + {108250.01, 108268.35, 108134.08, 108155.04, 1, 5000}, + {108155.04, 108155.05, 108020.00, 108033.74, 1, 6000}, + {108100.00, 108100.00, 108100.00, 108100.00, 1, 7000}, + }; + for (bool resting : {false, true}) { + CarriedView engine(resting, false); + engine.run(carry_bars.data(), static_cast(carry_bars.size())); + CHECK(near(engine.carried_partial_view, -0.09493)); + CHECK(near(engine.full_close_view, 0.0)); + CHECK(!engine.old_bracket_at_full_close); + CHECK(engine.rows().size() == 4); + if (engine.rows().size() != 4) continue; + CHECK(near(engine.rows()[0].qty, 0.0002)); + CHECK(near(engine.rows()[1].qty, 0.00012)); + CHECK(engine.rows()[2].exit_id == "__margin_call__"); + CHECK(engine.rows()[2].exit_time == 4000); + CHECK(near(engine.rows()[2].qty, 0.09493)); + CHECK(near(engine.rows()[2].exit_price, 108267.53)); + CHECK(engine.rows()[3].entry_time == 5000); + CHECK(engine.rows()[3].exit_id == "Long Exit"); + CHECK(near(engine.rows()[3].qty, 0.09493)); + CHECK(near(engine.rows()[3].entry_price, 108250.01)); + CHECK(near(engine.rows()[3].exit_price, 108033.74)); + } + auto partial_bars = carry_bars; + partial_bars[3] = {108153.99, 108200.0, 108050.0, 108100.0, 1, 4000}; + CarriedView partial(true, true); + partial.run(partial_bars.data(), static_cast(partial_bars.size())); + CHECK(near(partial.carried_partial_view, -0.09493)); + CHECK(partial.rows().size() == 4); + if (partial.rows().size() == 4) { + CHECK(partial.rows()[2].exit_comment == "part"); + CHECK(near(partial.rows()[2].qty, 0.00949)); + CHECK(near(partial.rows()[2].exit_price, 108153.99)); + CHECK(near(partial.rows()[3].qty, 0.08544)); + } +} + +// The same broker snapshot liquidates when this checkpoint owns it. Other +// dispatchers and pending-order lifecycles must retain both their live position +// and their order book for their existing settlement path. +class CheckpointOwnership : public BacktestEngine { +public: + explicit CheckpointOwnership(int scenario) { + initial_capital_ = 50.0; + current_bar_ = {100.0, 100.01, 99.0, 99.5, 1, 2000}; + bar_index_ = 1; + position_open_bar_ = 0; + position_side_ = PositionSide::SHORT; + position_qty_ = 0.5; + position_entry_price_ = 100.0; + position_entry_time_ = 1000; + position_entry_count_ = 1; + position_cycle_seq_ = 1; + qty_step_ = 0.01; + syminfo_mintick_ = 0.01; + syminfo_.pointvalue = 1.0; + PyramidEntry entry{}; + entry.price = 100.0; + entry.qty = 0.5; + entry.time = 1000; + entry.entry_id = "Short"; + entry.entry_bar_index = 0; + entry.entry_incarnation = 7; + entry.ordinary_market_open = true; + pyramid_entries_.push_back(entry); + cycle_filled_entry_ids_.insert("Short"); + switch (scenario) { + case 1: process_orders_on_close_ = true; break; + case 2: calc_on_order_fills_ = true; break; + case 3: bar_magnifier_enabled_ = true; break; + case 4: stream_phase_ = StreamPhase::REALTIME; break; + case 5: commission_value_ = 0.1; break; + case 6: + account_currency_fx_timestamps_ = {0}; + account_currency_fx_rates_ = {1.0}; + break; + case 7: + qty_step_ = 1.0; + position_qty_ = pyramid_entries_[0].qty = 1.0; + initial_capital_ = 100.0; + break; + case 8: pyramid_entries_[0].ordinary_market_open = false; break; + case 9: + position_side_ = PositionSide::LONG; + margin_long_ = 50.0; + initial_capital_ = 25.0; + break; + case 10: coof_scheduler_active_ = true; break; + default: break; + } + if (scenario >= 11) { + PendingOrder order; + order.id = "Exit"; + order.type = OrderType::EXIT; + order.from_entry = "Short"; + order.stop_price = 102.0; + order.limit_price = 98.0; + if (scenario == 11) { order.type = OrderType::MARKET; order.id = "Next"; } + if (scenario == 12) order.from_entry = "Foreign"; + if (scenario == 13) order.from_entry.clear(); + if (scenario == 14) order.trail_points = 10.0; + if (scenario == 15) order.trail_points = INFINITY; + if (scenario == 16) order.dormant_bracket = true; + pending_orders_.push_back(order); + } + } + void on_bar(const Bar&) override {} + void checkpoint() { process_short_margin_before_script(current_bar_); } + std::size_t trades_count() const { return trades_.size(); } + std::size_t pending_count() const { return pending_orders_.size(); } + double quantity() const { return position_qty_; } + double realized() const { return net_profit_sum_; } +}; + +void test_other_checkpoint_owners_are_untouched() { + CheckpointOwnership owned(0); + owned.checkpoint(); + CHECK(owned.trades_count() == 1); + CHECK(owned.quantity() == 0.0); + for (int scenario = 1; scenario <= 16; ++scenario) { + CheckpointOwnership other(scenario); + const double quantity_before = other.quantity(); + const auto orders_before = other.pending_count(); + other.checkpoint(); + CHECK(other.trades_count() == 0); + CHECK(other.quantity() == quantity_before); + CHECK(other.realized() == 0.0); + CHECK(other.pending_count() == orders_before); + } +} +} +int main() { + test_full_liquidation_and_replacement(); + test_explicit_bracket_survives(); + test_partial_and_funded(); + test_partial_close_reads_reduced_quantity(); + test_carried_liquidation_script_state(); + test_other_checkpoint_owners_are_untouched(); + std::printf("%d passed, %d failed\n", passed, failed); + return failed ? 1 : 0; +} From 7ad5d38d7230c973421fa6fb46e2b0432e4ea408 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 7 Sep 2026 13:17:55 +0800 Subject: [PATCH 3/4] docs: refresh parity scoreboard through round 23 --- README.md | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5f02276..176eb0e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **The open-source PineScript v6 backtest engine that reproduces TradingView trade-for-trade.** [![CI](https://img.shields.io/github/actions/workflow/status/pineforge-4pass/pineforge-engine/ci.yml?branch=main&label=ci&logo=github)](https://github.com/pineforge-4pass/pineforge-engine/actions) -[![Parity](https://img.shields.io/badge/TradingView%20parity-4%2C189%20%2F%204%2C190%20probes-brightgreen)](#validation-scoreboard) +[![Parity](https://img.shields.io/badge/TradingView%20parity-4%2C190%20%2F%204%2C190%20probes-brightgreen)](#validation-scoreboard) [![Trades](https://img.shields.io/badge/trades%20matched-2.8M-brightgreen)](#validation-scoreboard) [![Speed](https://img.shields.io/badge/median%20162%C3%97%20vs%20PyneCore-success)](benchmarks/results/speed.md)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) @@ -26,7 +26,7 @@ TradingView's strategy tester is the reference every Pine author trusts, and nothing outside TradingView reproduced it — until now. PineForge is a C++17 runtime with a stable C ABI that runs PineScript v6 strategies exactly the way TradingView's broker emulator does: same fills, same sizing, same margin calls, same trailing stops, same `request.security()` buckets, on any OHLCV you give it, in microseconds per bar. -- **Proven, not promised.** 4,189 of 4,190 probes — 312 open reference strategies plus 413 real community scripts on 15 markets and timeframes — grade *excellent* or *strong* against TradingView's own trade lists. 2.82 million TradingView trades measured, 2.815 million matched row-for-row. +- **Proven, not promised.** All 4,190 probes — 312 open reference strategies plus 413 real community scripts on 15 markets and timeframes — grade *excellent* or *strong* against TradingView's own trade lists: **4,166 excellent, 24 strong, zero moderate**. The current full sweep evaluates 2,819,967 TradingView trades, with 2,816,973 matched by the verifier. - **Open.** Engine, transpiler, corpus, benchmarks and the validation tooling are all public and Apache-2.0. The only thing you cannot download is the closed test set, because TradingView's Terms of Service forbid redistributing community scripts. - **Fast.** In-process, no interpreter: median **162× faster than PyneCore** on 99 timed strategies. Parameter sweeps re-run a loaded `.so` with new inputs — no recompile, no fork. - **Deterministic to the bit.** Two runs with the same inputs produce identical trade lists. Same on Linux and macOS. @@ -76,7 +76,7 @@ Prefer zero install? The hosted server at **[mcp.pineforge.dev/mcp](https://mcp. git clone https://github.com/pineforge-4pass/pineforge-engine.git && cd pineforge-engine cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j -ctest --test-dir build --output-on-failure # 198 tests +ctest --test-dir build --output-on-failure # 215 tests bash tutorial/run.sh # MACD on BTC/USDT, end to end python3 tutorial/run_stream.py # OHLCV warm-up → realtime trades ``` @@ -108,33 +108,37 @@ Every PineForge-compiled strategy `.so` exports this same ABI — write the harn ## Validation scoreboard -| Board | Test set | Result | Trades verified | +**Round 23 · 2026-09-07:** **4,166 excellent / 24 strong / zero moderate** across all **4,190 scored probes**. This round adds two excellent results, with zero regressions on any canonical metric. + +| Board | Test set | Result | TradingView trades evaluated | |---|---|---|---| -| **Public** — [open corpus](https://github.com/pineforge-4pass/pineforge-corpus) | 312 reference strategies, Apache-2.0, reproducible by anyone | **309/309 graded excellent** trade-for-trade (ETH/USDT-perp 15m; the corpus' declared engine-only / anomaly probes are not graded) | ~430k | -| **Closed test** — the parity campaign | 413 community-shared TradingView scripts × 15 market/timeframe lanes = **3,881 script-lane probes** — private under TradingView's Terms of Service | **3,825 excellent + 55 strong + 1 moderate** = 3,880/3,881 (99.97%) excellent-or-strong | ~2.4M | +| **Public** — [open corpus](https://github.com/pineforge-4pass/pineforge-corpus) | 312 reference strategies, Apache-2.0, reproducible by anyone | **309/309 graded excellent** (ETH/USDT-perp 15m; the corpus' declared engine-only / anomaly probes are not graded) | 429,866 | +| **Closed test** — the parity campaign | 413 community-shared TradingView scripts across 15 market/timeframe lanes: **3,881 script-lane probes** — private under TradingView's Terms of Service | **3,857 excellent + 24 strong + zero moderate** = 3,881/3,881 (100%) excellent-or-strong | 2,390,101 | + +**2,819,967 TradingView trades** evaluated, **2,816,973 matched by the verifier** (99.89%), from the round 23 full Cloud Run sweep. **18 TradingView-side anomalies** remain excluded under the unchanged population; each was documented before exclusion. No scored probe remains below *strong*. -**2.82 million TradingView trades** measured, **2.815 million matched row-for-row** (99.8%), as of **2026-09-06**. **18 TradingView-side anomalies** were found on the way (all on the ETH 15m lane), each confirmed with a purpose-built sensor script exported from TradingView and documented before exclusion. The one probe below *strong* is a verifier-harness limitation on a range-start chart trim, not an engine divergence. +Round 23 improves the BTC/USDT 15m **Trendline and Horizontal Breakout** and **LL: Momentum and Curl Master** strategies from strong to excellent. Their combined **10,742 trade rows** match TradingView exactly on side, time, price, and quantity. The engine fix uses shared broker state and order ownership; it contains no strategy, symbol, date, or benchmark-ID conditions. Grading rules, verifier, harness, population, and tapes are unchanged. ### The closed test, lane by lane | Market · timeframe | Probes | Excellent | Strong | Moderate | |---|---:|---:|---:|---:| -| BINANCE:ETHUSDT.P · 15m *(hard lane: zero regression allowed)* | 395 | 393 | 2 | — | -| BINANCE:BTCUSDT · 15m | 354 | 341 | 13 | — | -| BINANCE:BTCUSDT · 1D | 259 | 258 | 1 | — | +| BINANCE:ETHUSDT.P · 15m *(hard lane: zero regression allowed)* | 395 | 394 | 1 | — | +| BINANCE:BTCUSDT · 15m | 354 | 350 | 4 | — | +| BINANCE:BTCUSDT · 1D | 259 | 259 | — | — | | CME_MINI:ES1! · 15m | 174 | 173 | 1 | — | -| CME_MINI:ES1! · 1D | 117 | 116 | 1 | — | +| CME_MINI:ES1! · 1D | 117 | 117 | — | — | | CME_MINI:NQ1! · 15m | 174 | 174 | — | — | | CME_MINI:NQ1! · 1D | 116 | 116 | — | — | -| NASDAQ:AAPL · 15m | 356 | 352 | 4 | — | +| NASDAQ:AAPL · 15m | 356 | 354 | 2 | — | | NSE:NIFTY · 15m | 191 | 190 | 1 | — | | NSE:NIFTY · 1D | 145 | 145 | — | — | -| NYSE:F · 15m | 340 | 331 | 9 | — | -| NYSE:F · 1D | 263 | 262 | 1 | — | -| OANDA:EURUSD · 15m | 373 | 356 | 17 | — | -| OANDA:XAUUSD · 15m | 376 | 370 | 5 | 1 | +| NYSE:F · 15m | 340 | 335 | 5 | — | +| NYSE:F · 1D | 263 | 263 | — | — | +| OANDA:EURUSD · 15m | 373 | 365 | 8 | — | +| OANDA:XAUUSD · 15m | 376 | 374 | 2 | — | | OANDA:XAUUSD · 1D | 248 | 248 | — | — | -| **Total** | **3,881** | **3,825** | **55** | **1** | +| **Total** | **3,881** | **3,857** | **24** | **0** | ### How a probe is graded From 00938e4ca48ae6b37e6979f9c6b0f9a2969b0e79 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Mon, 7 Sep 2026 13:32:06 +0800 Subject: [PATCH 4/4] docs: retire ad-hoc 4emarsi research files and fix test inventory --- README.md | 4 +- docs/pages/metrics.md | 7 +- tests/test_affordability_fx.cpp | 2 +- .../inputs.json | 14 - .../tv_commission_crack.py | 183 --------- .../tv_equity_crack.py | 358 ------------------ .../usdtusd_daily_close.json | 1 - 7 files changed, 6 insertions(+), 563 deletions(-) delete mode 100644 validation-adhoc/4emarsi-commission-slippage-ethusdt/inputs.json delete mode 100644 validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_commission_crack.py delete mode 100644 validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_equity_crack.py delete mode 100644 validation-adhoc/4emarsi-commission-slippage-ethusdt/usdtusd_daily_close.json diff --git a/README.md b/README.md index 176eb0e..0cec9e5 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ PyneCore's 15 non-excellent strategies involve `strategy.exit(stop=…, limit= - `libpineforge.a` — the static runtime: order matching and fills, sizing and margin, the bar magnifier, 66 indicator classes, `request.security()`, time and session math. - `` — the public C ABI, the stability-pinned consumer surface. - `` — internal C++ headers the transpiler emits against (not part of the stability guarantee). -- 198 ctest cases, most of them replays of recorded TradingView bars; CI on Linux + macOS × Release + Debug, sanitizers, and a `find_package` smoke consumer. +- 215 ctest cases, most of them replays of recorded TradingView bars; CI on Linux + macOS × Release + Debug, sanitizers, and a `find_package` smoke consumer. - `corpus/` — the 312-strategy public validation corpus (submodule). - `benchmarks/` — the three-way comparison harness and the throughput package. - `scripts/` — `run_corpus.sh`, `verify_corpus.py`, `run_strategy.py` (load any `.so` via ctypes), `regen_corpus_cpp.sh`, `coverage.sh`. @@ -244,7 +244,7 @@ src/ 26 .cpp files split by concern ├── ta_*.cpp 66 indicator classes (moving averages, oscillators, │ volatility/trend, extremes/volume, misc) └── magnifier / matrix / session_time / timeframe / timezone / math / str_utils -tests/ 198 ctest cases (C++ unit + TradingView replay tests, 1 pure-C ABI check) +tests/ 215 ctest cases (C++ unit + TradingView replay tests, 1 pure-C ABI check) corpus/ public submodule: 312 strategies + the 1-minute feed and derived 15m bars benchmarks/ three-way comparison harness, throughput package, results/ scripts/ run_corpus.sh, verify_corpus.py, run_strategy.py, regen_corpus_cpp.sh, coverage.sh diff --git a/docs/pages/metrics.md b/docs/pages/metrics.md index 4656758..e3c5897 100644 --- a/docs/pages/metrics.md +++ b/docs/pages/metrics.md @@ -77,7 +77,7 @@ truncated curve and metrics over the truncated prefix. | Surface | Validated against | Result | | --- | --- | --- | | Trade statistics (counts, PF, percent bases, averages, largest-%, bars) | Real TradingView Strategy Tester export (`composite-4emarsi-integration-01`, 336 trades, All/Long/Short panels) | Match within TV 2-dp rounding; three TV conventions arbitrated and adopted (net return-on-cost `pnl_pct`, independent largest-%, inclusive bar counts) | -| Commission + slippage economics | Second TV export, same strategy: commission 0.1 % percent + slippage 2 ticks via `strategy_set_override` (`validation-adhoc/.../inputs.json`) | All 672 fill prices bit-exact (slippage rules pinned: market/stop fills, directional, mintick-composed); commission formula `rate·(entry+exit)·qty·pointvalue` exact — residual per-trade deltas fully explained by TV's account-currency conversion (USDT→USD at previous-UTC-day close; 335/336 trades reproduced to the cent). Extended by a third TV export (`bracket-exit-tp-sl-fixed-01`, BINANCE:ETHUSDT.P): 396/396 trades bit-exact, pinning the limit-fill rules — limit fills are NOT slipped, off-tick limits snap one tick favorably (limit-or-better), gapped limits fill at the raw open, and stop fills confirmed slipped | +| Commission + slippage economics | Second TV export, same strategy: commission 0.1 % percent + slippage 2 ticks via `strategy_set_override` ([historical inputs](https://github.com/pineforge-4pass/pineforge-engine/blob/a03ac6d3fb42df5af1db9e39727daf450b2fb71f/validation-adhoc/4emarsi-commission-slippage-ethusdt/inputs.json)) | All 672 fill prices bit-exact (slippage rules pinned: market/stop fills, directional, mintick-composed); commission formula `rate·(entry+exit)·qty·pointvalue` exact — residual per-trade deltas fully explained by TV's account-currency conversion (USDT→USD at previous-UTC-day close; 335/336 trades reproduced to the cent). Extended by a third TV export (`bracket-exit-tp-sl-fixed-01`, BINANCE:ETHUSDT.P): 396/396 trades bit-exact, pinning the limit-fill rules — limit fills are NOT slipped, off-tick limits snap one tick favorably (limit-or-better), gapped limits fill at the raw open, and stop fills confirmed slipped | | TV risk panel (Sharpe, Sortino, drawdown/run-up rows, CAGR) | TV xlsx export (Performance + Risk-adjusted performance sheets) | Every panel value reproduced from the engine curve once TV's conventions are applied — see the definition-delta table below | | Equity statistics (max DD ±%, Sharpe/Sortino both variants, CAGR, Calmar, recovery) | quantstats 0.0.81 + empyrical-reloaded 0.5.12 (`scripts/crossvalidate_metrics.py --all`) | All 246 corpus strategies ran, 0 skipped, 0 mismatches; worst engine-convention \|rel Δ\| = 1.886e-11 (`pyramid-cash-fractional-commission-01`, sharpe/sortino_bar vs empyrical); 3 degenerate NaN fields (sharpe_tv, zero monthly variance) agree on degeneracy across engine/numpy/empyrical/quantstats; known library-convention deltas labelled in single-strategy mode | | Closed-form unit oracles | `tests/test_metrics.cpp` (e.g. monthly Sharpe 19/20, Sortino 114/61 exact rationals) | Bit-level | @@ -98,9 +98,8 @@ exactly from the engine curve: | All currency rows | Converted to **account currency** at previous-UTC-day close of the quote-currency pair | engine reports symbol currency (USDT here) | TV-only fields not computed by the engine: outliers, run-up/drawdown -durations, intrabar excursion variants, account-size/margin rows. The -reverse-engineering scripts live in -`validation-adhoc/4emarsi-commission-slippage-ethusdt/`. +durations, intrabar excursion variants, account-size/margin rows. The retired research scripts and overrides are preserved in +[Git history](https://github.com/pineforge-4pass/pineforge-engine/tree/a03ac6d3fb42df5af1db9e39727daf450b2fb71f/validation-adhoc/4emarsi-commission-slippage-ethusdt). ## Consuming from Python diff --git a/tests/test_affordability_fx.cpp b/tests/test_affordability_fx.cpp index bb6cb34..2792213 100644 --- a/tests/test_affordability_fx.cpp +++ b/tests/test_affordability_fx.cpp @@ -494,7 +494,7 @@ int main() { // E2. TradingView converts a realized trade's complete net symbol-currency // PnL at the EXIT bar's daily rate, including both percent-commission legs - // (validation-adhoc/4emarsi.../tv_commission_crack.py: 335/336 exact). + // (335/336 exact; archived investigation linked in docs/pages/metrics.md). // gross 50*2 - entry fee 400*10%*2 - exit fee 450*10%*2 = -70. { std::vector bars = {mk_bar(1000, 400.0), mk_bar(2000, 450.0)}; diff --git a/validation-adhoc/4emarsi-commission-slippage-ethusdt/inputs.json b/validation-adhoc/4emarsi-commission-slippage-ethusdt/inputs.json deleted file mode 100644 index e2bb429..0000000 --- a/validation-adhoc/4emarsi-commission-slippage-ethusdt/inputs.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "_comment": "Ad-hoc TV-parity probe: commission + slippage arbitration on BINANCE:ETHUSDT.P 15m (2026-06-12). Run against corpus/validation/composite-4emarsi-integration-01 with --inputs-json. TV chart properties at export time: commission 0.1 %, slippage 2 ticks; syminfo passed at runtime below.", - "strategy_overrides": { - "commission_type": "percent", - "commission_value": 0.1, - "slippage": 2 - }, - "runtime_overrides": { - "timezone": "UTC", - "session": "24x7", - "mintick": 0.01, - "pointvalue": 1.0 - } -} diff --git a/validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_commission_crack.py b/validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_commission_crack.py deleted file mode 100644 index ec5bfbc..0000000 --- a/validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_commission_crack.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 -""" -tv_commission_crack.py — Determine TradingView's exact percent-commission rule from data. - -VERDICT (spoiler): - TV's commission formula is EXACTLY the engine's formula, in symbol currency (USDT): - commission_USDT = 0.001 * (entry_fill_price + exit_fill_price) * qty - The 46 "divergent" trades were never a commission discrepancy. They are account- - currency conversion: the symbol is BINANCE:ETHUSDT.P (quote = USDT) but the TV - account currency is USD. TV converts each trade's net PnL (and the commission - stats) from USDT to USD using the USDTUSD daily close of the PREVIOUS UTC DAY - relative to the trade's exit: - NetPnL_USD = round( USDTUSD_close(utc_day(exit) - 1 day) * (gross - commission_USDT), 2 ) - Validated with Coinbase USDT-USD daily closes as a proxy for TV's rate source: - 335/336 trades match the displayed 2dp PnL EXACTLY; 1 trade (#261, |pnl|~2.08) - is off by one cent because TV's own USDTUSD close on 2026-02-07 was <=0.999335 - vs Coinbase's 0.99937 (0.4bp source difference). All four summary aggregates - (net profit -2006.50, commission paid 2035.71, gross profit 2830.30, - gross loss 4836.80) reproduce exactly under this model. - -Inputs: - /tmp/eng_commslip.json engine run (fills bit-exact vs TV) - ~/Downloads/PF_4emaINT_BINANCE_ETHUSDT.P_2026-06-12_*.csv TV list-of-trades export - /tmp/usdtusd_daily_close.json cached Coinbase USDT-USD daily - closes (auto-fetched if missing) -""" -import csv -import datetime -import glob -import json -import os -import sys - -UTC = datetime.timezone.utc -ENGINE_JSON = "/tmp/eng_commslip.json" -TV_CSV_GLOB = os.path.expanduser( - "~/Downloads/PF_4emaINT_BINANCE_ETHUSDT.P_2026-06-12_*.csv") -RATES_CACHE = "/tmp/usdtusd_daily_close.json" -COMM_RATE = 0.001 # 0.1 % - - -# ----------------------------------------------------------------------------- data -def load_engine(): - d = json.load(open(ENGINE_JSON)) - # [entry_ms, exit_ms, entry_px, exit_px, pnl, commission, is_long] - return d["trades"], d["all"] - - -def load_tv(): - paths = sorted(glob.glob(TV_CSV_GLOB)) - if not paths: - sys.exit(f"TV csv not found: {TV_CSV_GLOB}") - tv = {} - with open(paths[0], encoding="utf-8-sig") as f: - for row in csv.DictReader(f): - n = int(row["Trade number"]) - side = "exit" if row["Type"].startswith("Exit") else "entry" - tv.setdefault(n, {})[side] = row - return tv - - -def load_rates(): - """Daily USDTUSD closes keyed 'YYYY-MM-DD' (UTC). Coinbase = proxy for TV's source.""" - if os.path.exists(RATES_CACHE): - return json.load(open(RATES_CACHE)) - import time - import urllib.request - out = {} - for s, e in [("2025-03-20", "2025-10-01"), - ("2025-10-01", "2026-04-15"), - ("2026-04-10", "2026-05-05")]: - url = ("https://api.exchange.coinbase.com/products/USDT-USD/candles" - f"?granularity=86400&start={s}T00:00:00Z&end={e}T00:00:00Z") - req = urllib.request.Request(url, headers={"User-Agent": "curl/8"}) - for t, lo, hi, o, c, v in json.load(urllib.request.urlopen(req, timeout=20)): - out[datetime.datetime.fromtimestamp(t, UTC).strftime("%Y-%m-%d")] = c - time.sleep(0.4) - json.dump(out, open(RATES_CACHE, "w")) - return out - - -def utc_day(ms, days_back=0): - return (datetime.datetime.fromtimestamp(ms / 1000, UTC) - - datetime.timedelta(days=days_back)).strftime("%Y-%m-%d") - - -def round2(x): - """Round-half-away-from-zero to 2dp (TV display rounding).""" - return (1 if x >= 0 else -1) * round(abs(x) + 1e-12, 2) - - -# ------------------------------------------------------------------- step 1: implied -def implied_commissions(trades, tv): - """implied = gross_USDT - displayed_NetPnL (the original, conversion-blind view).""" - out = [] - for i, (e_ms, x_ms, e, x, pnl, comm, is_long) in enumerate(trades): - n = i + 1 - assert abs(float(tv[n]["entry"]["Price USDT"]) - e) < 1e-9 - assert abs(float(tv[n]["exit"]["Price USDT"]) - x) < 1e-9 - gross = (x - e) if is_long else (e - x) - out.append(gross - float(tv[n]["exit"]["Net PnL USD"])) - return out - - -# ------------------------------------------------------------------- step 2: models -def run(): - trades, eng_all = load_engine() - tv = load_tv() - rates = load_rates() - N = len(trades) - assert N == 336 - - imp = implied_commissions(trades, tv) - eng = [t[5] for t in trades] - diffs = [abs(a - b) for a, b in zip(imp, eng)] - print("== Step 1: implied-commission dataset (conversion-blind) ==") - print(f" engine formula 0.001*(entry+exit): " - f"{sum(d <= 0.006 for d in diffs)}/336 within +-0.006, " - f"{sum(d > 0.02 for d in diffs)} decisive divergences >0.02 " - f"(worst {max(diffs):.4f})") - print(" -> no notional-substitution hypothesis closed the gap; the residual") - print(" r_n = TVpnl/ENGpnl turned out to be a smooth per-day multiplier") - print(" (same-day trades share it; |dev| up to 17.7bp on 2025-10-11).") - - print("\n== Step 2: account-currency conversion model ==") - print(" NetPnL_USD = round( rate * (gross_USDT - 0.001*(entry+exit)), 2 )") - print(" rate = USDTUSD daily close of utc_day(exit_fill) - 1 (Coinbase proxy)\n") - - exact, cent, fails = 0, [], [] - net = comm_total = gp = gl = 0.0 - for i, (e_ms, x_ms, e, x, pnl, comm, is_long) in enumerate(trades): - n = i + 1 - r = rates[utc_day(x_ms, 1)] - pred = round2(r * pnl) - tvp = float(tv[n]["exit"]["Net PnL USD"]) - if abs(pred - tvp) < 1e-9: - exact += 1 - elif abs(pred - tvp) <= 0.011: - cent.append(n) - else: - fails.append(n) - net += r * pnl - comm_total += r * comm - gp += r * pnl if pnl > 0 else 0.0 - gl -= r * pnl if pnl < 0 else 0.0 - - print(f" per-trade: {exact}/336 EXACT 2dp match, " - f"{len(cent)} off by one cent {cent}, {len(fails)} worse {fails}") - print(f" aggregates (model -> TV xlsx):") - print(f" net profit {net:10.2f} -> -2006.50 (engine USDT -2006.08)") - print(f" commission paid {comm_total:10.2f} -> 2035.71 (engine USDT 2035.63)") - print(f" gross profit {gp:10.2f} -> 2830.30") - print(f" gross loss {gl:10.2f} -> 4836.80") - - print("\n residual detail:") - for n in cent + fails: - e_ms, x_ms, e, x, pnl, comm, is_long = trades[n - 1] - r = rates[utc_day(x_ms, 1)] - tvp = float(tv[n]["exit"]["Net PnL USD"]) - lo, hi = sorted(((tvp - 0.005) / pnl, (tvp + 0.005) / pnl)) - gap = (r - hi) if r > hi else (lo - r) if r < lo else 0.0 - print(f" #{n}: engpnl={pnl:.5f} coinbase_rate={r} pred={round2(r*pnl)} " - f"tv={tvp}; TV's rate must lie in [{lo:.6f},{hi:.6f}] " - f"-> {gap*1e4:.2f}bp source mismatch") - - # sanity: the old "decisive 46" all land exactly under the model - dec = [i + 1 for i in range(N) if diffs[i] > 0.02] - dec_ok = sum( - abs(round2(rates[utc_day(trades[n-1][1], 1)] * trades[n-1][4]) - - float(tv[n]["exit"]["Net PnL USD"])) < 1e-9 for n in dec) - print(f"\n former 46 decisive trades under conversion model: {dec_ok}/{len(dec)} exact") - print(" (incl. #160: 61.7804*1.00181 -> 61.89 displayed; " - "#54: 61.9348*1.00058 -> 61.97 displayed)") - - print("\n== Conclusion ==") - print(" TV percent commission = 0.001*(entry_fill + exit_fill)*qty in SYMBOL currency.") - print(" Engine commission formula is ALREADY EXACT. The divergence is TV's") - print(" USDT->USD account-currency conversion at previous-UTC-day USDTUSD close.") - return 0 - - -if __name__ == "__main__": - sys.exit(run()) diff --git a/validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_equity_crack.py b/validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_equity_crack.py deleted file mode 100644 index 10af9f3..0000000 --- a/validation-adhoc/4emarsi-commission-slippage-ethusdt/tv_equity_crack.py +++ /dev/null @@ -1,358 +0,0 @@ -#!/usr/bin/env python3 -""" -tv_equity_crack.py — Crack TradingView's equity-panel conventions from data. - -Strategy composite-4emarsi-integration-01, BINANCE:ETHUSDT.P 15m, comm 0.1%, -slippage 2 ticks, qty 1, capital 1,000,000 USD (account ccy USD, symbol ccy -USDT). Reference: PF_4emaINT_BINANCE_ETHUSDT.P_2026-06-12_b9188.xlsx. - -VERDICT (all four targets reproduced, see run output): - - 1. SHARPE -13.787 - = (mean(r) - 0.02/12) / pop_stddev(r), NOT annualized, where r are - monthly simple returns of the month-end REALIZED equity in USD - (initial capital + cumulative closed-trade net PnL converted USDT->USD - at previous-UTC-day USDTUSD close), months bucketed in UTC, baseline - 1,000,000. n=14 (Apr 2025 .. May 2026). Open profit is EXCLUDED from - month-end equity. Reproduced: -13.7873. - Engine reports -13.5932 (sample stddev, NY-tz months, USDT marked - equity) — three convention deltas: ddof, month tz, realized-vs-marked. - - 2. MAX/AVG RUN-UP & DRAWDOWN (close-to-close) 39.20 / 29.48 / 2068.23, - durations 9 days / 374 days - "Close-to-close" = TRADE-close-to-TRADE-close: the equity series is - realized USD equity sampled ONLY at trade exits (first point = first - trade's close). TV splits that polyline into alternating phases at its - GLOBAL maximum and GLOBAL minimum (3 phases here): - run-up #1: series start -> global max = 19.7647 (12.86 d) - drawdown : global max -> global min = 2068.2365 (374.55 d) - run-up #2: global min -> series end = 39.1959 (5.77 d) - Phase value = endpoint-to-endpoint net change; max run-up = 39.20, - avg run-up = (19.76+39.20)/2 = 29.48, avg duration = floor(9.32) = 9 d, - avg drawdown = max drawdown = 2068.23, duration floor(374.55) = 374 d. - (Local dips inside a phase are NOT separate phases — that is why avg - drawdown == max drawdown.) Classic cummax drawdown coincides here - (2068.23) but classic cummin run-up would give 260.22 — TV's 39.20 is - only explained by the global-extreme phase rule. - - 3. MAX DRAWDOWN (intrabar) 2072.41 / MAX RUN-UP (intrabar) 285.65 - TV keeps two curves: - settled(t): realized USD equity steps + an ENTRY-COMMISSION dip at - each entry fill (equity drops by 0.001*entry_px the - moment a position opens; no mark-to-market otherwise). - excursion events per trade n (USD, converted at exit-day rate): - hi_n = cum_{n-1} + (gross MFE - entry_comm) - lo_n = cum_{n-1} + (-gross MAE - entry_comm) - (== TV's per-trade Favorable/Adverse excursion columns) - Max drawdown (intrabar) = max_n [ runmax(settled before n) - lo_n ] - Max run-up (intrabar) = max_n [ hi_n - runmin(settled before n) ] - i.e. the *measured* extreme is intrabar, the *reference* extreme is the - settled curve. Reproduced exactly: 2072.41 (peak = realized +22.5366 on - 2025-04-16, trough = lo of trade 331 on 2026-04-26) and 285.65 (trough - = equity right after trade 205's entry fill on 2025-12-09, cum204 - - 3.12 entry commission = -1664.05; peak = hi of trade 207 = -1378.40). - Plain runmax/runmin over the full intrabar envelope gives 2076-2077 and - 292-303 — wrong; the settled-reference rule is decisive. - - 4. CAGR -0.18% - (final/initial)^(365 / D) - 1 with D = the BACKTESTING-RANGE span - (Mar 31 2025 20:00 -> May 1 2026 20:00 display tz = 396.0 days), on the - USD-converted net. Gives -0.1850% -> -0.18; long -0.0776% -> -0.08, - short -0.1073% -> -0.11 (all three displayed digits match only with - D=396/365-day year). Engine's -0.19% uses the traded span (393.75 d). - -Inputs: - /tmp/eng_commslip.json engine trade list (entry/exit ms, fill px, pnl, - comm, is_long) — fills bit-exact vs TV. Regenerate - with scripts/run_strategy.py if missing (see - README block at bottom of this docstring). - corpus/data/ohlcv_ETH-USDT-USDT_15m_warmup6m.csv (repo) for excursions - /tmp/usdtusd_daily_close.json Coinbase USDTUSD daily closes (auto-fetch) - ~/Downloads/PF_4emaINT_..._b9188.xlsx (optional) to cross-check targets -""" -import csv -import datetime -import json -import math -import os -import sys - -UTC = datetime.timezone.utc -REPO = "/Users/haoliangwen/code/pineforge-engine" -ENGINE_JSON = "/tmp/eng_commslip.json" -OHLCV_CSV = os.path.join(REPO, "corpus/data/ohlcv_ETH-USDT-USDT_15m_warmup6m.csv") -RATES_CACHE = "/tmp/usdtusd_daily_close.json" -XLSX = os.path.expanduser( - "~/Downloads/PF_4emaINT_BINANCE_ETHUSDT.P_2026-06-12_b9188.xlsx") - -INITIAL = 1_000_000.0 -COMM_RATE = 0.001 -RF_MONTHLY = 0.02 / 12 -BACKTEST_DAYS = 396.0 # Mar 31 2025 20:00 -> May 1 2026 20:00 (display tz) - -TARGETS = { - "sharpe": -13.787, "sortino": -0.997, - "dd_c2c": 2068.23, "dd_intra": 2072.41, - "ru_c2c": 39.20, "ru_intra": 285.65, - "ru_avg": 29.48, "dd_avg": 2068.23, - "ru_dur_days": 9, "dd_dur_days": 374, - "cagr_pct": -0.18, -} - - -# --------------------------------------------------------------------- inputs -def load_rates(): - if os.path.exists(RATES_CACHE): - return json.load(open(RATES_CACHE)) - import time - import urllib.request - out = {} - for s, e in [("2025-03-20", "2025-10-01"), ("2025-10-01", "2026-04-15"), - ("2026-04-10", "2026-05-05")]: - url = ("https://api.exchange.coinbase.com/products/USDT-USD/candles" - f"?granularity=86400&start={s}T00:00:00Z&end={e}T00:00:00Z") - req = urllib.request.Request(url, headers={"User-Agent": "curl/8"}) - for t, lo, hi, o, c, v in json.load(urllib.request.urlopen(req, timeout=20)): - out[datetime.datetime.fromtimestamp(t, UTC).strftime("%Y-%m-%d")] = c - time.sleep(0.4) - json.dump(out, open(RATES_CACHE, "w")) - return out - - -RATES = load_rates() - - -def rate_prev_utc_day(ms): - """TV converts symbol-ccy amounts at the USDTUSD close of the PREVIOUS - UTC day (Coinbase daily closes as proxy for TV's source; 335/336 trades - match the displayed 2dp PnL exactly).""" - d = (datetime.datetime.fromtimestamp(ms / 1000, UTC) - - datetime.timedelta(days=1)).strftime("%Y-%m-%d") - return RATES[d] - - -def load_trades(): - # [entry_ms, exit_ms, entry_px, exit_px, pnl_net_usdt, commission, is_long] - tr = json.load(open(ENGINE_JSON))["trades"] - tr.sort(key=lambda t: t[1]) - assert len(tr) == 336 - return tr - - -def load_bars(): - out = {} - with open(OHLCV_CSV) as f: - for r in csv.DictReader(f): - out[int(r["timestamp"])] = (float(r["open"]), float(r["high"]), - float(r["low"]), float(r["close"])) - return out - - -def gross_excursions(trade, bars): - """Gross MFE/MAE in symbol ccy from entry fill to exit fill (exit bar - contributes only its open — position closes at the exit-bar open).""" - e_ms, x_ms, e_px, x_px, pnl, comm, is_long = trade - dr = 1 if is_long else -1 - mfe = mae = 0.0 - for b in sorted(t for t in bars if e_ms <= t <= x_ms): - o, h, l, c = bars[b] - if b == x_ms: - mfe = max(mfe, dr * (o - e_px)) - mae = max(mae, -dr * (o - e_px)) - break - mfe = max(mfe, (h - e_px) if dr > 0 else (e_px - l)) - mae = max(mae, (e_px - l) if dr > 0 else (h - e_px)) - return mfe, mae - - -def fmt_ms(ms): - return datetime.datetime.fromtimestamp(ms / 1000, UTC).strftime("%Y-%m-%d %H:%M") - - -# ------------------------------------------------------------------- sections -def section_sharpe(tr): - print("== 1. Sharpe / Sortino (monthly, UTC, realized USD equity) ==") - cum = 0.0 - month_end = {} # (y, m) -> realized USD equity - for t in tr: - cum += rate_prev_utc_day(t[1]) * t[4] - d = datetime.datetime.fromtimestamp(t[1] / 1000, UTC) - month_end[(d.year, d.month)] = INITIAL + cum - eqs = [month_end[k] for k in sorted(month_end)] - rets, prev = [], INITIAL - for e in eqs: - rets.append(e / prev - 1.0) - prev = e - n = len(rets) - mean = sum(rets) / n - sd_pop = math.sqrt(sum((r - mean) ** 2 for r in rets) / n) - sd_smp = math.sqrt(sum((r - mean) ** 2 for r in rets) / (n - 1)) - dd_pop = math.sqrt(sum(min(0.0, r - RF_MONTHLY) ** 2 for r in rets) / n) - sharpe = (mean - RF_MONTHLY) / sd_pop - sortino = (mean - RF_MONTHLY) / dd_pop - print(f" n={n} monthly returns (UTC buckets, baseline 1,000,000)") - print(f" Sharpe (population stddev, not annualized) = {sharpe:9.4f} TV {TARGETS['sharpe']}") - print(f" Sharpe (sample stddev, for reference) = {(mean-RF_MONTHLY)/sd_smp:9.4f} <- engine-style ddof=1") - print(f" Sortino (population downside vs rf) = {sortino:9.4f} TV {TARGETS['sortino']}") - ok = abs(sharpe - TARGETS["sharpe"]) <= 0.01 - print(f" -> Sharpe within +-0.01: {'YES' if ok else 'NO'}") - return sharpe, sortino - - -def section_c2c(tr): - print("\n== 2. Close-to-close run-up / drawdown (trade-close USD equity, global-extreme phases) ==") - cum = [0.0] - for t in tr: - cum.append(cum[-1] + rate_prev_utc_day(t[1]) * t[4]) - ex_ms = [t[1] for t in tr] - N = len(tr) - gmax = max(range(1, N + 1), key=lambda i: cum[i]) - gmin = min(range(1, N + 1), key=lambda i: cum[i]) - assert gmax < gmin, "phase logic below assumes peak-before-trough shape" - ru1, dd = cum[gmax] - cum[1], cum[gmax] - cum[gmin] - ru2 = cum[N] - cum[gmin] - d_ru1 = (ex_ms[gmax - 1] - ex_ms[0]) / 86400000 - d_dd = (ex_ms[gmin - 1] - ex_ms[gmax - 1]) / 86400000 - d_ru2 = (ex_ms[N - 1] - ex_ms[gmin - 1]) / 86400000 - print(f" series: realized USD equity at the 336 trade exits; phases split at") - print(f" global max trade #{gmax} ({fmt_ms(ex_ms[gmax-1])}, {cum[gmax]:+.4f}) and") - print(f" global min trade #{gmin} ({fmt_ms(ex_ms[gmin-1])}, {cum[gmin]:+.4f})") - print(f" run-up #1 {ru1:9.4f} ({d_ru1:6.2f} d) start -> global max") - print(f" drawdown {dd:9.4f} ({d_dd:6.2f} d) global max -> global min TV {TARGETS['dd_c2c']}") - print(f" run-up #2 {ru2:9.4f} ({d_ru2:6.2f} d) global min -> end") - print(f" Max run-up = {max(ru1, ru2):8.2f} TV {TARGETS['ru_c2c']}") - print(f" Avg run-up = {(ru1+ru2)/2:8.2f} TV {TARGETS['ru_avg']}") - print(f" Avg ru dur = floor({(d_ru1+d_ru2)/2:.2f}) = {int((d_ru1+d_ru2)/2)} days TV {TARGETS['ru_dur_days']} days") - print(f" Max/Avg dd = {dd:8.2f} TV {TARGETS['dd_avg']}") - print(f" Avg dd dur = floor({d_dd:.2f}) = {int(d_dd)} days TV {TARGETS['dd_dur_days']} days") - # classic definitions, for the delta table - rm, rmin, cdd, cru = -1e18, 1e18, 0.0, 0.0 - for e in cum[1:]: - rm, rmin = max(rm, e), min(rmin, e) - cdd, cru = max(cdd, rm - e), max(cru, e - rmin) - print(f" [classic cummax dd = {cdd:.2f} (coincides); classic cummin run-up = {cru:.2f} != 39.20 -> phase rule is decisive]") - return cum - - -def section_intrabar(tr, cum, bars): - print("\n== 3. Intrabar run-up / drawdown (settled reference vs excursion events) ==") - # per-trade excursion events in USD, netted of entry commission (TV's - # Favorable/Adverse excursion convention, verified vs xlsx columns) - dd = ru = 0.0 - dd_info = ru_info = None - runmax_settled = 0.0 # settled = realized cums + entry-comm dips - runmin_settled = 0.0 - rmin_at = ("initial", 0) - for n, t in enumerate(tr, 1): - r_exit = rate_prev_utc_day(t[1]) - comm_entry = COMM_RATE * t[2] * rate_prev_utc_day(t[0]) - entry_pt = cum[n - 1] - comm_entry - if entry_pt < runmin_settled: - runmin_settled, rmin_at = entry_pt, ("entry", n) - mfe, mae = gross_excursions(t, bars) - hi = cum[n - 1] + (mfe - COMM_RATE * t[2]) * r_exit - lo = cum[n - 1] + (-mae - COMM_RATE * t[2]) * r_exit - if runmax_settled - lo > dd: - dd, dd_info = runmax_settled - lo, (n, runmax_settled, lo) - if hi - runmin_settled > ru: - ru, ru_info = hi - runmin_settled, (n, hi, runmin_settled, rmin_at) - if cum[n] > runmax_settled: - runmax_settled = cum[n] - if cum[n] < runmin_settled: - runmin_settled, rmin_at = cum[n], ("close", n) - n, pk, lo = dd_info - print(f" Max drawdown (intrabar) = {dd:9.4f} TV {TARGETS['dd_intra']}") - print(f" peak = settled {pk:+.4f}, trough = adverse excursion of trade #{n} ({fmt_ms(tr[n-1][1])})") - n, hi, ref, at = ru_info - print(f" Max run-up (intrabar) = {ru:9.4f} TV {TARGETS['ru_intra']}") - print(f" peak = favorable excursion of trade #{n} ({fmt_ms(tr[n-1][1])}, {hi:+.2f}),") - print(f" trough = settled ref {ref:+.2f} = equity at {at[0]} of trade #{at[1]} (entry-commission dip)") - print(" [naive full-envelope runmax/runmin give 2076-2077 / 292-304 -> the settled-reference rule is decisive]") - return dd, ru - - -def section_cagr(tr): - print("\n== 4. Annualized return (CAGR) ==") - for name, sel, tv in [("all", lambda t: True, -0.18), - ("long", lambda t: t[6], -0.08), - ("short", lambda t: not t[6], -0.11)]: - net = sum(rate_prev_utc_day(t[1]) * t[4] for t in tr if sel(t)) - cagr = ((INITIAL + net) / INITIAL) ** (365.0 / BACKTEST_DAYS) - 1 - print(f" {name:5s}: net {net:9.2f} USD -> ({(INITIAL+net)/INITIAL:.8f})^(365/396) - 1 " - f"= {cagr*100:8.4f}% TV {tv}") - print(" [engine -0.19% uses the traded span 393.75 d; TV uses the configured") - print(" backtesting range = 396.0 d with a 365-day year]") - - -def section_xlsx_check(tr, bars): - if not os.path.exists(XLSX): - print("\n(xlsx not found; skipping reference cross-check)") - return - try: - import openpyxl - except ImportError: - print("\n(openpyxl missing; skipping reference cross-check)") - return - print("\n== 5. Cross-check vs TV's own per-trade columns (xlsx) ==") - wb = openpyxl.load_workbook(XLSX, data_only=True) - ex = {r[0]: r for r in list(wb["Trades"].iter_rows(values_only=True))[1:] - if r[1].startswith("Exit")} - bad = 0 - for n, t in enumerate(tr, 1): - mfe, mae = gross_excursions(t, bars) - r = rate_prev_utc_day(t[1]) - pf = max(0.0, (mfe - COMM_RATE * t[2])) * r - pa = -(mae + COMM_RATE * t[2]) * r - if abs(pf - ex[n][9]) > 0.011 or abs(pa - ex[n][11]) > 0.011: - bad += 1 - print(f" excursion model (gross -/+ entry comm, exit-day rate): " - f"{336-bad}/336 trades match TV's FE/AE columns within 1 cent") - # exact-from-TV-columns recomputation of the two intrabar metrics - cumx = [0.0] + [ex[n][13] for n in range(1, 337)] - rm = rmin = dd = ru = 0.0 - for n in range(1, 337): - entry_pt = cumx[n - 1] - COMM_RATE * tr[n - 1][2] - rmin = min(rmin, entry_pt) - hi, lo = cumx[n - 1] + ex[n][9], cumx[n - 1] + ex[n][11] - dd = max(dd, rm - lo) - ru = max(ru, hi - rmin) - rm, rmin = max(rm, cumx[n]), min(rmin, cumx[n]) - print(f" same formulas on TV's own columns: dd {dd:.4f} (TV 2072.41), ru {ru:.4f} (TV 285.65)") - - -def delta_table(): - print(""" -== Definition delta table (TV equity panel vs engine) == - metric TV convention (cracked) engine today - ------------------------- ------------------------------------------------------- ------------------------------------- - Sharpe -13.787 monthly simple returns of month-end REALIZED equity in sample stddev, NY-tz months, USDT - USD, UTC month buckets, rf 2%/12, POPULATION stddev, marked equity -> -13.5932 - not annualized -> -13.7873 - Sortino -0.997 same series, population downside dev vs rf -> -0.9974 matches (-0.9975) - Max dd (c2c) 2068.23 trade-close USD equity, global-max -> global-min phase per-bar USDT cummax dd 2071.01; - (== classic cummax dd on that series) -> 2068.24* daily-sampled 2060.63 - Avg dd / 374 days one phase: value 2068.23, floor(374.55 d) n/a - Max run-up (c2c) 39.20 phase global-min -> series end = 39.1959 cummin run-up 260+ (different def) - Avg run-up 29.48 / 9 d mean of the 2 run-up phases (19.76, 39.20); floor(9.32) n/a - Max dd (intrabar) 2072.41 runmax(settled) - per-trade adverse excursion event; n/a (engine has per-trade MAE) - settled = realized + entry-commission dips -> 2072.41 - Max run-up (intra) 285.65 per-trade favorable excursion event - runmin(settled) n/a (engine has per-trade MFE) - -> 285.65 - CAGR -0.18% (1+ret)^(365/396) - 1, D = configured backtest range -0.19% (traded span 393.75 d) - * 0.01 residuals are USDTUSD rate-source noise (Coinbase proxy vs TV's feed), - same one-cent class as the known trade #261 discrepancy.""") - - -def main(): - tr = load_trades() - bars = load_bars() - section_sharpe(tr) - cum = section_c2c(tr) - section_intrabar(tr, cum, bars) - section_cagr(tr) - section_xlsx_check(tr, bars) - delta_table() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/validation-adhoc/4emarsi-commission-slippage-ethusdt/usdtusd_daily_close.json b/validation-adhoc/4emarsi-commission-slippage-ethusdt/usdtusd_daily_close.json deleted file mode 100644 index efa8896..0000000 --- a/validation-adhoc/4emarsi-commission-slippage-ethusdt/usdtusd_daily_close.json +++ /dev/null @@ -1 +0,0 @@ -{"2025-10-01": 1.00068, "2025-09-30": 1.00011, "2025-09-29": 1.00061, "2025-09-28": 1.00053, "2025-09-27": 1.00048, "2025-09-26": 1.00066, "2025-09-25": 1.00035, "2025-09-24": 1.00038, "2025-09-23": 1.00034, "2025-09-22": 1.00081, "2025-09-21": 1.00058, "2025-09-20": 1.00055, "2025-09-19": 1.00061, "2025-09-18": 1.00041, "2025-09-17": 1.00031, "2025-09-16": 1.00039, "2025-09-15": 1.00033, "2025-09-14": 1.00046, "2025-09-13": 1.00044, "2025-09-12": 1.0007, "2025-09-11": 1.00029, "2025-09-10": 1.00015, "2025-09-09": 1.00011, "2025-09-08": 1.00017, "2025-09-07": 1.00008, "2025-09-06": 1.00036, "2025-09-05": 1.00005, "2025-09-04": 1.0002, "2025-09-03": 1.00046, "2025-09-02": 1.00003, "2025-09-01": 1.00013, "2025-08-31": 1.00005, "2025-08-30": 1.0001, "2025-08-29": 1.00006, "2025-08-28": 1.00003, "2025-08-27": 1, "2025-08-26": 1.00011, "2025-08-25": 1.00011, "2025-08-24": 0.99987, "2025-08-23": 0.99962, "2025-08-22": 0.99963, "2025-08-21": 0.99974, "2025-08-20": 1.00007, "2025-08-19": 0.99988, "2025-08-18": 1.00045, "2025-08-17": 1.00059, "2025-08-16": 1.0006, "2025-08-15": 1.00065, "2025-08-14": 1.00065, "2025-08-13": 1.00034, "2025-08-12": 0.99982, "2025-08-11": 1, "2025-08-10": 1.00017, "2025-08-09": 1.00025, "2025-08-08": 1.00011, "2025-08-07": 1.00026, "2025-08-06": 1.00017, "2025-08-05": 0.99988, "2025-08-04": 0.99998, "2025-08-03": 1.00006, "2025-08-02": 0.99974, "2025-08-01": 0.99957, "2025-07-31": 0.99989, "2025-07-30": 0.99996, "2025-07-29": 0.9998, "2025-07-28": 0.99994, "2025-07-27": 1.0002, "2025-07-26": 1.0003, "2025-07-25": 0.99999, "2025-07-24": 1.00048, "2025-07-23": 1.00051, "2025-07-22": 1.00044, "2025-07-21": 1.00046, "2025-07-20": 1.00043, "2025-07-19": 1.00047, "2025-07-18": 1.00069, "2025-07-17": 1.00068, "2025-07-16": 1.00048, "2025-07-15": 1, "2025-07-14": 1.00019, "2025-07-13": 1.00043, "2025-07-12": 1.00033, "2025-07-11": 1.00041, "2025-07-10": 1.00011, "2025-07-09": 1.00034, "2025-07-08": 1.0002, "2025-07-07": 1.00006, "2025-07-06": 1.00011, "2025-07-05": 1.00026, "2025-07-04": 1.00025, "2025-07-03": 1.00032, "2025-07-02": 1.00043, "2025-07-01": 1.00017, "2025-06-30": 1.00025, "2025-06-29": 1.00022, "2025-06-28": 1.00039, "2025-06-27": 1.00042, "2025-06-26": 1.0003, "2025-06-25": 1.00049, "2025-06-24": 1.0004, "2025-06-23": 1.00073, "2025-06-22": 1.00027, "2025-06-21": 1.00028, "2025-06-20": 1.0002, "2025-06-19": 1.00016, "2025-06-18": 1.00023, "2025-06-17": 1.00018, "2025-06-16": 1.00051, "2025-06-15": 1.00016, "2025-06-14": 1.00044, "2025-06-13": 1.00048, "2025-06-12": 1.00044, "2025-06-11": 1.00032, "2025-06-10": 1.00021, "2025-06-09": 1.00043, "2025-06-08": 1.00048, "2025-06-07": 1.00053, "2025-06-06": 1.0007, "2025-06-05": 1.00048, "2025-06-04": 1.00057, "2025-06-03": 1.00066, "2025-06-02": 1.00048, "2025-06-01": 1.0005, "2025-05-31": 1.0005, "2025-05-30": 1.00036, "2025-05-29": 1, "2025-05-28": 1.00029, "2025-05-27": 1.0005, "2025-05-26": 1.00037, "2025-05-25": 1.00034, "2025-05-24": 1.00035, "2025-05-23": 1.00024, "2025-05-22": 1.00026, "2025-05-21": 1.00042, "2025-05-20": 1.00035, "2025-05-19": 1.00026, "2025-05-18": 1.00043, "2025-05-17": 1.0003, "2025-05-16": 1.00031, "2025-05-15": 1.00014, "2025-05-14": 1.00013, "2025-05-13": 1.00006, "2025-05-12": 0.99995, "2025-05-11": 1.00019, "2025-05-10": 1.00007, "2025-05-09": 1.00011, "2025-05-08": 0.99997, "2025-05-07": 1.00016, "2025-05-06": 0.99993, "2025-05-05": 0.99995, "2025-05-04": 1.00009, "2025-05-03": 1.00022, "2025-05-02": 1.00036, "2025-05-01": 1.00029, "2025-04-30": 1.00009, "2025-04-29": 1.00025, "2025-04-28": 1.00022, "2025-04-27": 1.00034, "2025-04-26": 1.00029, "2025-04-25": 1.0006, "2025-04-24": 1.00038, "2025-04-23": 1.00039, "2025-04-22": 1.0004, "2025-04-21": 1.00001, "2025-04-20": 0.99997, "2025-04-19": 0.9999, "2025-04-18": 0.99989, "2025-04-17": 1, "2025-04-16": 0.99998, "2025-04-15": 0.99992, "2025-04-14": 0.99992, "2025-04-13": 0.99971, "2025-04-12": 0.99988, "2025-04-11": 0.99953, "2025-04-10": 0.99939, "2025-04-09": 0.99974, "2025-04-08": 0.99906, "2025-04-07": 0.99952, "2025-04-06": 0.99927, "2025-04-05": 0.99961, "2025-04-04": 0.99966, "2025-04-03": 0.9996, "2025-04-02": 0.9998, "2025-04-01": 1, "2025-03-31": 0.99979, "2025-03-30": 0.99992, "2025-03-29": 0.99967, "2025-03-28": 0.99952, "2025-03-27": 0.99977, "2025-03-26": 1.00009, "2025-03-25": 1.00017, "2025-03-24": 1.00019, "2025-03-23": 0.99998, "2025-03-22": 0.99984, "2025-03-21": 0.99965, "2025-03-20": 0.99941, "2026-04-15": 1.00033, "2026-04-14": 1.00046, "2026-04-13": 1.00019, "2026-04-12": 0.99991, "2026-04-11": 1.0002, "2026-04-10": 1.00032, "2026-04-09": 0.99998, "2026-04-08": 1.00009, "2026-04-07": 0.99984, "2026-04-06": 1.0001, "2026-04-05": 0.99959, "2026-04-04": 0.99987, "2026-04-03": 0.99994, "2026-04-02": 1.00015, "2026-04-01": 0.9999, "2026-03-31": 0.99909, "2026-03-30": 0.99924, "2026-03-29": 0.99932, "2026-03-28": 0.9992, "2026-03-27": 0.99942, "2026-03-26": 0.99946, "2026-03-25": 0.99975, "2026-03-24": 0.99958, "2026-03-23": 0.9999, "2026-03-22": 0.99973, "2026-03-21": 0.99986, "2026-03-20": 1.00001, "2026-03-19": 1.00009, "2026-03-18": 1.00021, "2026-03-17": 1.00035, "2026-03-16": 0.99998, "2026-03-15": 1.00028, "2026-03-14": 1.00026, "2026-03-13": 1.00029, "2026-03-12": 1, "2026-03-11": 1.00024, "2026-03-10": 0.99999, "2026-03-09": 1.00015, "2026-03-08": 1.00002, "2026-03-07": 1.00001, "2026-03-06": 0.99995, "2026-03-05": 1.00018, "2026-03-04": 1.00021, "2026-03-03": 0.99996, "2026-03-02": 0.99999, "2026-03-01": 0.99993, "2026-02-28": 1.00007, "2026-02-27": 0.99996, "2026-02-26": 0.99988, "2026-02-25": 1.00006, "2026-02-24": 1.00011, "2026-02-23": 0.99965, "2026-02-22": 0.99961, "2026-02-21": 0.9998, "2026-02-20": 0.99956, "2026-02-19": 0.99959, "2026-02-18": 0.99964, "2026-02-17": 0.9996, "2026-02-16": 0.99948, "2026-02-15": 0.99942, "2026-02-14": 0.99956, "2026-02-13": 0.99949, "2026-02-12": 0.99931, "2026-02-11": 0.99934, "2026-02-10": 0.99956, "2026-02-09": 0.99961, "2026-02-08": 0.99927, "2026-02-07": 0.99937, "2026-02-06": 0.99917, "2026-02-05": 0.99811, "2026-02-04": 0.99789, "2026-02-03": 0.99863, "2026-02-02": 0.99924, "2026-02-01": 0.9991, "2026-01-31": 0.99883, "2026-01-30": 0.99848, "2026-01-29": 0.99843, "2026-01-28": 0.99864, "2026-01-27": 0.99864, "2026-01-26": 0.99901, "2026-01-25": 0.99902, "2026-01-24": 0.99848, "2026-01-23": 0.99876, "2026-01-22": 0.99905, "2026-01-21": 0.99918, "2026-01-20": 0.99876, "2026-01-19": 0.99945, "2026-01-18": 0.99966, "2026-01-17": 0.99969, "2026-01-16": 0.99966, "2026-01-15": 0.99973, "2026-01-14": 1.00013, "2026-01-13": 0.99948, "2026-01-12": 0.99891, "2026-01-11": 0.99862, "2026-01-10": 0.99869, "2026-01-09": 0.99872, "2026-01-08": 0.99909, "2026-01-07": 0.99919, "2026-01-06": 0.99968, "2026-01-05": 0.99996, "2026-01-04": 0.99957, "2026-01-03": 0.99963, "2026-01-02": 0.99964, "2026-01-01": 0.99879, "2025-12-31": 0.99859, "2025-12-30": 0.99897, "2025-12-29": 0.99885, "2025-12-28": 0.9993, "2025-12-27": 0.99932, "2025-12-26": 0.99922, "2025-12-25": 0.99942, "2025-12-24": 0.99936, "2025-12-23": 0.99939, "2025-12-22": 0.99956, "2025-12-21": 0.9998, "2025-12-20": 0.99971, "2025-12-19": 0.9995, "2025-12-18": 0.99962, "2025-12-17": 0.99972, "2025-12-16": 0.99999, "2025-12-15": 1.00004, "2025-12-14": 1.00019, "2025-12-13": 1.00038, "2025-12-12": 1.00022, "2025-12-11": 1.00019, "2025-12-10": 1.00013, "2025-12-09": 1.00008, "2025-12-08": 1.00005, "2025-12-07": 1.00034, "2025-12-06": 1.00033, "2025-12-05": 1.00042, "2025-12-04": 1.00016, "2025-12-03": 1.00032, "2025-12-02": 1.00042, "2025-12-01": 1.00009, "2025-11-30": 1.00017, "2025-11-29": 1.00037, "2025-11-28": 1.00027, "2025-11-27": 0.99997, "2025-11-26": 1.00002, "2025-11-25": 0.99962, "2025-11-24": 0.99976, "2025-11-23": 0.99983, "2025-11-22": 0.9996, "2025-11-21": 0.99952, "2025-11-20": 0.99897, "2025-11-19": 0.99922, "2025-11-18": 0.99974, "2025-11-17": 0.99903, "2025-11-16": 0.99942, "2025-11-15": 0.99954, "2025-11-14": 0.99928, "2025-11-13": 0.99953, "2025-11-12": 1, "2025-11-11": 0.99974, "2025-11-10": 0.99982, "2025-11-09": 0.99994, "2025-11-08": 0.99986, "2025-11-07": 0.99947, "2025-11-06": 0.99966, "2025-11-05": 1.00003, "2025-11-04": 0.99983, "2025-11-03": 0.99995, "2025-11-02": 0.99999, "2025-11-01": 0.9997, "2025-10-31": 0.9998, "2025-10-30": 0.99998, "2025-10-29": 1.00008, "2025-10-28": 1.00019, "2025-10-27": 1.00012, "2025-10-26": 1.00005, "2025-10-25": 1.00019, "2025-10-24": 1.00024, "2025-10-23": 1.00038, "2025-10-22": 1.0001, "2025-10-21": 1.00042, "2025-10-20": 1.00041, "2025-10-19": 1.0003, "2025-10-18": 1.00036, "2025-10-17": 1.00039, "2025-10-16": 1.0002, "2025-10-15": 1.00058, "2025-10-14": 1.00057, "2025-10-13": 1.00079, "2025-10-12": 1.001, "2025-10-11": 1.00105, "2025-10-10": 1.00181, "2025-10-09": 1.00058, "2025-10-08": 1.00033, "2025-10-07": 1.00053, "2025-10-06": 1.0004, "2025-10-05": 1.00032, "2025-10-04": 1.00044, "2025-10-03": 1.00066, "2025-10-02": 1.00069, "2026-05-05": 0.99995, "2026-05-04": 0.99978, "2026-05-03": 0.99991, "2026-05-02": 1, "2026-05-01": 0.99987, "2026-04-30": 0.99953, "2026-04-29": 0.9996, "2026-04-28": 0.99983, "2026-04-27": 0.99984, "2026-04-26": 1.00036, "2026-04-25": 1.00033, "2026-04-24": 1.00035, "2026-04-23": 1.00035, "2026-04-22": 1.00041, "2026-04-21": 1.00032, "2026-04-20": 1.00046, "2026-04-19": 1.00048, "2026-04-18": 1.00046, "2026-04-17": 1.00032, "2026-04-16": 1.00035} \ No newline at end of file