76 Commits
Author SHA1 Message Date
Haozhe Wu b63aef9f86 docs: publish October 15 maintenance news in seven languages 2026-10-15 18:20:24 +08:00
Haozhe Wu be4dca6065 feat(strategy): bind adaptation generation and backtest evidence across surfaces 2026-10-13 15:03:41 +08:00
Haozhe Wu daafdfc309 feat(portfolio): integrate dated extraETF imports and allocation proposals 2026-10-13 14:31:26 +08:00
Haozhe Wu 9fc5c259fb Merge PR #1485: integrate bounded Gildata research and scoped NAV evidence 2026-10-13 13:48:15 +08:00
Haozhe Wu d3c76819c3 fix: complete October 11-12 maintenance and research updates
Fix live option guards, grounding operand evidence, source-bound benchmarks, trade artifacts, attribution, drawdowns, valuation tolerances, and SEC point-in-time factor inputs.

Add Brazil research data and market support, 13 fundamental and valuation factors, channel progress forwarding, and opt-in ZimaOS templates. Update seven README News sections with three visible dates.

Validation: 17,635 backend tests passed; 1,084 final targeted tests and 103 frontend tests passed. Frontend build, package build, documentation checks, and CI gates passed.
2026-10-12 02:29:46 +08:00
Haozhe Wu 8e430079fd fix(loaders): preserve bounded BaoStock 0.9.4 connections 2026-10-10 23:24:28 +08:00
Haozhe Wu 6965c2884b Complete UTF-8 recovery, channel review, pinned portfolio surfaces and validation repairs 2026-10-10 23:05:38 +08:00
Haozhe Wu f1fee5d35f Fix causal option pricing, channel refresh state, and market routing
Preserve scientific-notation amounts and add regression coverage for the five review findings. Update README News after successful backend, frontend, and contract checks.

Signed-off-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
2026-10-09 19:18:19 +08:00
Haozhe Wu ce73fff5a4 Finish thirteen-PR integration and repair uncovered boundary cases
Signed-off-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
2026-10-09 18:42:52 +08:00
Haozhe Wu 14cabaf195 docs: record October 7 and 8 maintenance updates 2026-10-08 03:32:24 +08:00
zeus229 79f2ceb1a5 feat(email): protect scheduled PDF reports with password
Signed-off-by: zeus229 <69399886+zeus229@users.noreply.github.com>
2026-10-06 04:47:18 +00:00
Haozhe Wu 8c6bfe0f98 docs: publish October 4 workflow news and synchronize tool inventories
Signed-off-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
2026-10-04 03:15:00 +08:00
Haozhe Wu f21aa13d90 docs: publish October 2 maintenance news in all README languages 2026-10-02 16:14:41 +08:00
Haozhe Wu 18027a0c2b docs: CHANGELOG and News for the 09-30 merges
Unreleased entries for #1572, #1634, #1636, #1578, #1602 and #1637.
#1572's own CHANGELOG hunk was written into the 0.1.16 section before
that release was cut, so it is dropped from the merge and rewritten
here under its own PR number (it cited #1569, the Email/WebSocket issue).
2026-09-30 16:35:12 +08:00
Haozhe Wu e1dbea8ad3 release: 0.1.16 — a number that can show where it came from
Rolls up 492 commits / 116 merged pull requests since 0.1.15, from 16
contributors.

The final-answer grounding gate stopped guessing what a number is from
the words around it: the model declares each figure's role (observed,
derived, proposed, cited or count), the gate checks the declaration
against the session's tool evidence, and a figure that fails is cut from
the answer instead of the whole answer being refused. A backtest's own
output now grounds the report written about it, run cards cite a metric
only where the saved CSV agrees, and the generated broker matrix states
each profile's permissions as declarations. Research runs keep what they
fetched: compaction follows the model's real context window instead of a
fixed 40K estimate. The missing-data sweep reached 59 more alphas.
Argentina (BYMA), weekly and monthly bars, Bahasa Indonesia, Gildata,
OpenCode, Email and WebSocket channels and four broker connectors (KIS,
Upbit, Toss Securities, Scalable Capital) land alongside.

Verified before release: the reporter's risk-parity prompt, which had
stopped with no_progress after 93 seconds on DeepSeek V4 Pro, completed
on 4a40a7f6's parent in 8m13s with both backtests run, 13 steps saved and
the report released with its core figures grounded.

Version declarations bumped in pyproject.toml, agent/SKILL.md,
Dockerfile, frontend/package.json, both package-lock.json entries, all
nine locales and the Layout test mock. CHANGELOG cites 116/116 merged
pull requests and credits 16/16 authors. README News carries the release
in seven languages, three entries visible.
2026-09-29 16:13:16 +08:00
Haozhe Wu 1f6e05d3ba fix(backtest): Hong Kong RMB and USD counters stay out of the HKD pool, decided by the code
The BYMA / TSX fix could not close Hong Kong. The hk_equity chain starts with
Tencent and Eastmoney, which declare no currency, so a guard on Yahoo's
declared currency only moved a counter to the next source. The venue gives a
better handle. HKEX's Stock Code Allocation Plan (updated 2026-03-12) assigns
80000-89999 to "Products traded in Renminbi", and 09000-09199, 09200-09399,
09400-09499, 09500-09599, 09700-09799, 09800-09849, 10900-10999 and
41500-41599 to products traded in USD. Every other .HK code trades in HKD, and
that includes 09600-09699 and 09850-09999 (9988.HK, 9618.HK). Checked against
the currency Yahoo declared for 24 codes on 2026-09-24: 6 CNY, 10 USD and
8 HKD, all matching.

Before: 80700.HK (Tencent's RMB counter, 375.40 CNY) or 9834.HK (a USD ETF)
ran in the HK engine's HKD pool next to 00700.HK with no check at all, because
Composite's mixed-currency guard only runs across markets. The grounding gate
also required the answer to call their prices HKD.

- _market_hooks.hk_counter_currency(): the one table. code_currency() uses it,
  so Composite's guard sees CNY / USD counters.
- runner._create_market_engine refuses a Hong Kong code traded in another
  currency, for single-market and cross-market runs alike, before any engine
  prices it.
- grounding identity._infer_currency reads the same table (lazy import, as
  context.py already imports from backtest), so 80700.HK is quoted in CNY
  whichever source served it.

Tests cover the 24 live codes, both sides of every range boundary, the refusal
(mixed, pure RMB, USD, cross-market) and an HKD-only run that still builds.
Mutants each fail: no RMB range (12), a USD range one code too wide (1),
code_currency ignoring the counter (1), the runner guard off (4), grounding
ignoring the counter (1). Related suites: 2769 passed.
2026-09-24 20:08:03 +08:00
Haozhe Wu 198f73856d fix(channels): plain IMAP upgrades with STARTTLS before the password goes out
With imap_use_ssl off, the Email adapter and its connection probe logged in
over an unencrypted IMAP connection. That predates #1544, but #1544 put the
switch in the guided setup. SMTP already had smtp_use_tls, STARTTLS by
default when not on implicit SSL. IMAP had no equivalent.

- imap_use_tls (default true) upgrades a plain IMAP connection with STARTTLS
  before LOGIN, in the adapter and the probe. A server that offers no
  STARTTLS, or a failed handshake, is reported as network-class before any
  LOGIN, so the password is never sent. Turning it off is the one explicit way
  back to a plaintext login, and its help text says so in nine locales.
- verify_tls now governs every TLS path. Both STARTTLS calls used their own
  always-verifying context, so a self-signed server had an opt-out on implicit
  SSL but not on STARTTLS. email_tls_context serves all four paths.

The panel's label map in ChannelConfigPanel.tsx is a second hand-written list
beside FIELD_HINTS. A hint missing from it still renders, as its raw key with
no help, so the plaintext warning would have been dropped silently. A test now
requires every hand-written hint to have a panel entry and an en label and
help. It passes for all four existing tables and fails if imap_use_tls's
entry is removed.

Mutants each fail: no STARTTLS in the adapter (1) or the probe (5),
imap_use_tls ignored (1), probe or adapter SMTP STARTTLS ignoring verify_tls
(1 each). Channel sweep: 448 passed. Frontend i18n + settings: 64 passed.
Upgrade note in the CHANGELOG: an imap_use_ssl=false setup against a server
without STARTTLS stops connecting until imap_use_tls is set to false.
2026-09-24 20:02:44 +08:00
Haozhe Wu 9830ab8f6b docs: the header break was openai 3.19.2, not openai 3.x (#1573)
openai 3.19.0 still built request headers with the case-sensitive
_merge_mappings; 3.19.2 switched to _merge_headers. Main's pre-fix code with
langchain-openai 1.6.5 passes 9/9 on openai 3.19.0 and fails the one test on
3.19.2. The comments, test docstring, CHANGELOG and news said 3.x.
2026-09-24 19:02:46 +08:00
Haozhe Wu e65832d348 docs(readme): 2026-09-24 news in seven languages, and CHANGELOG 2026-09-24 18:55:24 +08:00
Haozhe Wu 62842f4f4b Merge PR #1544: feat(channels): guided Web UI config for Email and WebSocket
# Conflicts:
#	CHANGELOG.md
2026-09-24 18:48:12 +08:00
Haozhe Wu a65427a3d5 docs(readme): 2026-09-23 news in seven languages, and CHANGELOG 2026-09-23 12:30:00 +08:00
shadowinlife c67af90752 Merge branch 'main' into feat/email-websocket-web-config 2026-09-23 10:36:14 +08:00
shadowinlife 2242c412ca fix(security): verify email TLS certs and guard settings-write routes
Two defense-in-depth fixes from an adversarial review of the Email/WebSocket
guided-config surface (#1544). Both harden pre-existing patterns that the new
email probe — the first whose target host is caller-controlled while reusing
stored credentials — made relevant.

Email TLS certificate verification (CERT_NONE -> verified):
- The implicit-SSL paths (IMAP4_SSL/SMTP_SSL) in the connection probe and the
  polling/send adapter used Python's default unverified context (CERT_NONE, no
  hostname check), so the mailbox password was sent to a server whose
  certificate was never checked; an active MITM could harvest it. All four
  call sites now build their context through a shared email_tls_context()
  that verifies the certificate and hostname against the system CA bundle,
  matching the STARTTLS path, which already did.
- New EmailConfig.verify_tls field (default True) is the documented opt-out
  for self-signed/internal-CA servers, surfaced in the guided Email setup with
  a localized label/help in all nine locales.

Settings-write CSRF guard:
- require_settings_write_auth was missing the cross-site browser guard its
  siblings (require_auth, require_event_stream_auth) enforce on unsafe
  methods. POST /channels/email/test merges stored credentials with a caller
  patch, so a cross-site page could steer the stored mailbox password to an
  attacker-chosen host. The guard now runs first on every settings-write
  route; same-origin Web UI calls and non-browser clients (no Origin) are
  unaffected.

Tests: 9 new email TLS regressions (helper + probe/adapter x verify_tls) and
5 new settings-write cross-site guard regressions; the email field-count
(29 -> 30) and i18n parity assertions were updated. Channel sweep 639 passed,
frontend 656 passed, test_readme_counts 108 passed; new/changed files are
black- and ruff-clean (email.py's 9 ruff errors are pre-existing import-block
debt, unchanged 9 -> 9).

Follow-up to #1519 (extends #1520, hardens #1544).

Signed-off-by: shadowinlife <shadowinlife@gmail.com>
2026-09-22 18:28:19 +08:00
shadowinlife de467063fd feat(channels): guided Web UI config for Email and WebSocket
Extend the contract-driven channel config surface from #1520 to the Email
and WebSocket adapters: both can now be configured, connection-tested and
hot-applied from Settings with no restart and no hand-edited JSON — the
"one hints dict entry" path #1519 designed for the long tail of adapters.

Backend
- Email: standalone IMAP + SMTP credential probe (email_probe.py) — login,
  mailbox select, SMTP connect/(STARTTLS)/login — classified into the frozen
  ok|invalid_credentials|network codes with secret-scrubbed, side-prefixed
  details; the adapter's test_connection delegates to it.
- WebSocket: local-server probe (websocket_probe.py) — TLS cert/key material
  validation plus a read-only bind of every resolved address (SO_REUSEADDR
  gated to POSIX, matching asyncio's create_server default); an address held
  by the already-running server reads as the expected state, not a failure.
  The config routes now build the WebSocket adapter with the gateway services
  it requires, so a WebSocket save or test no longer fails validation.
- config_meta: hand-written field hints for both channels. A hand-written
  hint's secret flag is now authoritative for the keys it covers (the regex
  fail-safe still masks every unhinted key), so token-shaped non-secrets such
  as websocket_requires_token stay visible and editable instead of being
  masked — a form save can no longer silently flip the stored token requirement.
- Email polling skips stale-config delete/move post-actions once a stop or hot
  swap begins; the already-fetched batch is still delivered.

Frontend
- Localized labels/help and guided setup for both channels across all nine
  locales; secrets are masked and never round-trip to the browser.

Adapter fix found during a live end-to-end smoke test against a real 163.com
mailbox: NetEase (163/126/yeah.net) accepts LOGIN but rejects the first SELECT
with "Unsafe Login" until the client sends an RFC 2971 IMAP ID, so the Email
channel could not read the most common Chinese mailboxes. The probe and the
polling loop now send a static, best-effort IMAP ID after login
(utils.send_imap_id) — it carries no user data or secrets and is ignored by
servers that do not support it. Verified end to end: an inbound mail from an
allow-listed sender passed SPF/DKIM, reached the agent, and an auto-reply was
delivered back over SMTP; on the same run the anti-spoofing check rejected the
mailbox's own SPF-failing service mail.

Follow-up to #1519 (extends #1520).

Signed-off-by: shadowinlife <shadowinlife@gmail.com>
2026-09-22 15:13:45 +08:00
Haozhe Wu f9a3371be8 docs(registry): Tencent serves no bars at all for 899050.BJ
The index-caliber comment said fqkline returns only "day" for all seven
indices measured; that holds for the six SSE/SZSE ones, and 899050.BJ
comes back empty.
2026-09-22 11:58:07 +08:00
Haozhe Wu 863b401486 docs(readme): second 2026-09-22 news entry in seven languages, and CHANGELOG
Weekly and monthly bars (#1479), price indices stamped raw (#1541), the
compaction replay (#1488), field-ref tail-risk identity (#1444) and the
QVeris refusal (#1494). The 2026-09-20 entry, which announced that 1M was
rejected, moves under Earlier news.
2026-09-22 11:55:23 +08:00
Haozhe Wu f3d8cf1f44 docs(readme): add #1517 to the 2026-09-22 news and CHANGELOG 2026-09-22 02:22:52 +08:00
Haozhe Wu f9355d6feb docs(readme): 2026-09-22 news in seven languages, and CHANGELOG 2026-09-22 02:13:52 +08:00
Haozhe Wu cc3e08ee4d docs(changelog): Unreleased entries for the 2026-09-21 batch 2026-09-21 20:40:45 +08:00
Haozhe Wu ead3f2e74c fix(gildata): READMEs, order fixtures and token text for the new loader (#1474)
The PR's CI failed on its own changes: the seven README loader tree lines
still said 27 sources without gildata, and two tests pinned a seven-source
A-share order that stops validating once gildata joins the chain.
README_id.md, which landed after the PR's base, also gets the source-table
row, the count and the chain. The loader docstring and CHANGELOG still said
the token rides the URL query string; since the review it is sent in the
Authorization header.
2026-09-21 20:31:02 +08:00
Haozhe Wu e6095d5209 Merge PR #1474: feat(data): Gildata (恒生聚源) A-share OHLCV loader via the raw-api MCP endpoint 2026-09-21 20:29:34 +08:00
sambazhu fd6f07b1e8 feat(tools): three Gildata research tools over the srv-tool NL endpoint
get_cn_macro_series (MacroIndustryData: China macro/regional/industry EDB
— FRED only covered US/global until now), get_cn_announcements
(AnnouncementData: A-share/HK/fund filings with highlighted excerpts —
get_sec_filings covers SEC only) and search_broker_reports
(FinancialResearchReport: the Juyuan report library, coexisting with the
eastmoney-backed get_research_reports). One thin shared client
(gildata_srv.py: JSON-RPC POST + format=json, own throttle bucket, dual
Accept header), routed api_names echoed so a mis-routed NL question is
visible to the agent, rows capped via limit, key-gated on GILDATA_TOKEN
with an optional GILDATA_SRV_TOOL_URL override. News/screening/FinQuery
deliberately left to the existing tools. MCP surface 75 -> 78.
2026-09-18 17:06:57 +08:00
sambazhu 1620acef01 feat(tools): get_fund_nav — NAV history for Chinese off-exchange funds
Off-exchange funds publish daily NAV, not OHLCV, so this lands as an
agent tool instead of stretching the loader contract: NetFundUnitValueReport
behind the same ParamCandidateRecall + disk-cache machinery (new otc_fund
wiring). Serves unit/accumulated/adjusted NAV and daily growth, ascending,
most-recent limit, per-fund errors never abort the batch; exchange-listed
codes are rejected with a pointer to get_market_data. Key-gated on
GILDATA_TOKEN; exposed as MCP tool #75 (manifest + six-README lists).
2026-09-18 17:06:57 +08:00
sambazhu 20fc8541bb feat(data): gildata serves HK equities and CN indices via internal-code recall
HKStockDailyQuotes for 00700.HK-style symbols (raw bars, volume already in
shares, stamped raw as a per-market exception after measuring 00700.HK) and
IndexDailyQuote for exchange-specific CN index codes (000xxx.SH / 399xxx.SZ,
so 000001.SZ stays Ping An Bank). Both tools need a Juyuan internal code:
ParamCandidateRecall with an exact ref_code match, cached to
~/.vibe-trading/cache/gildata-codes.json so a symbol costs one round-trip
per machine. Joins the hk_equity chain tail. US equities excluded — this
token's US history is sparse before ~2022 (12 rows in 2020, 134 in 2021),
which would silently corrupt backtests.
2026-09-18 17:06:57 +08:00
Haozhe Wu 60ef024ba3 fix(providers): a chat-completions refusal that names /v1/responses is healed there (#1473)
OpenAI's gpt-5.6-* reject function tools on /v1/chat/completions unless
reasoning_effort is 'none', the model's own default effort included, so the
agent's default openai configuration failed on its first tool call. Setting
'none' would run the model without its reasoning, so the adapter now treats
that 400 like a rejected temperature (#1223) or stream_options (#1224): the
request is retried on /v1/responses, where langchain-openai carries the effort
as reasoning.effort, and the model is remembered for the process so later
calls go there first. The matcher reads the endpoint's own instruction
(reasoning_effort + not supported + /v1/responses); a gateway that rejects the
field as unknown is not rerouted, an explicit LANGCHAIN_USE_RESPONSES_API=true
is never second-guessed, and a refusal on /v1/responses itself surfaces once.
2026-09-18 15:06:28 +08:00
sambazhu 998eb2ec2e docs(changelog): note that a saved a_share order stops validating once gildata joins 2026-09-18 13:52:58 +08:00
sambazhu 4b48ed9f40 docs: gildata row in data-routing skill, all six READMEs, changelog
Source-count 27->28, table row, A-share chain line, and an Unreleased
changelog entry describing the loader's caliber and auth behavior.
2026-09-18 13:27:39 +08:00
Haozhe Wu b1820b34bc fix(grounding): a redaction keeps a table's row numbers and the words in its cells (#1471)
Every integer in a table cell was a measurement, so a ranked table's # column
(1, 2, 3) and label cells ("12m mean return", "Mean beta (9 reported)") were
flagged and cut; _sweep_same_values then removed every bare figure sharing a
cut figure's digits, single digits included, so "Names 1 to 3", "3 names" and
the "12m" headers went with them. Three shape rules, no word list:

- a column whose numbered cells read 1, 2, 3 ... in row order (two rows or
  more, never an OHLC-bound column) is the table's index: exempt;
- a plain integer in a cell that also holds a word (two letters of any
  script) is read as prose: bare, still subject to the integer-price check
  for instruments quoted in the thousands. A cell holding only a number, any
  decimal / percent / currency figure, and any cell under an OHLC column stay
  measured;
- the sweep never keys on a single digit.

Reproduced on the two-bar ledger: 20 cuts before, the 11 invented figures
after.
2026-09-18 02:06:43 +08:00
Haozhe Wu 0144aeb7d6 fix(factors): gtja191_098 keeps NaN through an unknown drift; the smoother exception to the NaN contract is stated and pinned (#1463)
gtja191_098 compared a NaN drift with 0.05, read the False as "not below"
and emitted -DELTA(CLOSE, 3) for every bar whose 30-bar mean still held the
gap. It now yields NaN there; on gap-free data every finite value is
bit-identical and only warmup rows (3..58, inside the declared 60) change.

The remaining 35 alphas the perturbation sweep flags are all accounted for:
28 recursive statistics (GTJA SMA(A, n, m) as ewm, one running product),
which skip a missing observation and continue from their last state — the
policy decided on #1463, now stated in the operator layer's header and
pinned by test_a_smoother_skips_the_gap (gap bar NaN, next bar not, later
values not the gap-free ones; a NaN-until-rewarm mutant fails it); 3 that
declare their own partial window; 4 rank(ts_cov(rank, rank, 5)) alphas whose
flagged cells are the oracle's rank-tie artifact (moves of exactly 1/48 or
1/24, 10 to 241 bars past a 5-bar window, covariances equal to twelve
decimals).
2026-09-18 01:41:52 +08:00
Haozhe Wu 0a196e7cb6 fix(backtest): a cash-starved open in hold mode is one insufficient_capital rejection, not a zero_size per search step (#1470)
In position_adjustment="hold" the capital-fit search re-planned every open
on each bisection step, and every step whose size rounded to zero booked a
zero_size rejection for the same symbol on the same bar: one contract the
cash could not hold was reported as 26 lot-rounding failures. Trial plans
inside the search no longer reach _on_plan_rejected (_plan_open_order takes
observe=False); a sleeve that was a real order at full scale and left the
basket is reported once, after the search, as insufficient_capital. The
rebalance path's dropped sleeves use the same reason, and zero_size now means
the lot rule only. The #1274 test never scaled a sleeve away (its B target
was below one lot at full scale); it now does, and the lot-rule case is a
separate test.
2026-09-18 01:35:17 +08:00
Haozhe Wu 10447ca8a3 docs(changelog): the VaR grounding alias and the constant-substitution alpha fixes (#1464, #1463) 2026-09-18 00:39:50 +08:00
Haozhe Wu 7f14cbc9e4 docs(changelog): the local: backtest routing and event-study CAR variance fixes (#1467, #1466) 2026-09-17 22:14:54 +08:00
Haozhe Wu 1b888f8c3f feat(robinhood): read-only portfolio source and account-bound live trading (#1428, #1442)
One Robinhood login reaches several accounts, and every account-scoped tool
takes account_number. Nothing sent one, and every consumer read the reply one
level too shallow (data.positions where Robinhood nests data.data.positions),
so the portfolio could not use Robinhood and the live runner and pre-trade gate
failed closed on every read.

Shapes: one module, connectors/robinhood/mcp.py, reads every Robinhood reply,
built only from the shapes posted on #1428 (observed get_accounts and
get_portfolio, advertised get_equity_positions outputSchema). A non-empty
data.next, a null list or item, or an unmapped data key is an error, never a
shorter account.

Portfolio (#1428): robinhood-live-mcp-readonly shares the MCP server and
OAuth grant. A connection instance carries account_ref, chosen from
get_accounts through the API, the CLI (connector select-account, setup
--account) or the connection center, with nothing preselected. A source with
no account errors before any call. Positions stay unpriced, because the profile
declares no quotes.read. A non-zero or unreported options, crypto, futures,
event-contract, mutual-fund or fixed-income value fails the source.

Live (#1442): /mandate/commit requires an account for such a broker and checks
it against get_accounts (listed, not deactivated, agentic_allowed). The Web
confirm dialog and the CLI REPL ask the user to pick from GET /live/accounts,
and never take the account from the agent-written proposal. Runner reads and
submits, the ceiling fetch and the gate's reads resolve the mandate's account
on each call and unwrap through the same module. The gate refuses an unbound
mandate or an order naming another account, and prices held positions through
the order-notional quote path, failing closed per row.

Existing gate and runner tests encoded replies Robinhood never sends. They now
use tests/robinhood_mcp_helpers.py.
2026-09-17 21:33:20 +08:00
Haozhe Wu 0e820f43af fix(grounding): a metric comes from the head of a field name, and a decimal-comma answer reads its three-digit fractions (#1418, #1426)
Both issues were reproduced on main before closing them and neither example
was fixed yet:

- `sharpe_sample_size = 1.8` grounded "Sharpe ratio 1.8" and
  `drawdown_threshold = 0.12` grounded "Max drawdown was 12%", because a
  metric word anywhere in a compound leaf named its kind. Only the head names
  it now, after trailing numeric parameters and period/unit qualifiers
  ("cvar_95", "hit_rate_daily"); "strategy_max_drawdown" and
  "portfolio_var_95" still resolve.
- "−5,132%" and "2,237" were read as −5132% and 2237 next to "1,57%". An
  answer whose numbers include an unambiguous, marked decimal comma and no
  thousands grouping reads every single-comma number as a decimal; an
  unmarked "1,50" proves nothing, and "1,234,567" or "2,237.50" keeps grouping.

Replayed today's six real runs: identical verdicts. The #1418 VaR report grounds
once its 95% confidence level is declared as a count.
2026-09-15 01:17:13 +08:00
Haozhe Wu b9f79ef2e3 docs(changelog): the four residual leaks are closed, and the contract entry says so
Untagged fences, integer prices of instruments quoted in the thousands, a price
declared count, and an unchecked number reaching the stream are no longer
described as unchecked.
2026-09-15 00:53:19 +08:00
Haozhe Wu c9eaff1ea6 fix(grounding): close the review's remaining residuals and a double-streamed fallback
Each item was reproduced on the branch before this commit before fixing; two reviewer residuals
did not reproduce (a plain 56.7 against a 0.567 low is already refused; a
bare-ticker call already grounds its canonical symbol) and are not changed.

- A table cell declared with a call's ref is compared with that call's values
  for its own column: a close cell holding the high passed with `ref c1`.
- `var` and `es` count as tail-risk evidence only as the whole leaf name;
  `var_explained` and `sales_es` were read as a VaR.
- A buffered run that hit the tool-call-syntax fallback streamed its message
  twice (pre-existing).
- `thinking_done` no longer carries the figures block in an unbuffered run, and
  a held-back last line in a tool-call turn is shown once the turn ends.
- The `_PrivateCompanySkillTool` test stub (named load_skill) takes a required
  constructor argument, so registry discovery cannot instantiate it as the
  real tool for later tests.
2026-09-15 00:53:17 +08:00
Haozhe Wu 77e4587ffd docs(changelog): describe the figures-block grounding gate as it ships
The [Unreleased] entries written with this branch's first commit described the word-catalogue
gate this branch deletes (level-word allowlists, subject words, derivation
phrasings). They now describe the shipped contract: shape-only inference, the
declared roles and what each is checked against, revision cap 2 with a cut
release, the status line, and the fixes found by review and real runs.
2026-09-15 00:53:14 +08:00
Haozhe Wu 3201f2fd7d fix(grounding): release a rejected draft with its figures redacted, and accept the derivations a correcting model writes
A ten-minute run for 159516.SZ ended in the three-sentence safe_fallback
refusal after the gate rejected four drafts. Three things were wrong: the
fallback discarded the whole draft, the gate rejected derivations its own
correction prompt asks for, and a missing provenance word cost a full model
round. This makes the rejected draft releasable with only its unverified
figures cut out, and closes the escapes an adversarial audit of that change
found.

Which evidence backs which claim
- _is_price_denominated_indicator decides on the LEAF. Under the old
  any-token-anywhere rule indicators.ma_diff, .sma_cross, .ema_gap,
  .support_strength and .supertrend_direction all classified as prices, and
  any of them set to 1.30 let the answer print "现价 1.30 元". Pivot levels
  r1/r2/r3/s1/s2/s3 were unreachable dead entries (the digit strip turned r1
  into r) and now match; window/period/length/span/cross join the denylist.
- An indicator answers a price LEVEL claim and nothing else. The rule was
  first written the other way round — admissible unless the clause names an
  OHLC field — against a hand-written list of field phrases, and a
  hand-written denylist is only as complete as the day it was typed. With
  sma_20 at 1.150 and the observed close at 1.171, "收盘价为 1.150 元" was
  caught while "closed at 1.150" / "the close was 1.150" / "highest price was
  1.150" were released, and all ten spellings of a spot quote (现价 / 最新价 /
  成交价 / 报价 / 股价 / current price / the quote is) leaked in both scripts,
  because a spot quote names the latest print and names no OHLC field. The
  allowlist is 支撑 / 阻力 / 均线 / band / pivot and their kin; everything else
  is compared against OHLC evidence at the unchanged 0.5% band. A level word
  the allowlist misses costs a correction round — a denylist gap released a
  fabricated print.
- An indicator is symbol-bound. With evidence for two instruments and a
  clause naming neither, 562500's sma_20 grounded a 600519 price claim.
- A metric figure is not grounded by happening to equal an observed price. A
  run holds hundreds of observed values spanning the instrument's range, so
  any percent-free metric written in that range collides with one:
  "策略夏普比率 1.171" and "| 夏普比率 | 1.171 |" validated against an observed
  close with no backtest in the session. The figure must also be WRITTEN as a
  price, and a metric cell is exempt only when it restates a PAIR of observed
  prints ("| 峰值→当前 | 1.180 → 1.110 |"), which is the shape the exemption
  was added for.

Derivations and endpoint arithmetic
- Only a formula's own operands and result are exempt, at the position where
  they sit. Value-scoped, "建议买入价 0.95 元(0.95 × 1.171 = 1.11245 参考)"
  released an entry price 19% below every observed bar: arithmetic that
  derives nothing, whose free operand inherited the exemption its own copy
  inside the bracket earned. The occurrence inside the formula is exempt; the
  one beside it is compared like any other claim.
- A formula's RESULT may not be a market print. "2026-06-23 收盘价 = 1.171 ×
  0.80 = 0.937" claims a close the ledger holds as 1.137. When a print word
  labels the equation itself, the operands stay exempt and the result is
  compared; "基于收盘价 1.171 × 0.80 = 0.937 作为买入价" — the word on an
  operand, the result proposed as an entry — still passes.
- The result separators are one shared list for both languages: "≈" passed
  while "approximately" burned a revision round.
- _ARITHMETIC_EQUATION_RE is deleted. _evaluate_formula's two-operand rule
  already subsumes it (mutation-verified: forcing the precondition false
  changes no verdict), and it removes one entry from the regex catalogue.
- An English period ends a clause, exactly as 。 does. Two English sentences
  sharing a line were one clause, so the header's "last close" labelled the
  level claim after it and a derivation in the first sentence exempted a
  fabricated price in the second — while the Chinese translation of the same
  text got the opposite verdict.
- A drawdown may only be grounded by the falling direction. Measured on a
  1.053/0.666 ledger (true fall −36.75%, inverse rise +58.11%):
  "最大回撤约 58.1%" and "58%" both validated.
- The written-precision half unit is applied in the units the figure was
  written in. A percent figure is compared only against the percentage-point
  derivation; running it against the fraction gave an integer percent a
  0.5 band in FRACTION units — 50 percentage points — so "区间收益率约 0%"
  validated against a real +58% move.
- A STRATEGY-subject metric gets neither exemption. "回测显示该策略最大回撤
  5.9%(从 1.180 跌至 1.110)" left a session with no backtest in it at all, on
  the exemption written for a price drawdown against an observed high; the
  subject word (策略 / 回测 / 组合 / 净值 / strategy / backtest / portfolio /
  NAV) is what decides, and "股价较 5 月高点 1.053 元已回撤约 37%" keeps it.
- The endpoint exemption is applied figure by figure and the derivation
  exemption is measured in the clause the claim was measured in. One correct
  ratio used to carry every percentage in its clause out with it ("…down about
  37% from the 1.053 May high with a max drawdown of 60%"), and any true
  arithmetic anywhere on the line grounded a metric in the next clause.

Release path
- Cuts are located by the clause's character SPAN, recorded on the issue,
  not by substring-searching the normalised claim text. Measured before:
  a price written "1,450.50" was unlocatable and the release silently fell
  back to the refusal; a metric table ("| 最大回撤 | 12% |") likewise, because
  the claim is the synthetic "label: value"; and a bare cell claim "1.10"
  matched inside "21.10 亿元", rewriting an untouched turnover figure into
  "2(略※)". All three release correctly now, and the substring fallback is
  deleted rather than left carrying a rationale the span had already settled.
- After cutting the flagged spans, the same figure is swept out of the rest
  of the document. Before: "建议买入价 1.30 元" was cut while "| 第一档 | 1.30 |"
  and "- 关键位:1.30 / 1.45" survived under a footnote asserting the figure
  had been removed. Two kinds of occurrence are never swept — one the ledger
  observed, and one sitting inside a valid formula. The formula protection is
  scoped to its own character SPANS: scoped to the value, the rejected entry
  price 0.95 survived in "| 第一档 | 0.95 |" as soon as the model also wrote
  "基于收盘价 1.171 × 0.95 = 1.112" beside it, protected by the derivation. A
  percent literal this same gate accepted in the same run is protected too —
  a validated "区间收益率约 5.5%" was cut and footnoted as unmatched because an
  unrelated "策略年化波动率 5.5%" was flagged in the next clause.
- A figure the run observed is no longer reported as an invented analysis
  metric, in prose or in a table cell. "最新收盘 1.171 元且策略最大回撤 12%"
  reported 1.171 (the first unmatched number), so the release cut the
  correctly quoted close and footnoted it as unverifiable.
- The released document is validated WITH its footnote; the footnote prints
  the symbol the way the answer spells it (562500.SS, not the canonical
  562500.SH) and a large price in full (".10g", not "%g"'s 1.23457e+06).
- The redaction footnote is the only redaction statement in the document. A
  draft writing its own "※ 略去 0 处……" shipped two contradictory footnotes;
  both it and any marker pasted into the body are stripped before the cuts.
- The marker follows the script of the LINE it cuts, so a Chinese report
  containing an English table no longer releases "| First |(略※) |", and the
  column padding after a table pipe survives.
- 元 is only swallowed when it is a unit: "0.95 元宵节后关注" no longer
  becomes "(略※)宵节后关注", and 元/股 is one compound unit — cutting half of
  it left "建议买入价(略※)/股".
- The release path's own rechecks run through _validate(record=False).
  One successful redacted_release raised validation_count 1 → 2 with no model
  round, shrinking the revision budget and overstating the rejected-draft
  count in the run reason and the artifact; the safe_fallback branch now
  captures its count where the redacted branch does. The zero-round
  provenance repair takes the same non-recording path, and the document that
  actually shipped is recorded once under "released" so the artifact carries
  evidence for the fail-closed promise instead of nothing at all.
- repair_provenance fills in the source and the currency only.
  canonical_symbol_not_surfaced was repaired too: a draft reading
  "贵州茅台(雅虎,CNY)最新收盘价 1.171 元" was released in zero model rounds
  under a note naming 562500.SH. Neither is a draft carrying a price COLUMN no
  validator reads ("| 档位 | 挂单价 |"): the note says where this run's prices
  came from, and attaching it in zero rounds spends the round that was the
  model's last chance to drop that figure.

Verification
- tests/test_grounding_release.py: 28 → 83 cases (144 with parametrisation),
  every behaviour two-sided.
- Full suite green: 12903 passed, 138 skipped, 0 failed.
- 27 mutants, 26 killed, including one per finding above. The survivor is
  equivalent by construction: with the analysis derivation exemption now
  measured on the clause's own values, its `_DERIVATION_RE` keyword pre-filter
  can be widened back to the line without changing any verdict.
- Real deepseek-v4-pro E2E on 159516.SZ (run 20260909_223146_43_233953,
  10 calls / 551k tokens): status success, NOT degraded, one correction round,
  and the released answer names an entry price (0.650, the observed
  2026-09-04 intraday low) — the question the screenshot run refused.
2026-09-15 00:53:01 +08:00
Haozhe Wu 5f82526603 docs(changelog): correct what the version guard actually caught
The 0.1.15 entry said the guard "caught a fourteenth declaration site that
the eleven it was written for did not cover". That is wrong, and it credits
the guard with the opposite of what makes it good: the `Layout.test.tsx`
`app.version` mock has been in its enumeration since the guard shipped with
0.1.14 — `git show 7329cb09:agent/tests/test_release_version_consistency.py`
names it. It caught a stale value at a site it was explicitly written to
watch, which is the whole argument for enumerating declaration sites instead
of spot-checking remembered ones.
2026-09-09 15:59:57 +08:00
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