Skip to content

fix: TradingView range-end accounting for open positions; pad the TV-entry gate by the feed's previous bar - #173

Closed
luisleo526 wants to merge 2 commits into
mainfrom
fix/range-end-close-and-gap-pad
Closed

fix: TradingView range-end accounting for open positions; pad the TV-entry gate by the feed's previous bar#173
luisleo526 wants to merge 2 commits into
mainfrom
fix/range-end-close-and-gap-pad

Conversation

@luisleo526

@luisleo526 luisleo526 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two commuting fixes for the daily-timeframe lanes, measured as a 2×2 on spark and as a six-lane experiment on Cloud Run.

M2 — range-end close (TradingView emulation). TradingView's deep-backtest report lists the position still open at the range's last bar as a closed trade at that bar's close. The engine exported closed trades only, so on 1D lanes where that is the only trade in the window the engine reported 0 trades ("no-trades", tier minimal) although it held the same position with the same mark-to-market P/L (spark: 7/10 to the cent). The engine now emits a report-only row for each lot still open after the final bar of the range (record_range_end_close_trades, pf_trade_t::open_at_end, ABI v3), priced at the last close on-tick, commission per the strategy's rules, no slippage; broker state, Pine accessors and the equity curve before the last bar are untouched. The harness bounds the run to the tape's last row (ohlcv_end_ms) so the row lands on TradingView's range end, not the feed's end.

M1 — TV-entry gate pad. The TV-entry emit window padded by one bar-interval in milliseconds, so a Friday signal filled on Monday fell outside the gate across a weekend. The gate now admits the feed's previous script bar by index (trading_is_active, _window_start_before_first_entry).

Measurement

  • spark 2×2 on f-1d shard 0 (64 probes): base 10 engine-0; M2 → 0 engine-0, all excellent 100%, 6 previously-ok probes improved; M1 alone neutral for that set, +2 with M2; zero match% decreases in any cell.
  • Cloud Run exp-1d-range-end-20260902 (all six 1D lanes, 1148 probes, 21 cases, 5 min): excellent+strong 74.0% → 93.6% overall — BTCUSDT-1D 77.6→93.8%, ES1-1D 79.5→96.6%, NQ1-1D 75.0→92.2%, NIFTY-1D 78.6→97.2%, NYSE-F-1D 68.4→92.8%, XAUUSD-1D 70.6→91.1%; engine-0 probes 134 → 12; 225 entrants, 1 leaver (3commas-ena-grid-bot XAUUSD-1D: engine 28 vs TV 25 — one range-end row per pyramided open lot where TV carries fewer; follow-up).
  • Full-population gate: in progress (gate-cand-1d-range-end-20260902).

Test plan

  • ctest 153/153 with -UNDEBUG (new test_range_end_close, test_trade_start_gap_pad, Python test_run_strategy_gap_pad / test_run_strategy_range_end); patches commute (tree bc9b3797)
  • Cloud Run six-lane 1D experiment above
  • gate_candidate PASS recorded

🤖 Generated with Claude Code

https://claude.ai/code/session_01BXAE7TVTKZYbmtenHDHVJo

luisleo526 and others added 2 commits September 2, 2026 21:16
…s range-end close

TradingView's deep-backtest report (the ws-report-v1 tape the campaign
grades against) does not leave the last position open. It reports it as
a closed trade whose exit leg is the range's last bar at that bar's
close, with an empty exit Signal, and counts it in closedTrades. The
canonical row is orb-lite on NYSE:F 1D: Entry short 2026-03-16 @ 11.82,
Exit 2026-04-30 @ 12.08 -- 12.08 is the last close of the range,
metrics closedTrades:1. On the f-1d spark set 8/10 engine runs held the
same position to the last bar (bars-in-market = TV Duration + 1) and
7/10 had a mark-to-market open_pl equal to TV's row to the cent: the
row is exactly the engine's open position marked at the last close.

The engine exported closed trades only (trades_ grows in
emit_close_trade alone; neither run loop flattened at end of feed), so
every such probe was one trade short against the tape and the verifier
scored the missing row as an unmatched TV trade. The browser export
writes the same row with Signal "Open", which the verifier already
recognises; the ws tape gives it no marker at all.

Operator decision (2026-09-02): the engine emulates TradingView's
range-end accounting, in the report. After the last script bar is
dispatched and its equity point recorded, record_range_end_close_trades
builds the rows a close of the open position at the last bar's close
would record -- one per pyramid slice through the same arithmetic as
every other close (build_close_trade, split out of emit_close_trade) --
into range_end_trades_, which fill_trades_section merges behind the
script's closed trades. The live position, pending orders, trades_ and
the realized sums are left as the bar loop left them: a stream's
realtime bars continue the warmup position, Pine accessors never see
the row, and the broker-mechanics tests keep inspecting the open lot.

Pricing: the row is a mark at the close, not an order the broker fills
-- the raw close rounded to the nearest tick (bar_fill_price,
finding-446; the tape's 12.08 is the on-tick close), no slippage ticks,
commission per the strategy's rules on the exit leg like any close (the
tape in hand has commission 0, so that is the operator's call, not a
measurement), dated on the script bar's label. Accounting: the report's
closed-trade count, net profit, win/loss tallies and commission paid
include the rows. Every equity point before the last is untouched; the
last point is re-marked to the flat account (open_profit 0, equity =
capital + net profit including the rows) so the report identity
equity == initial_capital + net_profit + open_pl that test_metrics pins
still holds on the last bar -- with the tape's commission 0 and an
on-tick close that point's equity VALUE is the gross mark it already
was. A first cut re-marked the point and left the scalar extremes at the
bar loop's gross fold; review showed that broke the compute_equity_stats
walk == scalar identity on every commissioned run whose last bar was the
trough or the peak (the fold had seen the gross mark, the walk the net
one), so the scalars are re-folded from the curve after the re-mark
(fold_equity_extreme, split out of update_equity_extremes): the walk and
the scalars agree again, and every earlier bar's drawdown / run-up is
unchanged. A strategy flat after the final bar is unaffected.

The measurement must END where TradingView's range ends, or the row is
booked on the wrong bar. The engine marks after the last bar it is FED,
and the ETH chart feed (eth-corpus-15 / eth-scraped-15) runs on to
2026-05-04 15:00 UTC while every ETH tape ends at 2026-05-01 00:00 UTC
(286 of 305 with an Open row there @ 2261.44 = that bar's close); the
ws-report lanes (F, AAPL, NIFTY, BTC, XAU, ES, EURUSD, NQ) end on the
tape's last bar already. run_strategy.py now bounds the feed to the
tape's latest row (_load_tv_range_end_ms over entries, exits and the
open-position row -> ohlcv_end_ms, on both runners; the docker runner
pre-trims through _trim_ohlcv_csv) whenever the TV window is in use.
It hides nothing the emit window did not already: engine trades entered
after TV's last entry were dropped regardless.

The browser export's Open row is priced at the export-time quote, not a
bar's print: all 12 sampled OANDA:EURUSD tapes end with Open rows @
1.17256 while the feed's last bar is o 1.17289 h 1.17299 l 1.17282 c
1.1729 (246 of 358 EURUSD tapes end with such a row). The engine's row
at the close can never price-match a mark. verify_corpus.py treats a
TV Open row whose engine counterpart exits on the SAME bar as count-only
(counted, covered, entry compared; exit/pnl deltas left out and the
pair reported as open_mark_pairs); an engine exit on a different bar
keeps its deltas, since TV held to the end and the engine did not. ws
tapes write the row with no Signal and never take that path.
pineforge-utils/validator/validate.py tracks this rubric and needs the
same rule.

pf_trade_t gains open_at_end (1 on the range-end row) -- ABI v3, with
the ctypes mirrors (run_strategy, docker/run_json, tutorial, benchmarks)
and the docs updated; run_strategy.py carries the flag on the report
dict and writes the CSV row as an ordinary exit with no marker, the way
the ws tape prints it. run_stream_corpus compares the trades the two
brokers made, without the range-end rows.

tests/test_range_end_close.cpp pins the row (long, short, flat, pyramid
slices, commission on both legs, rounded close without slippage,
aggregated-path label, earlier curve points untouched and the last
re-marked flat, walk == scalar extremes
with commission and the last bar as trough and as peak, re-run reset);
test_run_inputs_overrides and test_accounting_reconciliation foot the
report to the blotter plus the range-end rows;
scripts/test_run_strategy_range_end.py pins the tape bound on the ETH
shape for both runners and the harness's EXPECTED_PF_ABI to the header's
PF_ABI_VERSION (the f-1d spark pre-check of 2026-09-02 failed all 64
probes with ".so reports 3, harness expects 2" while that guard was
still 2 -- the verifier runs every probe through run_strategy.py);
scripts/test_verify_corpus_open_mark.py pins the EURUSD Open-row
pairing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BXAE7TVTKZYbmtenHDHVJo
…endar

The validator opens its emit / trade-start window one bar interval
before TradingView's first entry so the signal bar that produced that
entry can place its order, and trading_is_active pads the gate by one
more script TF in milliseconds. Both pads are calendar-blind, and the
daily lanes exposed it. The bar before a Monday is Friday, three
calendar days back: orb-lite on NYSE:F 1D fires its short on the
2026-03-13 (Friday) bar and TradingView fills it on 2026-03-16
(Monday), its first entry. The window opened on Sunday (Monday minus
86,400,000 ms), the engine's buffer reached Saturday, and Friday's
strategy.entry fell before the gate -- dropped, so the engine's tape
started from a different placement entirely. The same shape hides on
every session gap: a Friday-close signal on an intraday equity feed, a
holiday, the 17:00 ET forex open whose opening minutes the tape does
not print.

The feed knows which bar precedes the first in-window bar, so the pad
is now ONE bar, by INDEX on the feed, in ONE place. The engine resolves
the script bar immediately preceding the first script bar at/after
trade_start_time_ once per run (compute_trade_start_preceding_script_bar
-- a preview of the same aggregation the bar loop performs, same TFs,
timezone and session, so the index it finds is the bar_index_ the loop
assigns), and when that bar resolves trading_is_active admits exactly it
and every bar at/after the gate; the millisecond buffer survives only
where the index cannot resolve (a gate past the feed -- a stream's
warmup -- or before it). The validator sets the gate ON TradingView's
first entry bar (run_strategy.py _load_tv_entry_window returns
(first entry, last entry)) and no longer pads itself: the trades filter
therefore also stops keeping a fill dated one bar before TV's first
entry, where TV has none.

A first cut kept the millisecond buffer beside the index rule and moved
the validator's pad to the previous FEED bar; review showed that stacks
two pads -- with the gate on Friday the buffer reaches Thursday, and a
strategy firing on both of the two bars before TV's first entry books
an engine-only trade the >= window-start filter keeps. One pad, in the
engine, closes both holes. On a continuous feed the index rule names the
bar the old subtraction named, so the 1m-feed/15m-script gate of
validation/62 is unchanged bar for bar; a same-TF 15m feed, which the
old pair of pads let two bars through, now admits one.

tests/test_trade_start_gap_pad.cpp pins the gate: Friday admitted and
Thursday refused with the gate on Sunday or Monday; one pad under a
Friday gate (Thursday in, Wednesday out) and a mid-week gate; the
aggregated 1m->15m path across an overnight gap under TV's label and
the old 1m pad; the continuous 1m pin of test_strategy_commands_extra
unchanged; one pad on a continuous 15m feed; no gate, a gate past the
feed, a gate before it. scripts/test_run_strategy_gap_pad.py pins the
window: it opens on the first entry (not Sunday, not Friday), ends on
the last entry, honours the tape's timezone and the meta plumbing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BXAE7TVTKZYbmtenHDHVJo
@luisleo526

Copy link
Copy Markdown
Collaborator Author

Superseded by the M2-only PR: the gap-pad factor (M1) drops the first trade of the 15m window on six hard-lane probes (gate gate-cand-1d-range-end-20260902 FAIL hard.regression) and goes back for rework.

@luisleo526 luisleo526 closed this Sep 2, 2026
@luisleo526
luisleo526 deleted the fix/range-end-close-and-gap-pad branch September 2, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant