fix(backtest): don't append symbol_suffix to symbols the broker carries bare, + two operator scripts - #18
Conversation
|
I reproduced a stale-cache failure in the current head.
That makes the stale cache authoritative in Please give the cache a finite configured maximum age, have I also ran |
3e8c680 to
6e8e203
Compare
…detail to callers From review: /compile accepted an unbounded body, wrote the source to disk, read the whole .ex5 back into memory, and base64-inflated it into the response - so one authenticated request could consume unbounded disk, memory, worker time and bandwidth. And the catch-all handler echoed the exception class and message to the caller, which for an OSError is an internal path. Two documented per-request caps, both enforced before the resource they bound is spent: - COMPILE_MAX_SOURCE_BYTES (default 2 MB): an oversized body is refused with 413 straight from its declared Content-Length, before parsing; the decoded source is then checked against the cap itself, before anything reaches disk. - COMPILE_MAX_EX5_BYTES (default 16 MB): the artifact is size-checked on disk, before it would be read or encoded. A refusal carries no binary at all, and is a 500, not a 422 - the caller's source compiled fine; the server is declining to return the result. The log names the knob. Both settings clamp rather than raise on bad values, matching the other numeric settings in config.py - it is imported by the whole API, so a typo in an optional endpoint's tuning must not stop trading. Unexpected errors now return a bare 'internal error'; the traceback goes to the server log only. The two remaining detail leaks on the 500 path (MetaEditor's absolute path, the OSError from launching it) are genericized the same way. Six new tests: the 413 fires before the compiler ever runs and before the work dir exists, the declared-length refusal happens before parsing (proven with a non-JSON payload - a 400 would mean the parser read it), the artifact refusal carries no ex5_base64, within-cap requests are unaffected both ways, and the 500 body contains neither the exception class nor its message nor a path. The existing kaboom test permitted the leak by asserting only that log was a string; the new one closes that hole. All six fail against the previous handler. Also evicted scripts/prune-terminal-logs.sh from this branch: the squash had swept it in from unrelated local work. It is PR psyb0t#18's file (byte- identical to that branch's copy, referenced by nothing here), and psyb0t#18's own review round has since fixed a selection bug in it - keeping a stale copy in this PR would both collide with psyb0t#18 on merge and reintroduce the bug that fix removes.
…ries bare _normalize_symbol appended `symbol_suffix` to every [Tester].Symbol that did not already end with it. Brokers rarely suffix their whole book: Eightcap Global carries 56 suffixed FX pairs (EURUSD.i) alongside 785 bare metals, indices and crypto (XAUUSD, BTCUSD, ASX200). Every non-FX backtest there asked the tester for a symbol that does not exist and came back empty. The suffix is now skipped when the broker's symbol list has the bare name and lacks the suffixed one. Brokers that carry both forms -- BlackBull lists AUDUSD and AUDUSDp -- still get the suffix, so `symbol_suffix: p` keeps meaning "use the prime variant". That check needs a symbol list, and a `mode: backtest` terminal never attaches the MT5 SDK while Bases/<server>/symbols/*.dat is encrypted, so there is no way to enumerate symbols at INI-build time. GET /symbols (unfiltered only) now persists what it saw to <terminal>/mt5api-symbols.json and the INI builder reads that back. With no cache present the previous append-always behaviour is used unchanged, so a terminal that has never been primed cannot regress. Tests: tests/test_symbol_suffix_remap.py covers the bare-only, suffixed-only, both-forms and no-cache paths.
…ment Two standalone operator utilities. Neither is imported by the API; both are run by hand. prune-terminal-logs.sh — terminal64.exe and metatester64.exe write UTF-16 logs inside each terminal directory. MT5 has no retention setting and nothing prunes them: rotate-logs.sh only covers the API's own logs and the log-rotator sidecar only ever saw data/shared/logs, so these grow without bound. measure-broker-offsets.py — config.yaml's per-terminal `utc_offset` is a static number subtracted from every broker timestamp, and nothing in the stack is DST-aware, so a value that is correct in August is an hour wrong after the autumn rollover. This reports the measured offset per terminal so config.yaml can be corrected. It carries a prominent warning: hitting an SDK route on a `mode: backtest` terminal launches terminal64.exe, and POST /terminal/shutdown only detaches the SDK client, so the terminal is left holding MT5's single-instance lock and the next backtest there returns an empty "Bars=0 Ticks=0 Symbols=0" report. It must be followed by `docker compose down && ./run.sh`.
…ot terminal log too Both from review. 1. The symbol cache was authoritative forever. save() recorded 'updated' and age_seconds() could compute it, but production never called it - load() served the cached set regardless of whether the stamp was missing, malformed, or years old. A broker moving a symbol between bare and suffixed stayed wrongly normalized until someone happened to call GET /symbols, contradicting the module's own stated stale-cache fallback. The age check now lives INSIDE load(), so no caller can forget it: past symbol_cache_max_age (default 7d, SYMBOL_CACHE_MAX_AGE in the environment, clamped not raised - config.py is imported by the whole API), or with a missing or malformed 'updated' stamp, the cache counts as absent and the conservative append-always fallback applies. Stale degrades to the behaviour that shipped before the cache existed, never to an old answer presented as a current one. Tests: stale/missing/malformed stamps (parametrized), the window respected both ways, and the integration-level case from review run through the real INI-builder path - a fresh bare-symbol cache suppresses the remap, then the same cache aged to epoch stops suppressing it and the suffix comes back. All fail against the previous code. 2. prune-terminal-logs.sh never selected the root <terminal>/logs. find_logs matched only Tester/logs and Tester/Agent-*/logs, so the terminal's own log - the biggest of the three documented locations - was invisible to both the age and the size pass and grew without bound while every pass reported clean. One '*/logs/*.log' pattern now covers all three, with MQL5/ excluded by name so expert logs stay untouched even on a case-insensitive mount. New behavioral suite (tests/test_prune_terminal_logs.py) runs the real script against a fixture tree: all three locations pruned by age, MQL5 trees untouched, recent files kept, the size cap truncating (not deleting) the root log, and dry-run naming the root log without touching anything.
6e8e203 to
14394f7
Compare
|
Both fixed in 1. The cache is no longer authoritative foreverYou named the design flaw precisely:
The integration-level test you asked for runs through the real INI-builder path ( 2. The root terminal log is pruned now
New behavioral suite ( Merge order#15 → #16 → #18 → #10, this third. #16 previously carried a stale copy of Also rebased onto current master (post-#17); the one conflict was the Full suite green, lint clean. |
Two independent changes. Happy to split them if you'd rather review separately — say the word.
1.
symbol_suffixwas appended to symbols the broker carries bare_normalize_symbolappendedsymbol_suffixto every[Tester].Symbolthat did not already end with it. Brokers rarely suffix their whole book: Eightcap Global carries 56 suffixed FX pairs (EURUSD.i) alongside 785 bare metals, indices and crypto (XAUUSD,BTCUSD,ASX200). Every non-FX backtest there asked the tester for a symbol that does not exist and came back empty.The suffix is now skipped when the broker's symbol list has the bare name and lacks the suffixed one. Brokers that carry both forms — BlackBull lists
AUDUSDandAUDUSDp— still get the suffix, sosymbol_suffix: pkeeps meaning "use the prime variant".That check needs a symbol list, and there is no way to get one at INI-build time: a
mode: backtestterminal never attaches the SDK, andBases/<server>/symbols/*.datis encrypted. SoGET /symbols(unfiltered only) persists what it saw to<terminal>/mt5api-symbols.json, and the INI builder reads that back.With no cache present the previous append-always behaviour is used unchanged, so a terminal that has never been primed cannot regress. Priming is one
GET /<broker>/<account>/symbolsper terminal.Covered by
tests/test_symbol_suffix_remap.py: bare-only, suffixed-only, both-forms, and the no-cache fallback.2. Two operator scripts
Neither is imported by the API; both are run by hand.
prune-terminal-logs.sh—terminal64.exeandmetatester64.exewrite UTF-16 logs inside each terminal directory. MT5 has no retention setting and nothing prunes them:rotate-logs.shonly covers the API's own logs, and the log-rotator sidecar only ever sawdata/shared/logs. They grow without bound.measure-broker-offsets.py—config.yaml's per-terminalutc_offsetis a static number subtracted from every broker timestamp, and nothing in the stack is DST-aware, so a value that is right in August is an hour wrong after the autumn rollover. This reports the measured offset per terminal.It carries a prominent warning: hitting an SDK route on a
mode: backtestterminal launchesterminal64.exe, andPOST /terminal/shutdownonly detaches the SDK client — so the terminal is left holding MT5's single-instance lock, and the next backtest there returns an emptyBars=0 Ticks=0 Symbols=0report. It must be followed bydocker compose down && ./run.sh.make test-unit: 433 passed, 2 skipped.make verify-binaries: OK.