Commit Graph
100 Commits
Author SHA1 Message Date
Haozhe Wu cc54832cb5 release: 0.1.15 — data that says what it is
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.
2026-09-09 15:47:15 +08:00
Haozhe Wu 703fb5018b fix(loaders): serve futures from a chain that implements them
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.
2026-09-09 15:33:37 +08:00
Haozhe Wu ca94a93011 docs(readme): add the 2026-09-09 news entry across six languages 2026-09-09 02:00:00 +08:00
Haozhe Wu 306aca2c13 feat(backtest): report which China-futures products were priced on a default
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.
2026-09-09 01:54:01 +08:00
Haozhe Wu be2c09f2a8 fix(skills): stop every skill example relying on the pandas pct_change fill default
#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.
2026-09-09 01:47:15 +08:00
Haozhe Wu 349bf202b8 fix(backtest): classify dated global futures that carry their venue suffix
#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.
2026-09-09 01:44:53 +08:00
Haozhe Wu b6c0adc2c6 Merge PR #1400 2026-09-09 01:43:13 +08:00
Haozhe Wu 1de543e5bf Merge PR #1399 2026-09-09 01:43:12 +08:00
Haozhe Wu a9dfbb5927 Merge PR #1398 2026-09-09 01:43:10 +08:00
Haozhe Wu 49b4f8347e Merge PR #1397 2026-09-09 01:43:09 +08:00
Haozhe Wu 5b75adc7be Merge PR #1396 2026-09-09 01:43:08 +08:00
Haozhe Wu aae1fd7624 Merge PR #1392 2026-09-09 01:43:07 +08:00
Haozhe Wu 545e6d5bab test(providers): pin the transport the reasoning-effort payload test asserts
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.
2026-09-09 01:39:49 +08:00
Haozhe Wu 8a88cc98e1 fix(backtest): fold china futures product casing at extraction, not per lookup
#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.
2026-09-08 17:51:05 +08:00
Haozhe Wu 53f0687b93 Merge PR #1389 2026-09-08 17:41:22 +08:00
Haozhe Wu 4abd6cc2b0 fix(deps): cap mcp below 1.30, which breaks IBKR OAuth discovery
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.
2026-09-08 01:51:34 +08:00
Haozhe Wu d99062e1e0 docs(readme): add the 2026-09-08 news entry across six languages 2026-09-08 01:32:28 +08:00
Haozhe Wu 31ad1c3314 docs: state the daily-only fundamentals contract where its consumers read it
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.
2026-09-08 01:19:59 +08:00
Haozhe Wu 16526e114b fix(backtest): make the sub-daily PIT guard reason only about clocks
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.
2026-09-08 01:15:26 +08:00
Haozhe Wu 973c5df2e8 test(engine): forward the sub-daily PIT policy through the enrichment fake 2026-09-08 01:00:01 +08:00
Haozhe Wu 260b1ddbc3 fix(backtest): stop fundamental PIT enrichment leaking into intraday bars
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.
2026-09-08 00:55:40 +08:00
Haozhe Wu f08ce62d0b Merge PR #1282
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.
2026-09-08 00:52:38 +08:00
Haozhe Wu 97c5afcdcc fix(quantlib): make the Frank copula stable at both ends of theta
#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).
2026-09-08 00:44:36 +08:00
Haozhe Wu a883c4a5bd Merge PR #1388 2026-09-08 00:38:51 +08:00
Haozhe Wu 9ae049eceb Merge PR #1386 2026-09-08 00:35:25 +08:00
Haozhe Wu 2a40fef908 fix(grounding): normalize every Futu venue prefix, not just HK
#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.
2026-09-08 00:34:55 +08:00
Haozhe Wu 5e2ca72175 Merge PR #1384 2026-09-08 00:32:37 +08:00
Haozhe Wu 174d1e7c58 Merge PR #1378 2026-09-08 00:31:44 +08:00
Haozhe Wu 64f546fb90 Merge PR #1375 2026-09-08 00:29:46 +08:00
Haozhe Wu 13a47c8f26 Merge PR #1383 2026-09-08 00:26:23 +08:00
Haozhe Wu c782adf0ec Merge PR #1382 2026-09-08 00:26:23 +08:00
Haozhe Wu a5cf9afe18 Merge PR #1381 2026-09-08 00:26:23 +08:00
Haozhe Wu 9eba37a6e0 Merge PR #1380 2026-09-08 00:26:23 +08:00
Haozhe Wu 7729a49d92 Merge PR #1379 2026-09-08 00:26:22 +08:00
Haozhe Wu 82c7208aa0 Merge PR #1377 2026-09-08 00:24:38 +08:00
Haozhe WuandClaude Opus 5 a4f06a29f6 docs(readme): add the 2026-09-07 news entry across six languages
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>
2026-09-07 02:43:58 +08:00
Haozhe WuandClaude Opus 5 a113ee0c74 fix(agent): refuse an identical call on its second failure, not its first
#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>
2026-09-07 02:20:53 +08:00
Haozhe Wu d09c0b55a5 Merge PR #1363 2026-09-07 02:17:50 +08:00
Haozhe WuandClaude Opus 5 759dfab924 fix: close the two gaps found reviewing #1368 and #1369
#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>
2026-09-07 02:01:42 +08:00
Haozhe Wu b78fd0ffe2 Merge PR #1370 2026-09-07 01:35:37 +08:00
Haozhe Wu e6daf97fbe Merge PR #1369 2026-09-07 01:35:37 +08:00
Haozhe Wu b764bc7a05 Merge PR #1368 2026-09-07 01:35:37 +08:00
Haozhe Wu f14466ff58 Merge PR #1374 2026-09-07 01:35:36 +08:00
Haozhe Wu ab6a7d4fe2 Merge PR #1373 2026-09-07 01:35:36 +08:00
Haozhe Wu 41867e1c58 Merge PR #1372 2026-09-07 01:35:35 +08:00
Haozhe Wu 465e691f19 Merge PR #1371 2026-09-07 01:35:35 +08:00
Haozhe Wu c09deeaa04 docs(readme): add 2026-09-06 news across six languages, and pin the loaders tree line
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.
2026-09-06 22:41:41 +08:00
Haozhe Wu ff6695912c fix: close the three residual risks from the 2026-09-06 merges
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.
2026-09-06 20:08:31 +08:00
Haozhe Wu 2308f25738 Merge pull request #1271 from AirHua-byte/codex/add-offline-harness-verifier
feat(evals): add offline harness artifact verifier
2026-09-06 15:50:06 +08:00
Haozhe Wu 1bf612efad Merge remote-tracking branch 'origin/main' into work-1271 2026-09-06 15:47:13 +08:00
Haozhe Wu e06d2580d5 Merge pull request #1263 from Emad211/feat/iran-market-data-sources
feat(loaders): add read-only Nobitex and Wallex market-data sources
2026-09-06 15:42:30 +08:00
Haozhe Wu 671582fe88 test(connections): stop the autoresolve test reading the real OS keyring
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).
2026-09-06 15:42:28 +08:00
Haozhe Wu 31bf54b2ab fix(loaders): never degrade an unavailable Toman source into the crypto chain
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.
2026-09-06 15:40:15 +08:00
Haozhe Wu e8b0df9acb Merge remote-tracking branch 'origin/main' into work-1263 2026-09-06 15:36:52 +08:00
Haozhe Wu 9e5386e7b5 Merge pull request #1338 from cgycorey/fix/1336-block-unsupported-claims
fix(grounding): gate analysis metrics on a completed analysis result (#1336)
2026-09-06 15:36:08 +08:00
Haozhe Wu f054180a8a Merge remote-tracking branch 'origin/main' into work-1338 2026-09-06 15:35:15 +08:00
Haozhe Wu 16a7b49f82 Merge remote-tracking branch 'origin/main' into work-1263 2026-09-06 15:03:40 +08:00
Haozhe Wu b6e4045bf0 fix(grounding): keep the attribution exemption out of the price gate, and make the derived-return exemption structural
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.
2026-09-06 15:02:34 +08:00
Haozhe Wu da3f81973d Merge remote-tracking branch 'origin/main' into work-1338 2026-09-06 14:56:22 +08:00
Haozhe Wu 00875cd240 Merge pull request #1366 from averatec0773/feat/anthropic-prompt-cache
feat(providers): cache the Anthropic static prefix and record cache usage
2026-09-06 14:54:37 +08:00
Haozhe Wu f0320ec4b9 Merge pull request #1193 from ashutoshsinghpr7/feat/zerodha-kite-connect
feat: add Zerodha Kite Connect broker support for Indian equities
2026-09-06 14:54:36 +08:00
Haozhe Wu c8e9211444 Merge remote-tracking branch 'origin/main' into work-1366 2026-09-06 14:44:33 +08:00
Haozhe Wu 90f5d4ae8e Merge remote-tracking branch 'origin/main' into work-1193 2026-09-06 14:44:26 +08:00
Haozhe Wu b271aa052a Merge pull request #1261 from Emad211/fix/windows-cross-platform
fix(windows): cross-platform locks, path handling, and test robustness
2026-09-06 14:13:10 +08:00
Haozhe Wu cbf26d8c9e Merge remote-tracking branch 'origin/main' into work-1261 2026-09-06 14:03:44 +08:00
Haozhe Wu 7a71aa04e1 Merge pull request #1356 from he-yufeng/fix/journal-dividend-cashflow
fix(agent): parse cash-dividend journal rows and book them in shadow real PnL
2026-09-06 04:46:10 +08:00
Haozhe Wu 10a285b59c fix(trade-journal): keep dividend rows out of top_symbols too
_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.
2026-09-06 04:43:34 +08:00
Haozhe Wu abfba54aae Merge remote-tracking branch 'origin/main' into work-1356 2026-09-06 04:41:54 +08:00
Haozhe Wu 4e1c7d5999 Merge pull request #1358 from saju01/fix/compaction-call-identity
fix(agent): reconcile compacted results by exact call identity
2026-09-06 04:41:03 +08:00
Haozhe Wu 07ad49f888 Merge pull request #1361 from cgycorey/fix/mcp-buy-limit-notional-worse-of
fix(live): size MCP buy-limit orders at the worse of quote and limit
2026-09-06 04:41:02 +08:00
Haozhe Wu cc4774ed42 Merge pull request #1362 from cgycorey/fix/report-audit-zero-verified-fail-closed
fix(report-audit): fail closed when nothing was verified
2026-09-06 04:41:01 +08:00
Haozhe Wu 9c0633122d Merge pull request #1359 from birdxs/fix/docker-build-upgrade-action-version
fix(ci): upgrade Docker actions to Node.js 24 compatible versions
2026-09-06 04:41:00 +08:00
Haozhe Wu a7bbaf51c0 Merge pull request #1357 from saju01/test/grounding-cjk-boundary
test(grounding): exercise the adjacent CJK boundary
2026-09-06 04:40:59 +08:00
Haozhe WuandClaude Opus 5 5c2766f9ec docs(readme): add 2026-09-05 news across six languages
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>
2026-09-05 21:40:00 +08:00
Haozhe WuandClaude Opus 5 19ff47d1ab fix(grounding): close the English price-phrase hole and pin language parity
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>
2026-09-05 15:48:28 +08:00
Haozhe WuandClaude Opus 5 2c17aaf481 Merge main into #1280 and reconcile the two FX guards instead of picking one
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>
2026-09-05 16:45:00 +08:00
Haozhe WuandClaude Opus 5 0c8d2b6144 test(strategy_store): cover the three ordering paths that had none
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>
2026-09-05 18:20:00 +08:00
Haozhe WuandClaude Opus 5 7780c0eb18 fix(test): make the preset-tool guard report which tool is missing
`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>
2026-09-05 18:00:00 +08:00
Haozhe WuandClaude Opus 5 4f6a7b8731 fix(grounding): cover the ordinary Chinese percentage-point spellings too
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>
2026-09-05 15:22:19 +08:00
Haozhe Wu 1b5e0db03b Merge remote-tracking branch 'origin/main' into work-1346 2026-09-05 15:03:05 +08:00
Haozhe Wu 28ddb0e045 Merge remote-tracking branch 'origin/main' into work-1347 2026-09-05 14:58:58 +08:00
Haozhe Wu 7b2ea792f2 Merge remote-tracking branch 'origin/main' into work-1345 2026-09-05 14:57:12 +08:00
Haozhe Wu d11562d36a Merge remote-tracking branch 'origin/main' into work-1352 2026-09-05 14:47:37 +08:00
Haozhe WuandClaude Opus 5 8a3acf7b1e Merge #1341 into #1352 and fold the cleared-marker representation change in
#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>
2026-09-05 17:30:00 +08:00
Haozhe Wu 92ef630328 Merge remote-tracking branch 'origin/main' into work-1341 2026-09-05 14:27:15 +08:00
Haozhe WuandClaude Opus 5 2b8488da25 Merge #1349 into #1341 and fix the two defects the combination exposes
#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>
2026-09-05 16:05:00 +08:00
Haozhe WuandClaude Opus 5 5a6eb80db8 test(loop): cover the call site that re-opens the dedup ledger (#1343)
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>
2026-09-05 15:40:00 +08:00
Haozhe Wu 5ca79c53e6 Merge remote-tracking branch 'origin/main' into work-1349 2026-09-05 14:14:17 +08:00
Haozhe WuandClaude Opus 5 87a289814a fix(runner): apply sandbox rlimits after exec, not in a forked child (#1355)
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>
2026-09-05 14:10:00 +08:00
Haozhe Wu d71ed8dd28 docs(readme): add 2026-09-03 and 2026-09-04 news across six languages
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.
2026-09-04 15:22:10 +08:00
Haozhe Wu 1fc13af8d5 build(deps): patch the seven open npm advisories in the two lock files
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.
2026-09-04 15:00:31 +08:00
Haozhe Wu 97f093b8d9 ci: stop waiting on npm's 503-ing audit endpoint during npm ci
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.
2026-09-04 14:35:35 +08:00
Haozhe Wu be623c1be7 ci: run the frontend as its own job instead of twice inside the Python matrix
`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.
2026-09-04 14:00:22 +08:00
Haozhe Wu 8555a05aea fix(live): sort the cash-only leverage sentinel below every number, not into the reject branch
#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.
2026-09-04 11:34:00 +08:00
Haozhe Wu 0d80e4b329 Merge PR #1285: enforce narrowing semantics for non-numeric and list mandate adjustments 2026-09-04 11:30:00 +08:00
Haozhe Wu 25d5e97aab Merge PR #1340: persist the attempt start so the elapsed clock survives navigation and reload 2026-09-04 10:52:00 +08:00
Haozhe Wu d26412cbf0 Merge PR #1332: mark halted positions at last traded close, not entry cost 2026-09-04 10:08:00 +08:00
Haozhe Wu 9b97d86811 Merge PR #1330: strip relocated extra_body.temperature in the Anthropic self-heal 2026-09-04 09:41:00 +08:00
Haozhe Wu 0ea8e92bf3 Merge PR #1331: default goal_id/expected_goal_id to the current goal 2026-09-03 23:27:00 +08:00
Haozhe Wu 3f473a7740 Merge PR #1333: preserve generic evidence timestamps 2026-09-03 23:02:00 +08:00