diff --git a/README.md b/README.md
index 5f02276a..0cec9e5a 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
**The open-source PineScript v6 backtest engine that reproduces TradingView trade-for-trade.**
[](https://github.com/pineforge-4pass/pineforge-engine/actions)
-[](#validation-scoreboard)
+[](#validation-scoreboard)
[](#validation-scoreboard)
[](benchmarks/results/speed.md)
[](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
@@ -186,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`.
@@ -240,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 46567583..e3c58974 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/include/pineforge/engine.hpp b/include/pineforge/engine.hpp
index d8275682..046df00d 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,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 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 a04a8960..2eac08ac 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:
@@ -997,6 +998,73 @@ 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 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
+ || 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_.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 != position_open_bar_
+ || 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;
+ }
+ 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) {
+ // 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();
+ }
+ }
+}
+
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 76c6b002..061a009a 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_short_margin_before_script(bar);
struct ChartEmaNaWarmupScope {
bool previous;
explicit ChartEmaNaWarmupScope(bool enabled)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6fe391da..5790a954 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,4 +1,5 @@
set(TEST_SOURCES
+ test_short_margin_script_state
test_ringbuffer
test_timeframe
test_magnifier
diff --git a/tests/test_affordability_fx.cpp b/tests/test_affordability_fx.cpp
index bb6cb344..27922131 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/tests/test_short_margin_script_state.cpp b/tests/test_short_margin_script_state.cpp
new file mode 100644
index 00000000..6059926e
--- /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;
+}
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 e2bb4292..00000000
--- 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 ec5bfbc8..00000000
--- 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 10af9f3f..00000000
--- 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 efa88967..00000000
--- 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