Rolls up 551 commits / 162 merged pull requests since 0.1.14, from 35
contributors.
Served price frames now stamp the adjustment caliber they carry, and a
source whose caliber was never measured against a live payload reports
`unknown` rather than guessing one. The Alpha Zoo's NaN contract moved out
of the individual alphas and into the registry: an alpha branching on an
`np.where` comparison never sees a NaN, so blanking every declared input on
one bar left 84 of 462 alphas returning a number anyway, each consumed as a
real signal because `dropna()` is what keeps a gap out of an IC. The same
sweep afterwards finds none. The futures loader chain was the last instance
of that class and closes in this release.
New markets and surfaces: UK equities (LSE .L/.IL, SDRT modelled as a
purchase-side-only duty), Zerodha Kite Connect as the fourteenth broker
(paper plus read-only, since Kite exposes no runtime live discriminator), a
read-only multi-broker portfolio across Web / REST / CLI / agent tool,
fifteen Quant Library additions, Brazilian Portuguese as the eighth locale,
read-only Nobitex and Wallex, and an offline evals harness that emits
NOT_EVALUABLE rather than passing when instrumentation is absent.
`test_release_version_consistency.py` caught a fourteenth declaration site
the eleven it was written for did not cover: the `app.version` mock in
`frontend/src/components/layout/__tests__/Layout.test.tsx`. The comment in
`test_cli_update.py` that named the current version is de-versioned here —
a version in a comment has no guard and goes stale at the next bump.
Reader-facing counts were re-measured rather than incremented: 74 MCP tools,
107 registry tools, 10 engines, 90 skills, 462 alphas, 27 data sources, 30
swarm presets, 14 broker connectors. None had drifted.
The `futures` chain named `tushare` and `akshare`. Neither implemented a
futures endpoint: `tushare._fetch_daily_frame` branches on ETF / index / HK
and sends everything else to `daily()`, the A-share equity endpoint, and
`akshare._fetch_one` ends with `# Default: try A-share`. Searching both files
for `fut_daily`, `futures_zh` or `_is_futures` returns nothing. So every
contract walked two sources that could only answer as an equity before
reaching `local`, and a user without local CSVs got "no data" from a chain
whose first two entries advertised the market in their own `markets` set.
Both futures engines are implemented and tested; the loader layer was the
missing half.
Trimming tushare's `markets` is not enough on its own. `resolve_loader`
walks FALLBACK_CHAINS and never consults `markets` — the only `.markets`
read is the explicit-source fallback — so tushare would have stayed first in
line, constructed and available on any TUSHARE_TOKEN, returning empty frames
before akshare was ever asked. The chain itself has to change.
akshare now serves Chinese contracts off the token-free Sina daily
endpoints. Measured against the live endpoints:
* `futures_zh_daily_sina` takes no date range and returns the contract's
whole life, so the requested window is applied here; RB2601 came back
with 242 rows for a 9-row request.
* It raises `ValueError: Length mismatch` rather than returning empty when
handed an exchange suffix, so `RB2601.SHFE` is stripped before the call
instead of being discovered as a fetch failure.
* `futures_main_sina` answers in Chinese column names carrying a `价`
suffix (`开盘价`), which is not the `开盘` spelling `_normalize` learned
from the equity endpoints; forwarding it unmapped selects an empty
column set.
A contract Sina does not carry — any global one — now returns None so the
chain reaches `local`, instead of being priced off `stock_zh_a_hist` without
erroring. That fallthrough is the defect itself, and it is the third time
this file has had it: ETFs (#50) and forex (#54) were each masked as broken
A-shares the same way.
The main continuous contract is routed as well. `RB0` matched no pattern and
fell to the `a_share` default, which put a leveraged futures series under
T+1 with no shorting and kept it out of the futures chain entirely — while a
dated contract lives about one year (RB2601 measured at 242 trading days),
so any backtest longer than a contract cycle has to name the rolled series.
The rule is generated from `_CN_FUTURES_PRODUCTS` rather than from a width
heuristic, so it has no collision surface: `TSLA0` stays an equity.
Verified against the live endpoints across all four exchange groups: SHFE
(RB2601, RB0), CFFEX (IF2512, T2512), GFEX (SI2601), ZCE (MA2605).
`CL2412.NYMEX` and `MA605` return None without touching the A-share path.
Closes#1395.
Closes#1393, in the direction agreed on the issue: make the default
observable rather than fill the tables, because filling them leaves the next
added product silently defaulting in exactly the same way.
china_futures.py keeps four product tables of different sizes. A product listed
in one and absent from another does not fail the run -- it takes a generic
constant (margin 0.10, commission ("fixed", 5.0), multiplier 10, price limit
0.05) that is indistinguishable from a real table entry in the output. 13
products have a multiplier and no margin entry, 21 have no commission entry.
Every table lookup now goes through one recorder. Values are unchanged -- this
is observability only -- but a miss is logged WARN once per (product, field)
and the run reports it in the metrics dict as ``pricing_assumptions``, beside
the existing ``unfilled_plan_rejections``. A fully covered run declares nothing.
The discrimination that makes the report worth having: ``rb``'s real margin
rate IS 0.10, the same number as the default. "Looked up 0.10" and "assumed
0.10" must not read alike, and the test asserts they do not. ``pg`` -- the
contract #1389's own test used -- now reports margin_rate, commission and
price_limit as assumed while its multiplier (20, from #1389) is correctly
absent from the report.
``_engine_diagnostics`` is a BaseEngine hook returning {} by default, merged
into the metrics next to ``_plan_rejection_metrics``.
Verified end to end on a real ``run_backtest`` rather than on the hook alone:
the three assumptions land in ``result["metrics"]["pricing_assumptions"]``.
Mutations: unwiring the hook kills the seam test; recording hits as well as
misses kills the rb discrimination test. 1807 passed across the
engine/backtest/futures/metrics subset.
#1397 and #1398 each fixed one call site. The defect is a class, not two
instances: pandas 2.x still defaults ``Series.pct_change`` to
``fill_method='pad'``, so a missing close is forward-filled into a **0.0%
return that never happened**, and the next bar reports a two-day move as a
one-day move. Neither is a bias with a known sign -- the rolling volatility
built on them lands either side of the truth depending on the data -- and both
are fabricated observations the strategy then trades on.
Swept ``agent/src/skills`` exhaustively rather than by eye: 24 call sites in 10
files, including the positional form (``pct_change(5)``) that a search for
``pct_change()`` does not see. Two of them sat in files the PRs above were
editing at the time -- ``multi-factor/example_signal_engine.py:80`` and
``ml-strategy/SKILL.md:66`` -- and one, ``zoo_signal_engine.py``, backs the
alpha zoo.
Skill files are not inert documentation. The agent reads these examples and
templates and runs what it copies, so a padded ``pct_change`` here becomes a
padded ``pct_change`` in a user's strategy.
The pine-script conversion tables are corrected for the same reason rather than
for tidiness: they document ``df['close'].pct_change()`` as equivalent to Pine's
``(close - close[1]) / close[1]`` and TDX's ``(CLOSE-REF(CLOSE,1))/REF(CLOSE,1)``,
which is exactly ``fill_method=None`` semantics and not the padded default, so
the table asserted an equivalence that did not hold.
Equity-curve returns in ``backtest/`` are deliberately untouched: the engine
builds that series with a value on every bar and ``metrics.py`` documents its
``.fillna(0.0)`` choice at the seam.
Adds a repo-wide guard so the class cannot reopen one file at a time.
Mutation-tested both forms: reintroducing ``pct_change()`` and ``pct_change(3)``
each turn it red, naming file:line. 2873 passed across the skill/template/factor
subset on the CI dependency set.
#1396 closed the China half of the classification gap. The same shape on the
global side was still open: the bare dated forms (CL2412, ESZ4, GCM2025)
classified as futures, and the continuous form with a venue (ES.CME) classified
as futures, but the combination of the two -- the way a dated contract is
normally written -- matched no pattern and fell through to the a_share default.
Measured on main before this commit:
CL2412.NYMEX a_share CNY
ES2503.CME a_share CNY
ESZ4.CME a_share CNY
GCM2025.COMEX a_share CNY
so a USD crude contract was settled in CNY and run under A-share T+1 with no
shorting. After: all four classify futures, currency resolves through the venue
(EUREX -> EUR, everything else -> USD), and the China codes, equities, crypto,
forex and index symbols are unchanged.
The product width opens to {1,4} only in the venue-suffixed pattern, because a
recognized futures venue already proves the class -- CBOT lists single-letter
grains (C, S, W, O) that ``^[A-Z]{2,4}\d{4}$`` cannot express without also
claiming bare codes it has no venue to justify. ``C2412`` therefore stays
a_share while ``C2412.CBOT`` classifies.
Also drops the duplicate ``_is_china_futures`` import #1396 added to the test.
Reverting the new pattern turns the new test red and leaves the other 98 green.
langchain-openai 1.6.0/1.6.1 (released 2026-09-08T14:20Z) auto-routes some
model names to the Responses API when ``use_responses_api`` is unset, so a
``ChatOpenAIWithReasoning`` built without that flag now serializes
``{"reasoning": {"effort": "none"}}`` into an ``input`` request instead of a
top-level ``reasoning_effort`` on a ``messages`` request. The test job installs
from the extras, not requirements-lock.txt, so every PR opened after that
release went red on ``test_explicit_none_reaches_the_request`` regardless of
its diff.
Production is unaffected: ``build_llm`` always passes the resolved boolean from
``uses_responses_api`` (never ``None``), and 1.6.1 honours an explicit False --
verified in a throwaway venv on 1.6.1: explicit False yields ``messages`` plus
top-level ``reasoning_effort``. Only the test constructed the class directly and
so inherited the library's new default.
Pin ``use_responses_api=False`` in the fixture to mirror what build_llm does,
assert the transport before reading its fields so a future switch fails with the
payload keys rather than a bare KeyError, and add a test that the mirror is real
(build_llm still pins the flag) so the fixture cannot drift away from production
silently.
Verified on langchain-openai 1.6.1 (43 passed) and 1.4.1 (36 passed, 7 skipped);
both mutations -- dropping the pin, and making build_llm stop pinning -- go red.
#1389 fixed a real defect: SHFE/DCE/INE/GFEX products are keyed lowercase
(au, rb) while a real ts_code is uppercase on every exchange, and
_is_china_futures already upper-cases the symbol before routing either form
here. On main, 21 of 36 lowercase-keyed products returned the wrong contract
multiplier under an uppercase symbol (AU2412.SHFE: 1000 -> 10) and 23 returned
the wrong margin rate, silently, as a generic default.
It fixed that at six leaf call sites via a _ci_lookup helper, which leaves a
seventh call site free to reintroduce the bug. The sibling engine in this
directory already folds at the funnel instead (global_futures.py, inside
_extract_product), and every table lookup here is already preceded by
_extract_product, so one fold covers all six sites and any later one.
Adds the guards the fold relies on: no two product keys collide when
upper-cased, and the four tables agree on how a product is spelled. #1389's
own nine tests are unchanged and still pass.
CI went red on main immediately after today's merges, on a test none of
them touched. The green run installed mcp 1.29.1 and the red one 1.30.0:
1.30.0 enforces RFC 8414 section 3.3, requiring an authorization server's
metadata `issuer` to equal the issuer used to build the well-known URL.
IBKR's official MCP server does not satisfy that. It advertises the
authorization server https://api.ibkr.com/oauth2, while its metadata at
both well-known paths reports "issuer": "https://api.ibkr.com" (fetched
live today). Under 1.30.0 discovery raises OAuthFlowError before any token
is requested, so no IBKR connection can be established at all.
The failing test's fixture mirrors that live document, which is why the
fix is a cap on the dependency and not an edit to the fixture — making the
suite green by loosening the fixture would have hidden a broken broker
path. A guard test asserts the cap still excludes 1.30.0 and still admits
1.29.1, so the cap cannot be dropped silently once the upstream
incompatibility is resolved.
The two skills that tell the agent how to build a backtest config are the
actual consumers of `fundamental_fields`; without this an intraday config
now fails at runtime with no doc explaining why. CLAUDE.md's factors row
still said the registry NaN fix was only tracked — it landed, and its
remaining half (warmup) is now stated with the measurement.
Two loose ends in the #1387 guard, both found by probing the edges rather
than by a failing test:
* a numeric index (RangeIndex / int64) was read by pd.to_datetime as
nanoseconds since the epoch, so every row landed at a distinct
sub-second time and the frame was rejected as intraday; an unparseable
index raised a date-parse error out of the detector instead of failing
where it always failed, in the merge.
* the engine let every ValueError through unwrapped to keep the contract
error readable, which also stripped the "Tushare enrichment failed"
wrapper off genuine provider failures. The contract error now has its
own type, SubdailyPitError, and only that one passes through.
Closes#1387. ann_date / f_ann_date carry no time of day, and the merge
normalizes the bar index to midnight, so on a 1h frame a filing announced
on D was visible from D's 09:30 bar — a full session of lookahead on
filings that in CN typically land after the close. Reproduced on a4f06a29
with a 1h frame for 600519.SH and an f_ann_date of 2024-04-26: the 09:30
bar carried income_total_revenue.
Sub-daily frames are now rejected by default with an error naming the
daily contract, and `fundamental_subdaily='next_day'` opts in to intraday
enrichment under the conservative convention that D's announcement is
visible from the first bar of D+1. Daily frames are untouched: same-day
visibility there is correct, because a daily signal fills at D+1's open at
the earliest.
Resolved against main and reworked where main had moved past the branch:
* the candidate filter no longer keys on "candidate is a canonical crypto
pair". Main's later hardening of _canonical_crypto_pair (the USD base
whitelist) makes that predicate None for exactly the near-string hits the
PR set out to drop, so the rule was a no-op and its three headline tests
failed on merge. A metal/FX pair query is now treated the same way the
crypto branch treats a pair query — an exact instrument assertion — and
keeps only candidates naming the same two legs, across every spelling
(XAUUSD / XAU-USD / XAU/USD / XAUUSD=X).
* the venue/instrument tables are named constants instead of two inline
literals, with a test pinning them against the resolver's own tables.
* a crypto-quoted pair (ETH-BTC, SOL-ETH) stays crypto: the branch as
written classified everything but a stablecoin quote as forex.
Verified on main before the change: search_symbol("XAUUSD") returned exactly
one candidate, VALOUR-BTC-0-SEK.ST (a Swedish Bitcoin ETP), and XAU/USD
returned AETHUSDT-USD. Both now return no candidate, leaving the identity
unresolved rather than locked to the wrong instrument.
#1386 fixed the theta > 0 cancellation and left the mirror case open:
for theta < 0 the direct form multiplies (e^{|theta|u} - 1)(e^{|theta|v}
- 1), which overflows to inf from |theta| ~ 355 and then returns nan —
the same failure, in the regime where Kendall tau approaches -1.
The theta > 0 branch also lost accuracy at the other extreme: spelled
with plain exp, `1 + e^{-th(t-s)} - e^{-th t} - e^{-th(1-s)}` is a
1+1-1-1 cancellation as theta -> 0, and the 1/theta factor multiplied
the error by 1e6 — C(0.3, 0.7) at theta=1e-6 came out 0.20974 against
the independence limit 0.21, below the Frechet lower bound. Rewritten
with expm1 as a sum of two positive terms it is 1.1e-9.
Verified against a 2500-digit Decimal reference over theta in
[-5000, 5000]: max abs error 2e-9, no non-finite value, and the Frechet
bounds now hold for all three Archimedean families at every parameter
tested (a regression test asserts exactly that).
#1384 locked HK.06693 onto 06693.HK, but the connector writes US.AAPL,
SH.600519, SZ.000001 and BJ.430047 the same way — those still scanned as
no symbol at all, so a user who pasted one got identity_required from
every market tool, which is the bug #1384 set out to fix.
The US branch is case-sensitive: with the file's IGNORECASE flag a
case-folded match turns a "…/us.reuters/…" host inside a cited source URL
into the symbol REUTERS.US.
Today's landings: the composite engine's shared sub-engine dropping a
50 oz gold order to zero and charging an index future's fee schedule,
the futures product codes that fell through to the default multiplier of
50, the four Alpha Zoo factors that fabricated a signal through a halt,
the per-regime position sizing, and the no-progress budget with its
second-failure retry rule.
Same shape as the existing entries: three visible, 2026-09-04 demoted
into the collapsed block, one date set and one link set across all six
files. Every figure quoted was measured on this tree rather than taken
from a PR description; the ~35-iteration figure is the reporter's and is
attributed as such.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1363's no-progress budget is right for #1353, but the other half of the
change made a transient failure permanent. `ToolProgress.record` added
every unsuccessful call's identity to the refusal set, and only a
successful MUTATING call cleared it — a pure research run often has
none. A read that failed once for a reason unrelated to its arguments
(rate limit, network blip, the 1800s tool timeout) could then never be
retried in that run. Measured against the branch: main executed such a
tool twice and got its data, the branch executed it once and never did.
Count failures per identity and refuse from the second one on. #1353's
loop reissued the same rejected path ~35 times, so the block still kills
it far inside the 8-iteration budget, while one honest retry survives.
Two-sided coverage, since the PR's seven tests only asserted refusals:
a transient failure whose identical retry must EXECUTE and succeed, and
a unit test pinning both edges of the threshold plus the mutation reset.
Updated the three existing expectations that encoded refuse-on-first
(two in the no-progress suite, one in the deterministic-cache suite that
this PR had already rewritten), and corrected the README paragraph, which
described the old behaviour.
Mutation-checked: putting the threshold back to 1 fails five tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1368 relaxed the benchmark availability check from the whole run to the
regime's own bars, but `_bar_returns` reads `series[index - 1]` — the
anchor bar is outside `bar_indices` by construction, so verifying the
regime's own bars verifies nothing about it. A single unparseable
benchmark cell before a regime's first bar then reached `previous <= 0`
as None; `rebuild_evidence`'s except swallowed the TypeError and the
WHOLE run produced no evidence rows. A missing endpoint now yields no
return for that bar, exactly as a non-finite one already did.
#1369 fixed the bare `M2K` form but not its dated contracts: `M2KZ4`
still extracted `M` and silently took the `_MULTIPLIER.get(product, 50)`
default. Resolve a symbol against the multiplier table first, longest
match wins — which subsumes the PR's own month-code-collision special
case (MYM2503, FESX2503), covers the embedded-digit forms it missed, and
keeps SILZ4 as micro silver rather than SI. A product absent from the
table still falls through to the shape regexes.
Mutation-checked both ways: reverting the None guard fails the two new
strategy-discovery tests with the original TypeError; dropping the
listed-product lookup fails six global-futures cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The news entry leads with the day's two guard failures — the MCP order gate
sizing a quantity order from the live quote alone, so a buy limit at twice the
market could fill at twice the authorized notional, and the report auditor
returning PASS after skipping every unverifiable point — then the grounding,
trade-journal, compaction and Windows fixes, then Zerodha, the two Iranian
sources, and Anthropic prompt caching. Three visible entries as always;
2026-09-03 moves into the collapsed block.
Writing it surfaced a real drift. Each README's repo-tree line both counts and
NAMES the loaders, and it read '24 sources' while the registry held 27 —
nobitex and wallex from #1263 were never added, and tickerall had been missing
for longer. All six were wrong in exactly the same way, so the existing count
tests, which only prove the READMEs agree with each other, stayed green. Same
failure mode the broker pin was added for after eToro shipped as the 13th
connector against a pin of 12.
The line is fixed in all six and now guarded against the live LOADER_REGISTRY
by name, not just by count, so omitting a source fails even when the number is
edited to match. Both mutations are killed: reverting the count to 24 fails,
and dropping a single name while keeping the count correct also fails.
Each of the three was merged on evidence I could not reach myself. This
replaces that trust with a test that runs on every platform, on every CI run.
1. Windows lock (#1261). The no-sentinel fix was unverifiable on POSIX —
msvcrt is None, so the branch never executes and a regression would be
invisible to Linux and macOS CI. tests/test_windows_lock_no_sentinel.py
swaps the module-level fcntl/msvcrt handles for stubs and drives the
Windows branch anywhere, asserting behaviour (an empty file stays empty,
the first ledger append succeeds and verify_chain passes) rather than
'was locking() called'. Restoring the sentinel in all three lock sites
fails 3 of the 4 tests, including the first-append regression itself.
2. Dividend cash column (#1356). A dividend row has no quantity and no price,
so 'amount or qty * price' collapses to 0.0 when the export books the
payout under a cash-movement heading instead of the fill column. That is a
silent zero — the shape #1207 item 15 is about. Dividend rows now also try
发生金额 / 资金发生额 / 红利金额 / 实发金额 / 税后金额 and Futu's net-amount
columns, across all four parsers; anything still zero is counted into a new
dividend_rows_missing_cash profile field so the caller can SEE that the
journal was read but the money was not.
3. Docker action majors (#1359). The v4/v6/v7 bump had never executed in this
repository: docker-build.yml is workflow_dispatch only, total_runs=0. Every
input the workflow passes was checked against each new major's action.yml
at the pinned SHA (13 inputs, all present; the two setup actions take none).
A validate-build job now runs the SAME pins with push: false on any change
to the Dockerfile, this workflow, or pyproject, and the publish job is
gated to workflow_dispatch so the new triggers cannot publish an image.
That gate is one 'if:' line, so it is pinned too — removing it, flipping
validate-build to push: true, or unpinning an action each fail a test.
The file comment claiming these majors were 'a deliberate, separately testable
bump, not a drive-by change here' contradicted the file after #1359 and is
replaced by what is actually true now.
test_auto_resolves_single_connection reached CredentialStore.backend, which
imports keyring and talks to the OS vault. A developer laptop has one; a CI
runner does not, so the test was green locally and red on both CI Pythons:
keyring.errors.NoKeyringError: No recommended backend was available.
Inject the no-secrets backend the store already supports. Reproduced with a
backend-less keyring stub: the test fails without this fixture and passes with
it, and returning no secret is what the assertion wants (configured is False).
nobitex and wallex are the only sources quoting in Iranian Toman, and they
declare markets={"crypto"} purely so the resolver can reach them. Measured
before this commit, an explicit source="nobitex" whose endpoint is
unreachable resolves to okx:
nobitex is unavailable, falling back to okx for market crypto
so a BTCIRT request comes back as a USDT-quoted series presented as Toman —
a caliber error of about six orders of magnitude, not a missing-data error.
Same rule already applied to fmp in #1270, for a worse failure mode. Both
join _NO_NETWORK_FALLBACK_SOURCES with an endpoint-specific hint, pinned by a
test that fails against the previous membership.
Two blockers from the #1338 review.
1. 95e2c2f8 applied _ATTRIBUTION_RE inside _validate_price_claims, so any
fabricated price passed once it carried a citation subject. With zero tool
evidence, main rejects and that branch accepted all five of
"Analysts say TSLA.US last traded at 412.35 USD.",
"分析师指出特斯拉现价 412.35 美元。",
"The paper reports that AAPL.US closed at 189.20.",
"据研究机构报告,AAPL.US 收盘价为 189.20。",
"The filing reports the stock closed at 412.35.".
The exemption is right in the analysis gate — a paper's Sharpe is a figure
this run could never observe — and wrong in the price gate, where the
quantity IS what the run observes. Removed there, kept there.
2. The derived-return exemption was gated on _GROWTH_FRAME_RE, a growth-phrase
list (从…到 / from…to). Any wording the list missed was rejected, so
"第一日收盘 100.0 美元,第二日收盘 112.4 美元,收益率 12.4%" failed while
the from/to spelling of the same arithmetic passed — the per-language drift
test_grounding_language_parity exists to stop. Replaced with the structural
fact the review asked for: the operands must themselves appear in the clause
and be observed values. Strictly narrower than the phrase list — a bare
"cumulative return of 12.4%" carries no operands — and language-independent.
_GROWTH_FRAME_RE had no other caller and is deleted.
Both pinned, both mutations killed. 370 grounding/discipline/parity tests pass.
_compute_profile already excludes dividend rows from the trade count, the
market and hourly distributions and the behavior diagnostics, but
top_symbols still grouped the full frame: a 500-yuan 红利入账 row counted
as a third trade on 600519 and inflated its turnover. Group the trade rows
instead and pin it next to the other exclusions.
Leads on the grounding gate matching `close` but not `closed`, so a
fabricated USD price passed in English while the identical Chinese claim was
caught, and on the compaction/dedup deadlock that left a run answering
"fundamental data not retrieved" for data it had already fetched.
2026-09-02 folds into <details> so three entries stay visible. All six files
carry the same 16 PR/issue links and the same 11 acknowledgements, checked
by set comparison rather than by eye.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The price gate keys on a phrase list, and a phrase list gets written in
whatever language the bug report arrived in. That has left it strictly
leakier on one side than the other, in both directions:
- `\bclose\b` does not match "closed at", which is how an answer actually
states an observed price. Measured on main with zero tool calls and no
canonical symbol to fall back on, "The stock closed at 412.35.",
"The stock last traded at 412.35." and "Shares were quoted at 412.35."
all PASSED, while "该股收盘 412.35。" — the same fabricated claim — was
caught. A model inventing a USD price in the most natural English phrasing
walked straight through.
- Chinese had the mirror-image hole: 成交价 / 最新价 / 股价 / 收报 are as
ordinary as 收盘价 and none of them matched.
The English past-tense forms require a following "at", so reporting volume
("traded 1.2M shares") or a corporate event ("the deal closed at a 30%
premium", already percentage-masked) is not read as a quote. "fell to" /
"rose to" and 跌至 / 涨至 are deliberately left out: they carry the same
false-positive risk in both languages ("fell to 3rd place") and belong to a
separate decision, not to this one.
The new suite asserts the two verdicts are EQUAL rather than asserting a
literal per language. A per-language literal is what let the halves drift
apart, and it would let the next patch move the breakage to the other side
while staying green — which is what #1249 does today, inverting the
derived-return asymmetry rather than removing it. Both arms are pinned: five
non-price statements must be accepted in both languages, because a gate whose
tests only assert rejection cannot see itself closing on everything.
Full suite: 12440 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main has since gained shared FX canonicalisation and index routing
(bb41643c, 0c2b516e), which collides with this branch in three places. All
three are resolved as unions, because in each case the two sides cover
different symbols and choosing either one alone loses coverage:
- `_MARKET_PATTERNS`: keep this PR's `=F` futures rule and the bare
precious-metal / G10 whitelist, keep main's `^...$` index rule, and keep
main's `{3,6}` width on `=X` rather than this branch's `{6}` — the branch
only ever asserts 6-character pairs, so narrowing it would drop shorter
forms for no gain.
- `_canonical_crypto_pair`, both call sites: apply BOTH rejections. main's
fiat/fiat rule does not catch XAU-USD (XAU is a metal code, not a fiat
code), which is the exact symbol this PR exists to stop resolving to
tokenized gold; this PR's USD whitelist does not catch a fiat pair quoted
in something other than USD. They also differ in kind on purpose: fiat/fiat
has no crypto reading and gives up, while a non-whitelisted USD base only
rules out that one quote asset and must `continue` so USDT/USDC can still
match, or XAUT-USDT would be stranded.
- `test_symbol_search_tool.py`: both test classes kept
(`TestCryptoUsdBaseWhitelist` from here, `TestFxPairAlignment` from main).
Verified: all 11 routing assertions this PR adds now pass, and ^SPX / ^VIX /
GBPCNY=X / AAPL.US / BRK.B.US / 600519.SH / BTC-USDT / TD.TO / VOD.L / AAPL
still route as they do on main. 152 tests pass across the two suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The patch changes six code paths and shipped three tests. Reverting only the
three that nothing covered — sqlite get_bench_history, sqlite
get_decay_history, and the in-memory get_decay_history — left the suite at
99 passed / 0 failed, so half the fix could regress silently.
Adding the three tests turned up something worth recording rather than
quietly leaving in place: the six paths are not equally load-bearing.
`artifacts` has no index on created_at (only status and type), so its order
comes entirely from the explicit ORDER BY, and removing the tiebreaker fails
both artifact tests. `bench_history` and `decay_snapshots` DO have
(artifact_id, created_at) indexes, and SQLite walking one of those in DESC
order is already a total order — measured: with `, id DESC` removed the store
still returns newest-first. That clause is redundant on any build using the
index.
The two sqlite history tests are kept regardless, because they pin the
documented behaviour rather than the mechanism: if the index is dropped or
changed the ordering inverts silently and these catch it. The comment says
explicitly that they cannot fail on the tiebreaker alone, so nobody later
reads them as coverage of that clause.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`test_every_preset_tool_exists_in_local_registry` built its failure message
with `agent.id`, but `agent` is a dict here — the other test in this file
uses `agent["id"]`. An assertion message is only evaluated when the assertion
fails, so this stayed invisible while everything passed.
Reproduced by adding a bogus tool name to investment_committee.yaml: the test
did fail, but with `AttributeError: 'dict' object has no attribute 'id'`
instead of naming anything. On the one day this guard matters it reported
nothing usable. It now says
`investment_committee/bull_advocate requests tool 'no_such_tool_xyz' ...`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mask required the number to sit directly against 百分点, which is the
rare Chinese spelling — the ordinary one puts the measure word 个 in between
("下降 3.6 个百分点"), and 基点 was not covered at all. Measured before this
change: of seven natural Chinese spellings only one was masked, so a Chinese
fundamentals answer kept being rejected as an unsourced price claim while the
identical English answer passed. This gate sees whatever language the model
answered in and the UI ships seven locales, so an English-only mask is half a
fix.
The CJK units also drop the trailing \b. After a CJK character \b requires a
non-word character to follow, so "下降 3.6 个百分点,主因是原材料" would not
have matched even with 个 allowed. The ASCII units keep \b, which is what
stops "3.6ppm" being read as pp.
Tests are a two-sided table, since this widens what the gate ignores: seven
Chinese spellings must be masked, and five real price claims — English and
Chinese — must still be extracted. Two mutations are pinned: restoring the
old regex fails 6, removing the mask entirely fails 17.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1352 is orthogonal to the #1343 deadlock work — it fixes token accounting,
not the dedup ledger — but it collides with #1341 on how a cleared result is
represented, and the collision is the silent kind.
This PR added `_CLEARED_CONTENT = "[cleared]"` and matched it by equality.
#1341 replaced that literal with a self-describing marker that embeds the
original payload length, so no two cleared results are the same string and an
equality test never matches again. Left alone, `_result_data_gone` would have
returned False for every real cleared result and the arguments-folding this
PR exists for would have quietly done nothing.
`_result_data_gone` now composes `_is_cleared` (prefix match, layer 1) with
the `_STUB_RESULT_CONTENT` equality (layer 3), which is the union of what
either side recognised. `_CLEARED_CONTENT` is deleted rather than kept as an
alias, since nothing can reference it correctly any more.
Two test fixtures still hardcoded the old "[cleared]" literal and are moved
onto `_cleared_text()`. One of them —
`test_context_collapse_stubs_args_of_cleared_paired_call` — actually failed,
which is how the representation change was caught rather than shipped.
62 tests pass across the three affected files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1341 rekeys the dedup ledger from a tool name to (name, canonical
arguments); #1349 re-opens the ledger for tools whose only result
microcompact cleared. Landed independently they cancel each other out, and
both suites stay green because each only exercises its own shape.
1. The unblock was `_called_ok.difference_update(<names>)` against a set of
(name, args) tuples, which removes nothing at all. Measured:
{('get_fund_flow', '{...}')} minus {'get_fund_flow'} is unchanged, so the
fix for #1343 silently becomes a no-op the moment #1341 lands. Now every
argument variant of a cleared tool is dropped by name.
2. #1341's self-describing marker is longer than microcompact's own 100-char
pruning threshold, so a second pass re-cleared an already-cleared result
and rewrote its recorded size with the MARKER's length — telling the model
a tool "returned 287 characters" when 287 is the length of the placeholder.
It also re-reported the tool as newly unreadable every iteration, re-opening
and re-tracing the ledger each time. Microcompact now skips cleared content.
The `[cleared]` literal becomes a `_CLEARED_PREFIX` sentinel with
`_cleared_text` / `_is_cleared` helpers, because the marker now embeds the
payload length and no two cleared results are the same string — an equality
check against it (which `_context_collapse` was doing) silently stops
matching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ledger update was the point of this PR and nothing tested it: deleting
`self._called_ok.difference_update(unreadable_tools)` left all 46 tests
green, because the suite only asserted on `_microcompact`'s return value —
a pure function — and never on the caller that consumes it.
Extract the four inline lines into `AgentLoop._microcompact_and_unblock` so
the behaviour is reachable from a test, and add one that drives the real gate
in `_process_tool_calls`: first call executes and fills the ledger, second is
correctly skipped while the result is still readable, third must execute once
microcompact has cleared that result. Deleting the ledger update now fails
that test with the exact production symptom ("Blocked duplicate call:
get_fund_flow").
The test uses FundFlowTool, one of the five tools #1343 observed blocked;
the file's existing fixtures are all `repeatable=True` tools, which have no
gate to exercise at all, and an explicit assertion pins that precondition so
a future flag flip cannot quietly turn this back into a no-op test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Runner.execute passed preexec_fn=_make_rlimit_preexec() to subprocess.run,
so resource.setrlimit ran as Python bytecode between fork() and exec(). The
parent is `vibe-trading serve` — uvicorn plus background agent loops, always
multi-threaded — and forking a multi-threaded process to run non
async-signal-safe code before exec is undefined behaviour per POSIX. On
aarch64/glibc 2.34 it reliably SIGSEGVs the child, so every backtest launched
from the Web UI died before it produced a line of output; single-threaded CLI
invocations did not reproduce it.
The same two ceilings (RLIMIT_AS, RLIMIT_NOFILE) are now applied by the
freshly-exec'd, single-threaded interpreter via a `python -c` bootstrap that
then runpy's the entry script. No preexec_fn remains at the launch site, so
the fork-time hazard is gone on every platform rather than papered over on
the ones that happen not to crash.
The bootstrap is transparent to the entry script: argv[0] is the script,
argv[1:] the caller's cli_args, __name__ is "__main__", the script's own
directory is on sys.path exactly as with `python script.py`, and a non-zero
exit propagates unchanged. Limits are still resolved in the parent, so
VIBE_TRADING_SANDBOX_RLIMIT_AS_MB keeps a single parse site, and are still
lowered best-effort so a hardened parent limit is never raised.
Reported with a live-verified diagnosis and coredump evidence by @nandofmike.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two dated entries, one per day of merges, matching the commit dates on main.
2026-09-03 — a data source that never answers, and dated figures that could not
be proved: stooq's proof-of-work page read as "no data" so the second link of
the US-equity chain was dead weight with nothing in the log (#1342), and a
generic tool result lost the date it carried alongside its price, so a dated
claim had no evidence that could match it (#1333). Plus the MCP goal wrappers
demanding an id the registered tools call optional (#1331), the Codex adapter
dropping provider token counts (#1334), and real subprocess coverage for the
swarm's MCP stdio path (#1335).
2026-09-04 — every Claude 5 run failing at the first request (#1330), a halted
position re-marked at entry cost and printing a drawdown with no market event
behind it (#1332), three commit-time mandate-widening paths closed with cash-only
restored as the floor of the leverage axis (#1285), the elapsed clock surviving
navigation and reload (#1340), and the seven npm advisories patched.
Both entries are visible; 09-01 and 08-31 move into the folded block, keeping
three on screen per the news convention. Cross-checked by script: the PR/issue
references and the acknowledged handles are identical in all six files. Every
closes-reference verified against the PR's own closingIssuesReferences, and
every acknowledgement against the PR author.
The forward-fill limit is described without a bar count: it is 5 within one
market and 10 across markets, so naming "5-bar" would have been wrong for any
cross-market run.
All seven Dependabot alerts on this repo, all transitive, all development scope:
frontend/package-lock.json
browserslist 4.28.6 -> 4.28.8 (patched 4.28.7, HIGH)
crash / prototype write via untrusted browserslist-stats.json
postcss-selector-parser 6.1.2 -> 6.1.4 (patched 6.1.3, LOW)
denial of service through uncontrolled AST recursion
two nested copies, under postcss-nested and tailwindcss
desktop/electron/package-lock.json
fast-uri 3.1.5 -> 3.1.7 (patched 3.1.6, HIGH x4)
host confusion via percent-encoded scheme normalization; SSRF via
malformed IPv6 normalization; SSRF via repeated hostname percent-decoding;
host confusion via skipped IDN canonicalization
@xmldom/xmldom 0.8.13 -> 0.8.15 (patched 0.8.15, MEDIUM)
XML fragment injection via invalid EntityReference.nodeName
Every parent range already admitted the patched version (autoprefixer wants
browserslist ^4.28.6, postcss-nested/tailwindcss want ^6.1.1 and ^6.1.2, ajv
wants fast-uri ^3.0.1, plist wants @xmldom/xmldom ^0.8.8), so `npm update` on
the named packages was enough and no package.json or override was needed. Both
lock files carry only these bumps plus the transitive tail browserslist pulls
with it (caniuse-lite, electron-to-chromium, node-releases, baseline-browser-
mapping, update-browserslist-db).
The third root copy of postcss-selector-parser stays at 6.0.10: @tailwindcss/
typography pins that exact version, and 6.0.10 sits outside the advisory's
>= 6.1.0, < 6.1.3 range, so it is not affected.
Verified after installing both locks: frontend `npm run build` clean and 617
vitest tests pass; desktop `npm run build`, test-locales (parity verified),
test-backend-resolution, and test-packaging-preparation all pass.
The frontend job on be623c1b spent 357 of its 407 seconds inside `npm ci`; the
build it exists to run (tsc + vite) took 13. The install itself was never the
problem — the audit request npm makes after installing was:
npm warn audit 503 Service Unavailable -
POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk
npm notice This endpoint is being retired. Use the bulk advisory endpoint instead.
npm retries that call before printing "added 380 packages in 6m". Measured
locally against the same lock file with the same warm cache:
npm ci 61s
npm ci --no-audit --no-fund 4s
NPM_CONFIG_AUDIT=false NPM_CONFIG_FUND=false npm ci 4s
This is the same slowdown that timed out `test / Python 3.11` on 8555a05a, so
splitting the frontend into its own job treated the symptom; this is the cause.
Turning the audit off costs no coverage:
- `npm ci` exits 0 no matter what the audit reports — verified by running it
against this tree, not assumed from the docs. It never gated anything.
- The endpoint currently returns no advisories at all, so the call is buying
zero information even when it does answer.
- Dependabot watches the same dependency tree and is the channel that actually
reports: the 7 open alerts on this repo (fast-uri, @xmldom/xmldom,
browserslist, postcss-selector-parser) all come from it, none from here.
Set as workflow-level env rather than flags on four separate `npm ci` lines, so
any npm invocation added later inherits it. It applies to CI only — a
developer's own `npm ci` still audits once npmjs.com recovers.
`test / Python 3.11` on 8555a05a reported `cancelled` with every step green
except the last: it hit `timeout-minutes: 10` partway through `npm ci`.
The backend was not the cause. Across the nine previous CI runs on main the
Frontend build step took 14-24s; on this run it took 87s on the 3.14 leg and was
still installing at 131s on the 3.11 leg when the budget ran out. What changed
was not the tree:
frontend/package-lock.json 7b43ba37 -> HEAD a7aafd3c -> a7aafd3c (identical)
setup-node cache key both runs node-cache-Linux-x64-npm-be942593
tsc -b --force, local pre-merge 12.3s post-merge 12.5s
Same lock file, same restored npm cache, same compile time, so `npm ci` was
doing identical work more slowly — a registry or runner effect, reproduced on a
re-run an hour later.
What made a slow install fatal is the budget. The frontend build and tests were
the tail of BOTH Python matrix legs, running the same npm ci + tsc + vite +
vitest twice per push against a tree with no Python in it, and the 3.11 leg was
already at 539s of its 600s ceiling on the last green run. Sixty seconds of
headroom is not enough to absorb anything.
Splitting the frontend into its own job removes the duplicated run, gives it its
own 10-minute budget, returns ~65s to each Python leg, and lets the two run in
parallel. The commands and the tree they run against are unchanged, so nothing
is tested less than before. No required status check names change either: main
currently has none configured.
#1285 closed three real narrowing bypasses (a stringified number widening a
numeric limit, bool passing the numeric guard, and an unguarded instrument
whitelist) but treated "none" as an invalid non-numeric type. "none" is this
module's own default and the FLOOR of the leverage axis, so the gate refused
the most conservative mandate a user can commit while the equivalent 1.0 was
accepted:
_profile_fits_ceilings({"leverage": "none"}, {"leverage": 2.0})
main: True #1285: False
_normalize_leverage maps the sentinel (and None) to 1.0, a real non-bool number
to itself, and everything else to None so both call sites still fail closed.
_resolve_profile and _profile_fits_ceilings now compare on that one scale, which
keeps every refusal #1285 added ("10" is still not a number, True is still not a
number, a widened whitelist is still refused) and restores cash-only as a valid
narrowing. A cash-only CEILING still forbids real leverage, since it normalizes
to 1.0 on the same scale.
Tests gain the column the branch was missing: every H19 case asserted a refusal,
which cannot distinguish "blocks the right things" from "blocks everything", and
that is how this regression survived two rounds of green CI. Added are the
adjustments that must still commit (2.0 -> "none", 2.0 -> 1.0, unchanged, and a
narrowed whitelist subset) and an eight-row ceiling table covering both sides.
Verified by mutation: reverting the sentinel branch of _normalize_leverage turns
five of the new tests red.