Rolls up 492 commits / 116 merged pull requests since 0.1.15, from 16
contributors.
The final-answer grounding gate stopped guessing what a number is from
the words around it: the model declares each figure's role (observed,
derived, proposed, cited or count), the gate checks the declaration
against the session's tool evidence, and a figure that fails is cut from
the answer instead of the whole answer being refused. A backtest's own
output now grounds the report written about it, run cards cite a metric
only where the saved CSV agrees, and the generated broker matrix states
each profile's permissions as declarations. Research runs keep what they
fetched: compaction follows the model's real context window instead of a
fixed 40K estimate. The missing-data sweep reached 59 more alphas.
Argentina (BYMA), weekly and monthly bars, Bahasa Indonesia, Gildata,
OpenCode, Email and WebSocket channels and four broker connectors (KIS,
Upbit, Toss Securities, Scalable Capital) land alongside.
Verified before release: the reporter's risk-parity prompt, which had
stopped with no_progress after 93 seconds on DeepSeek V4 Pro, completed
on 4a40a7f6's parent in 8m13s with both backtests run, 13 steps saved and
the report released with its core figures grounded.
Version declarations bumped in pyproject.toml, agent/SKILL.md,
Dockerfile, frontend/package.json, both package-lock.json entries, all
nine locales and the Layout test mock. CHANGELOG cites 116/116 merged
pull requests and credits 16/16 authors. README News carries the release
in seven languages, three entries visible.
A live DeepSeek V4 risk-parity run after c8c2dc38 completed and released
its report, with every declared backtest figure grounded. Its second
draft still lost eight differences: the prose wrote "−1.51pp" (risk
parity minus equal weight) beside "12.87% − 11.36%", the size right and
the direction reversed. That stays refused, because the sign is part of
the claim, but the correction said only "its own note evaluates to
1.51%", and the model rewrote the numbers instead of the formulas. The
issue now carries sign_reversed and the correction says the formula runs
the other way round. The hint for a figure declared in the other unit
matches by size and says when the declared value has the opposite sign
("0.0151" for "−1.51pp"), where before it said nothing at all.
The same draft opened with "The rejection was because several figures
... were not declared", in English to a user writing Chinese, and that
sentence was released. The correction now asks for the corrected answer
alone, in the user's language, without mentioning the rejection, the
check or the figures block.
A live risk-parity run on DeepSeek V4 stopped with no_progress after 93
seconds, before any backtest ran: the model went looking for the backtest
engine's source (../../agent/src/backtest, the checkout's absolute path,
skills/../backtest/engine.py) and every attempt got "File not found or
path escapes workspace". Each spelling was a new call, so none was
blocked, and none was an observation, so eight rounds exhausted the
no-progress budget. This is the reporter's 1m03s failure: same prompt,
same shape, no compaction involved.
The refusal now says which it was (error_code outside_readable_roots or
not_found), where read_file may open files (the run directory, skills/),
and that the engine's contracts are in the skills (load_skill lists a
skill's sections). A path that names a directory inside those roots
returns its entries instead of an error. The boundary is unchanged.
The same replayed risk-parity drafts, after d2dd7006:
- A derivation note with labels glued to its operands ("等权 Sortino
1.2115 − 风险平价 1.1329") or an aside in brackets ("0.09297 − 0.10469
(收益差,约−1.17pp)") was "not arithmetic". When the note as written
fails, its words and bracketed asides are now removed, never read, and
the arithmetic between them evaluated: formula_not_evaluable 11 -> 1.
- An operand written as a percent is the fraction the evidence holds:
"13.28% − 12.98%" is 0.1328 − 0.1298 (it was 13.28 − 12.98, which no
observation anchors), and "0.666 × (1 − 3%)" is 0.666 × 0.97.
- A square or cube (HHI is a sum of squared weights) is evaluated; the
exponent is not an operand and anchors nothing, and any other power
stays unsupported (9^9^9 would never return).
- A declared value may be a fraction ("1/3") or carry a multiple mark
("5.5x", "3 倍"); one unreadable line fails the whole draft.
- A draft ending in DeepSeek's closing tool-call tags
("</||DSML||invoke>") is recognised as tool-call syntax; the
fullwidth pattern only matched opening tags, so the tags reached the
gate as figures-block lines.
The formulas that now evaluate and still fail are the model's own errors
(operands rounded before subtracting, a reversed sign, 0.1357 − 0.1287
written as 0.0069).
a71d5312 refused every bare letters-and-digits code under source=auto as a
US ticker without its suffix. Chinese futures codes (RB0, rb2501, IF2412)
and joined crypto pairs (BTCUSDT) have that shape too; the market detector
the loaders route by reads them as futures and crypto, and they were
answered "US equity symbols must include the .US suffix". The check now
asks the same detector and refuses only what it reads as a US equity.
A risk-parity vs equal-weight backtest report ended in the canned refusal in
12 of 15 replayed runs. Replaying all 26 stored drafts through the gate
(880 of 882 recorded rejections reproduced) showed why:
- Only kind-named metrics (Sharpe, return, drawdown ...) became evidence.
Sortino, Calmar, turnover, final value, Monte Carlo p-values, the risk
X-ray and the optimiser's weights were refused although the backtest
wrote every one of them. The run card, metrics row, risk X-ray,
validation and rebalance summaries are now recorded whole (lists are
not descended; files other than metrics.csv/json must match the run
card's artifact manifest). A per-bar table counts only for the rows a
read_file showed, while it is still byte for byte what the engine wrote.
A file the model wrote is never evidence.
- A ref naming the run (rp, rp/artifacts/metrics.csv, rp/metrics.csv,
"risk_parity target_positions.csv", backtest::rp, rp::sortino) resolved
to nothing. The run directory now names that backtest's output and no
other run's; one run's same-named fields are one source, two runs are
two (correction candidates are written rp::field).
- Naming a symbol in the request made it a market answer, and a market
answer with no fetched price could never be released with its failing
figures cut. A completed analysis now can; the refusal for a price
question with neither prices nor analysis is unchanged, and the refusal
after an analysis no longer claims the draft cited prices.
- Derivations: formula constants parse unsigned, so a negative observation
(a drawdown) could never anchor; operands now match by magnitude. A field
ref two runs hold anchors both for arithmetic. With several instruments'
bars and no symbol, symbol-less evidence the ref names may anchor;
instrument prints still may not.
- A figure declared as a fraction but written as a percent stays
undeclared (spec section 2), and the correction now says which value
was declared.
Replay: value_mismatch 196 -> 1, not_in_referenced_call 167 -> 7,
formula_not_anchored 35 -> 18; releasable final drafts 3/15 -> 15/15.
Two-sided on the same runs (decimals): a real value declared with its run
directory passes 97% (was 23%), the same value moved 2-10% passes 0.7%.
The undeclared path is unchanged: widening it to the metrics row would
have passed 16% of invented values (4.6% today).
A user on gpt-6-sol saw "failed · 0 steps · no_progress" for a risk-parity
backtest and a 12-company comparison. Reproduced on v0.1.15 with
deepseek-v4-pro: layer 1 fired from iteration 3 (the system prompt alone
estimates ~12.5K of the fixed 20K trigger), cleared every tool result but the
last three, and the run re-fetched the same filings 4-5 times each until
no_progress at iteration 16. Raising the threshold alone took re-fetches from
54 to 0 across 8 runs.
- Compaction budget: window from VIBE_TRADING_CONTEXT_WINDOW > limit learned
from a context-length error > context_windows.json (sourced: Codex models
272K, deepseek-v4-pro/flash 1M) > 128K default; capped by
VIBE_TRADING_CONTEXT_MAX_TOKENS (200K); measured in real input tokens.
Layer 1 clears oldest-first only back to its line. A context-length error
compacts and retries once. TOKEN_THRESHOLD is deprecated and ignored.
- No-progress limits are unchanged; the stop message now names the tool,
reason and count instead of one fixed compaction-era sentence.
- Failed and cancelled attempts keep their tool trail (history said 0 steps).
- Codex: mid-run system messages become developer input instead of replacing
instructions; encrypted reasoning items are replayed; no fabricated fc_0
ids; prompt_cache_key per session; default model gpt-6-sol (gpt-5.4 is
rejected for ChatGPT accounts).
- get_market_data: no symbol resolved is an error envelope; grounding still
records each symbol as unavailable.
- loop.TOKEN_THRESHOLD test hook is a real attribute: monkeypatch used to
write the lazy 40000 back on undo and switch later tests to the old path.
Bound BaoStock messages and serialize SDK sessions; verify Copilot credential
lookup expiry without changing the SDK's per-call credential resolution.
Run public-loader health checks in isolated, credential-free processes and a
separate dependency environment, with explicit failure reports and a weekly
workflow. Render broker capabilities per profile and document verification
requirements. Update September 28 News in all seven README languages.
Closes#1626Closes#1623Closes#1617Closes#1492
Which read and order paths each connector exposes lived in per-connector
code. The profiles already declare it, so render them into a marked block in
the README's connector onboarding section; a drift test fails CI the moment
a profile changes without `python -m src.trading.capability_matrix`, so the
table cannot go stale.
Refs #1626
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Which read tools, quote paths and order kinds each connector actually has
lived in per-connector code and tribal knowledge. The profiles already
declare it, so render them: docs/broker-capabilities.md is generated by
`python -m src.trading.capability_matrix`, and a drift test fails CI the
moment a profile changes without a regenerate, so the table cannot go stale.
README's connector onboarding section links to it.
Refs #1626
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
The last few loader failures were all found by users rather than by us:
the northbound feed died silently (#1481), baostock can hang forever
(#1492), the A-share qfq caliber drifted (#1541). This adds a weekly lane
that hits every no-auth loader with one known-liquid symbol and asserts a
non-empty, correctly shaped, fresh frame, so a dead or drifted endpoint
goes red on CI instead of in someone's run.
The canary module is env-gated (VIBE_TRADING_LOADER_HEALTH=1) and
integration-marked, so the default CI lane skips it entirely. Loaders that
cannot be reached from the runner at all are reported as unreachable, not
failed; empty or stale answers fail, with one retry to ride out flaky
connections.
Closes#1623
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
baostock 0.9.3's send_msg reads with no socket timeout and loops until a
terminator that never comes on a closed connection: a silent server hangs
the loader forever, a closed one spins at 100% CPU. The loader now swaps in
a bounded send_msg and a connect with a deadline for the lifetime of each
fetch. The replacement is byte-compatible on a healthy server and returns
None on timeout, EOF, or a malformed reply, which every baostock caller
already maps to BSERR_RECVSOCK_FAIL, so a dead server degrades into an
ordinary per-symbol failure that falls through the loader chain.
The inactivity deadline defaults to 30s per read and can be tuned with
VIBE_TRADING_BAOSTOCK_TIMEOUT. Fake-server tests cover the silent-server,
close-immediately, healthy-roundtrip, malformed-reply, and refused-connect
cases.
Closes#1492
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>