3389 lines
212 KiB
Markdown
3389 lines
212 KiB
Markdown
# Changelog
|
||
|
||
- Strategy adaptation now binds each child to its own description, universe, date window and hashed backtest artifacts; scoped refresh preserves other strategies' evidence. Agent/MCP, CLI, authenticated REST and the Web welcome entry share the workflow.
|
||
|
||
All notable changes to Vibe-Trading are documented in this file.
|
||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||
|
||
## [Unreleased]
|
||
|
||
### Maintenance — 2026-10-15
|
||
|
||
- Separate live safety polling from model decisions (#1854): safety checks keep
|
||
their 60-second cadence, while decisions default to every 15 minutes and use
|
||
a fresh session. Per-broker daily budgets persist across restarts (26 decisions
|
||
and 1,000,000 reported tokens by default). Optional daily scheduling and
|
||
operator-priced USD estimates are configurable through Web, REST and CLI.
|
||
Usage is checked after a response, so one response can exceed a threshold;
|
||
estimates exclude auxiliary calls and are not a provider billing cap. Missing
|
||
usage or exhausted limits trigger the existing HALT path, including its
|
||
cancel/flatten behavior. Interrupted reservations require operator inspection.
|
||
- Load bounded run previews and move blocking artifact reads off the async
|
||
event loop (#1853); preserve full trade history for display/export, defaulting
|
||
to newest first while retaining same-time fill order (#1865).
|
||
- Preserve instrument identity in ticker-keyed evidence without mis-scoping
|
||
scalar contributions (#1851). Normalize percentage-point operands before
|
||
arithmetic, including ratios and compound factors; retain evidence and sign
|
||
validation (#1823).
|
||
- Record declared backtests in strategy history with atomic measurement-based
|
||
deduplication; copied directories cannot satisfy decay-history sample
|
||
requirements, while different evaluation windows remain distinct (#1849).
|
||
- Combine digit-leading crypto bases, separated USDC/BUSD quotes and perpetual
|
||
symbols in shared market classification, including signal alignment (#1861,
|
||
#1862, #1864). Emit swing signals only after confirmation (#1873), and compute
|
||
zero-target downside deviation over the complete return sample (#1875).
|
||
- Correct fiat-pair classification and accounting-negative amounts in trade
|
||
journals (#1871, #1872). Deliver Telegram final replies when a preview was
|
||
unavailable (#1860), correlate CLI tool progress by call ID (#1859), and keep
|
||
saved skill names consistent with their front matter (#1858).
|
||
- Expand private-key, cloud/platform-token and Cookie redaction, including
|
||
quoted Cookie values, while preserving public keys and idempotence (#1874).
|
||
- Make session-based ReAct iteration limits configurable through
|
||
`VIBE_TRADING_AGENT_MAX_ITERATIONS`; the default remains 50 and values below
|
||
one are rejected (#1833). This does not disable progress or spending limits.
|
||
|
||
### Maintenance — 2026-10-13
|
||
|
||
- extraETF Investments imports now feed portfolio snapshots, with explicit row-count/freshness checks, EUR reference-rate valuation, preserved account/ISIN identity, and non-executing allocation proposals across Web, REST, CLI and the portfolio tool.
|
||
|
||
- Standalone validation preserves saved parameters and seeds across walk-forward, Monte Carlo and permutation tests (#1815).
|
||
- Custom OpenAI-compatible providers use tested credentials in the OS vault, canonical model settings and the `provider custom` CLI; existing channel settings remain available (#1264).
|
||
- Pixel Office shares a transactional manual task board across Web, CLI and the agent tool. Assignment does not imply execution, and ranks grant no trading permissions (#1616).
|
||
- mStock Type B reads expose separate holdings, net-position books and raw fund limits. Orders remain disabled; unmapped account values are excluded from portfolio aggregation (#1675).
|
||
- Gildata adds bounded China macro, announcement and broker-report research plus OTC fund NAV reads. NAV evidence is scoped to that tool's declared fields. HK raw-price caliber and exact CN index identity are preserved (#1485).
|
||
|
||
### Maintenance — 2026-10-12
|
||
|
||
- Preserve the original opening fill after scale-ins (#1780), use the book
|
||
immediately before terminal liquidation for Brinson attribution (#1782),
|
||
and start drawdown duration at the latest visit to the peak (#1784).
|
||
Three-statement projections honor the caller's balance tolerance throughout
|
||
the model (#1786); existing balance checks remain enforced.
|
||
- Add ten SEC fundamental factors and three A-share valuation factors
|
||
(`val:pb`, `val:ps_ttm`, `val:dv_ttm`) via Tushare daily_basic (#1787, #1788,
|
||
#1789). The registry now has 475 factors, 17 in the fundamental family.
|
||
Date-only SEC filings become usable one bar later on the IC path (#1790,
|
||
#1794). Prior IC measurements using same-day filings need to be rerun.
|
||
XOM keeps its current SEC identity while pre-reorganization filings extend
|
||
its history. Basic shares never substitute for missing diluted shares.
|
||
Price-times-diluted-shares ratios are valuation proxies, not reported market
|
||
capitalization. Missing input data remains missing.
|
||
- Add Brazil `.SA`/BRL routing, an equity engine, local B3/CVM/BCB/Tesouro
|
||
ingestion and the `get_br_market_data` tool, plus market selection and
|
||
labels in all nine UI languages (#1783). COTAHIST bars remain raw;
|
||
CVM statements contain latest revisions, not point-in-time evidence.
|
||
ZimaOS Compose and a fork publishing template require explicit setup;
|
||
the upstream repository gains no automatic publishing schedule.
|
||
- Forward active attempt status, tool hints and opt-in reasoning to IM
|
||
channels, including Feishu CardKit (#1792). Final answers still pass
|
||
grounding before delivery; draft text and tool-result payloads stay private.
|
||
- Complete write-target helper extraction with actual loop delegation (#1791,
|
||
#1624), preserving runtime model provenance. Clarify that connector profiles,
|
||
not the legacy config.readonly field, govern order access (#1793).
|
||
|
||
### Maintenance — 2026-10-11
|
||
|
||
- Reject unsupported live options before broker dispatch (#1773). This is a
|
||
guard fix; live options execution is not enabled.
|
||
- Require evidence for every data operand in derived figures (#1622).
|
||
Chosen multipliers/divisors need an explicit parameter declaration and
|
||
matching visible label; arithmetic checks remain enforced.
|
||
- Keep explicit offline/source-bound benchmarks on their selected loader and
|
||
report unresolved benchmarks (#1781). Distinguish entry/exit events so
|
||
break-even closes remain in trade artifacts and downstream analysis (#1779).
|
||
- Preserve optional Codex tool arguments with explicit non-strict schemas
|
||
(#1785).
|
||
|
||
### Maintenance — 2026-10-10
|
||
|
||
- Slack and Discord gain localized connection forms and read-only credential
|
||
probes (#1764, #1777). Slack verifies both bot identity and Socket Mode app
|
||
access; Discord requires a bot identity. HTTP 408/429 report temporary errors.
|
||
Failed probes leave saved settings unchanged. Discord admission policies
|
||
refresh without reconnecting.
|
||
- Recover sessions and attempts with invalid UTF-8 metadata, and skip malformed
|
||
message records while preserving readable neighbors and original files (#1775).
|
||
Redact environment-style credentials and URL userinfo (#1765); generic trade
|
||
journals retain quantities and prices instead of being misclassified as Futu
|
||
exports (#1766).
|
||
- Memory links recognize two-character identifiers (#1771). FTS recall expands
|
||
past stale index rows until enough current memories are found or the index is
|
||
exhausted (#1772).
|
||
- Pin portfolio research to an immutable snapshot (#1769): the agent accepts
|
||
`snapshot_id`, the CLI supports `portfolio show --snapshot-id ID`, REST accepts
|
||
the same identifier, and Web exports the displayed sanitized research snapshot
|
||
as JSON. Unavailable or revoked snapshots never fall back to newer data.
|
||
Technical indicators accept an optional ISO `end_date` and report provenance
|
||
plus a fingerprint of their exact input bars (#1768).
|
||
- Exclude top-level process exit codes and elapsed times from observed financial
|
||
evidence (#1767). Register existing figure/identity checks without changing
|
||
their predicates (#1691); extract compaction, tuning, usage accounting and
|
||
terminal outcome policy from the agent loop (#1631, #1678, #1662, #1641).
|
||
- Expand the options connector evidence checklist (#1770). Update compatible
|
||
Python/frontend dependencies and pinned CI actions (#1692, #1668, #1667,
|
||
#1666, #1665); align Vitest with its coverage plugin. Keep `uuid-utils` at
|
||
0.17.0 because the proposed 1.0.0 upgrade (#1670) conflicts with the current
|
||
LangChain constraint. Preserve mootdx's retirement from the public canary.
|
||
BaoStock's bounded connection wrapper accepts the new 0.9.4 API-key argument
|
||
while retaining public/VIP routing and compatibility with 0.9.3.
|
||
|
||
### Maintenance — 2026-10-09
|
||
|
||
- Channel edits to live-read settings apply without reconnecting, including a
|
||
full Web form that echoes unchanged connection values (#1737; #1734/#1736).
|
||
Web results distinguish refresh, reconnect, deferred application and degraded
|
||
reset in all nine languages. Reset reasons redact URL userinfo and old/new
|
||
secret values; a best-effort notice goes to the changed channel's last chat.
|
||
Signal refreshes its writable nested policies, not its computed allowlist.
|
||
Refresh changes only declared live fields and preserves connection state
|
||
resolved during login, including WeChat's authenticated server address.
|
||
- Strip URL credentials even with malformed ports or multiple `@` characters
|
||
(#1742; #1741). Saving or testing an echoed configuration preserves the stored
|
||
URL credentials; explicit edits and clear flags remain effective (#1744; #1743).
|
||
Enable and connection-test failures also redact credentials, including raised
|
||
SDK exceptions, and leave saved configuration unchanged.
|
||
- Recognize known slash-USD crypto pairs before forex in both correlation and
|
||
shared Agent/MCP market-data routing, while keeping precious metals on the
|
||
forex chain (#1740, #1748). Normalize
|
||
suffixed Hong Kong aliases for duplicate detection (#1746; #1745), and treat
|
||
unusable persisted cache metadata as a recoverable cache miss (#1747).
|
||
Five-digit display aliases share an identity with their four-digit spelling;
|
||
distinct currency counters stay separate. Missing cache index metadata is a
|
||
miss too, with valid entries still reused after repair.
|
||
- Options drawdown includes the initial-cash high-water mark (#1750; #1749).
|
||
Pricing volatility receives the run's annualization factor; cross-market
|
||
pricing estimates each bar's frequency from elapsed return intervals known
|
||
at that time, including subdaily intervals. Extending a run cannot change
|
||
earlier option prices (#1753). Bootstrap and walk-forward validation use
|
||
only observed adjacent equity returns across missing data (#1754).
|
||
A walk-forward window without observed endpoints or any adjacent returns
|
||
reports an error instead of a fabricated zero score or an incomplete aggregate.
|
||
- Cash-flow ingestion refuses ambiguous single-dot amounts unless the decimal
|
||
separator is supplied, while accepting unambiguous scientific notation in
|
||
cash-flow and entity-panel files (#1739). American waterfalls reject a bare `as_of`
|
||
date rather than silently ignoring it; per-deal dates remain supported (#1732).
|
||
- Retire mootdx from the default A-share chain and public canary (#1730; #1729),
|
||
with explicit-source access retained. Old source-order overrides containing
|
||
the retired slot fall back to the current default with a warning.
|
||
- Restore main CI through the configured environment accessor and corrected
|
||
correlation routing (#1740; #1751). Repair the outdated Alpha Zoo test double
|
||
while retaining production factor-metadata validation.
|
||
|
||
### Maintenance — 2026-10-08
|
||
|
||
- Optional AES-256 protection for Email PDF reports (#1709): Web, CLI
|
||
playbooks and Agent proposals persist only the boolean choice. Confirmations
|
||
show it, and creation or confirmation refuses an unconfigured password.
|
||
The private Email setting is translated in all nine UI languages.
|
||
- KIS order and fill reads query KRX, SOR and NXT, deduplicate records and fail
|
||
incomplete venue reads (#1726). An explicitly expired token is refreshed
|
||
once, including HTTP 200 business errors and 401/403 replies; other business
|
||
errors remain failures.
|
||
- Derived formulas parse the complete expression after a descriptive label
|
||
while retaining operand evidence and arithmetic validation (#1728).
|
||
|
||
### Maintenance — 2026-10-07
|
||
|
||
- Move volatile workspace state outside the system prompt (#1708, resolves
|
||
#1707); align local daily dates across markets (#1710) and distinguish bare
|
||
equity tickers from explicit crypto pairs (#1711).
|
||
- A-share benchmarks follow the declared exchange suffix (#1712), and loader
|
||
suites share the live health checker’s OHLCV contract (#1725, resolves #1723),
|
||
including Binance and a catalog assertion for newly registered sources.
|
||
- OU lag pairs retain gaps (#1713), walk-forward validation skips empty purged
|
||
training folds (#1714), undefined correlations remain missing (#1715), and
|
||
risk X-ray returns do not bridge missing observations (#1717).
|
||
- CDS premium legs include the final stub (#1716); style exposure excludes
|
||
incomplete rows and reports unmatched weight (#1718); Heston rejects
|
||
nonfinite inputs and invalid integration bounds (#1719); options Sortino
|
||
uses the full return sample for downside deviation (#1727).
|
||
- Session search respects a zero limit (#1720), and archive audit diagnostics
|
||
use the shared ledger walker to report malformed records (#1721).
|
||
|
||
### Added
|
||
|
||
- **Editable scheduled delivery and rich Email reports** (#1649, #1680).
|
||
Choose a configured channel and destination, edit existing schedules, and
|
||
select plain text, sanitized HTML or a PDF attachment for Email across Web,
|
||
CLI playbooks and agent proposals. Confirmation surfaces show the format. Format edits
|
||
persist, active sends cannot be overwritten, and desktop PDF delivery uses
|
||
the packaged renderer when native libraries are unavailable, with embedded
|
||
CJK/Arabic fonts and tested multiline/table pagination. Email PDF delivery
|
||
also supports optional AES-256 password protection using an operator-managed
|
||
Email channel secret; scheduled jobs store only the boolean choice.
|
||
- **Structured backtest summaries and paged artifact reads** (#1646, #1647,
|
||
resolves #1644 and #1645). Complete scalar metrics, structured metrics and
|
||
validation accompany endpoint-preserving equity previews and OHLCV paths.
|
||
`read_run_artifact` streams manifest-listed CSV pages within the actual
|
||
tool-result limit, with resumable offsets and bounded metadata. Grounding
|
||
accepts only shown rows of unchanged engine tables, preserving run identity
|
||
and excluding model-written files and escaped symlinks.
|
||
|
||
- **Grounding validation records fired declared checks** (#1661, with the
|
||
registry substrate from #1628, thanks @he-yufeng). Validation artifacts name
|
||
the registered checks behind their findings; the existing private/listed
|
||
identity rule is unchanged.
|
||
- **Public loader-health failures carry sanitized warning evidence** (#1643,
|
||
thanks @cgycorey). Reports retain up to three bounded loader warnings while
|
||
continuing to fail unavailable sources. Structured and quoted credentials
|
||
are redacted; warnings containing filesystem paths are omitted in full.
|
||
|
||
- **Feishu joins the guided Web UI channel setup** (#1572, thanks
|
||
@shadowinlife). The Feishu panel gets field hints and a setup guide in all
|
||
nine locales. **Test connection** is a standalone probe that requests a
|
||
tenant access token from feishu.cn or larksuite.com, whichever the config
|
||
names, without starting the bot. `stop()` now closes the lark WebSocket and
|
||
stops its dedicated event loop, so a hot reload no longer leaves the old
|
||
connection answering messages beside the new one.
|
||
|
||
### Changed
|
||
|
||
- **An MCP tool result reaches the agent once, not four times** (#1634, thanks
|
||
@zeus229). A structured result used to arrive as `data`, an identical
|
||
`structured_content`, a text block restating it and the joined `text`. Only
|
||
exact copies of `data` are dropped, and only from what the agent reads;
|
||
`MCPServerAdapter.call_tool` still returns every surface to programmatic
|
||
callers, and error or text-only results are unchanged.
|
||
- **Tool-result helpers moved out of `loop.py`** into `src/agent/tool_results.py`
|
||
(#1636, thanks @Jackzigen), the first extraction of the loop split. The
|
||
moved functions are byte-identical and `loop` still exports them; their log
|
||
records now come from the `src.agent.tool_results` logger.
|
||
|
||
### Fixed
|
||
|
||
- **Channel settings and delivery** (#1681, #1508). Unchanged nullable form
|
||
values no longer block saving or testing. Email authentication requires a
|
||
configured receiving authserv-id and sender-domain alignment; comments and
|
||
duplicate headers cannot pass as authenticated results. The guided setup
|
||
exposes the trust setting in all nine interface languages.
|
||
- **Research continuity and output access** (#1682, #1688, #1679, #1676,
|
||
#1639). Memory snippets retain real spacing and match markers, goal evidence
|
||
preserves exact tool-call provenance, export refusals explain allowed-root
|
||
configuration, macro results report both observation and delivery limits,
|
||
and skill-file deletion follows resolved auxiliary-directory boundaries.
|
||
- **Risk and portfolio mathematics** (#1659, #1658, #1657, #1656, #1653).
|
||
Attribution rejects non-finite inputs, VaR transitions preserve missing
|
||
observations, implied volatility meets its price tolerance, monthly risk
|
||
requests obtain sufficient default history, and turnover prices signed
|
||
reversals plus cash liquidation and reentry.
|
||
- **Local data, updates and broker restrictions** (#1651, #1655, #1581,
|
||
#1585). Local ranges include the entire end day, CLI updates retain the VCS
|
||
installation source, SDK position-pricing fixes retain contributor ancestry,
|
||
and unsupported Robinhood option orders remain gated even when remotely
|
||
annotated read-only. Options execution remains unimplemented (#1435).
|
||
|
||
- **Strategy-file writes no longer crash on missing model provenance fields**
|
||
(#1673, closes #1672, thanks @tomaszkubiak-courses). Each writing turn records
|
||
the provider-reported model or falls back to the configured model.
|
||
- **Monte Carlo path metrics include starting capital** (#1664, thanks
|
||
@MetaAviator). Both actual and shuffled paths include the first trade's
|
||
return and loss against the initial high-water mark when computing Sharpe
|
||
and maximum drawdown.
|
||
- **Report audits retain accounting negatives and their units** (#1663,
|
||
closes #1660, thanks @he-yufeng). ASCII and fullwidth parentheses, inner or
|
||
outer units and currency marks retain the negative sign. Malformed pairs
|
||
and non-finite or oversized values are rejected without losing the labels
|
||
of subsequent values on the same line.
|
||
- **Indonesian tool documentation matches the registry** (#1671, thanks
|
||
@he-yufeng), including `get_southbound_flow` and the measured tool counts.
|
||
|
||
- **Carhart momentum no longer reads today's price** (#1578, thanks
|
||
@Shizoqua). `academic_carhart_mom` computed the 12-month return minus the
|
||
1-month return, which moves with today's close — the opposite of skipping
|
||
the most recent month. It is now the return from 252 to 21 trading days
|
||
ago, pinned by a test that moves every close in the skipped month and
|
||
requires the value to stay put.
|
||
- **Profit factor is undefined when no trade lost** (#1602, thanks
|
||
@davidalmeida90). With no losing trade, `profit_factor` and
|
||
`profit_loss_ratio` were reported as 0.0, which ranked a run that never lost
|
||
below every other run. They are now `None` — an empty `metrics.csv` cell and
|
||
`null` in JSON — in both the daily metrics and the options engine; a run
|
||
with no closed trade keeps 0.0.
|
||
- **Stooq's challenge page stops every later request, not just the warning**
|
||
(#1637, thanks @cgycorey). Once Stooq answers with its anti-bot page, the
|
||
loader returns no data for every remaining symbol in the process without
|
||
another throttled request, so the fallback chain moves on at once. A new
|
||
process probes again.
|
||
|
||
## [0.1.16] — 2026-09-29
|
||
|
||
Rolls up 492 commits / 116 merged pull requests since 0.1.15, from 16
|
||
contributors.
|
||
|
||
The theme of this cycle is a number that can show where it came from. 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 that 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 (its
|
||
Sortino, turnover, weights and Monte Carlo p-values, not only its Sharpe)
|
||
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 rather than as runtime verification. Research
|
||
runs stopped losing what they fetched: compaction follows the model's real
|
||
context window instead of a fixed 40K estimate that began clearing tool
|
||
results from the third step. The missing-data sweep reached 59 more alphas:
|
||
15 qlib158 count factors, 20 that filled a missing input with a constant
|
||
and 24 comparison gates. 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.
|
||
|
||
### Added
|
||
|
||
- **Stock profiles carry the listing's own identity** (#1577, thanks @Shizoqua).
|
||
`get_stock_profile` returns a `listing` block beside the issuer's
|
||
fundamentals: Yahoo's symbol, exchange, quote type and quote currency, plus
|
||
the financial currency and underlying symbol when Yahoo declares them. A
|
||
secondary listing no longer reads as the primary one. `GOOGL.BA` is an ARS
|
||
line in Buenos Aires carrying Alphabet's USD financials, and `GGALD.BA` is a
|
||
USD line whose issuer reports in ARS. Nothing is inferred from the ticker.
|
||
|
||
- **Volume statistics in `technical_indicators`** (#1571, thanks @zeus229). The
|
||
tool returns the latest volume, its 20-bar mean and their ratio, computed from
|
||
the bars it already fetched. The mean needs a complete 20-bar window.
|
||
`volume.unit` is the unit the serving source declares (board lots on the
|
||
A-share sources, shares on the Yahoo family, `null` when undeclared), because
|
||
the two are 100x apart and nothing in the numbers tells them apart.
|
||
|
||
- **Argentina (BYMA) market data** (#1543, thanks @zeus229). A `.BA` symbol — a
|
||
BYMA listing or a locally traded CEDEAR — is its own market `ar_equity`,
|
||
quoted in ARS, served by `yahoo` → `yfinance` → `local`, and reported as
|
||
market `ar` by `get_stock_profile`. Execution is deliberately not modelled: an
|
||
Argentine backtest raises instead of borrowing US or crypto commissions, lot
|
||
sizes, settlement and short-selling rules. The Web UI carries the market too —
|
||
Settings source priority, the positions asset-class grouping and the sector
|
||
union — and with it `uk_equity`, `vietnam_equity` and `index`, which had been
|
||
missing from one or more of those three since they landed. `EnvConfig.data`'s
|
||
per-market `MARKET_DATA_ORDER_*` fields are now guarded against
|
||
`FALLBACK_CHAINS` instead of a hand-written list, which is why `uk_equity`
|
||
never had one.
|
||
|
||
- **Weekly and monthly bars** (#1479). `get_market_data` and backtests take
|
||
`1W` and `1M` (and `1w` / `1wk` / `1mo`). Loaders are still asked for daily
|
||
bars; `loaders.base.resample_bars` builds the period bar at the three fetch
|
||
boundaries (runner, market data, benchmark), so a weekly bar is the same
|
||
whichever source served the days, at that source's daily caliber. A week
|
||
runs Monday to Sunday, a month is a calendar month, and each bar is dated on
|
||
the last trading day inside it: first open, highest high, lowest low, last
|
||
close, summed volume and amount, volume-weighted vwap, mean funding rate.
|
||
Annualisation is 52 / 12; a weekly perpetual bar settles 21 funding periods.
|
||
`1M` is matched before any case fold, which read it as one minute in
|
||
strict-100x validation and in attribution (98,280 bars a year). The
|
||
technical-indicator tool canonicalises its documented `1wk` / `1mo` and
|
||
fetches whole periods instead of two days per bar.
|
||
|
||
- **IM channels are configured from the Web UI** (#1520, #1529; #1519, #1530,
|
||
thanks @shadowinlife). The Settings panel renders each built-in channel's
|
||
fields from backend metadata, masks secrets (`****` plus the last four
|
||
characters, never sent back), tests the unsaved form against the provider
|
||
(`ok` / `invalid_credentials` / `network` / `unsupported`), and hot-swaps only
|
||
the saved channel with no restart. DingTalk and QQ have guided setups; a
|
||
shared token probe backs both. Saves write `channels.<name>` in
|
||
`~/.vibe-trading/agent.json` atomically at mode 0600; a YAML config is shown
|
||
read-only. Reloads of one channel are serialized in `ChannelManager`, and
|
||
`stop_all` cancels a reload's start that is still connecting.
|
||
|
||
- **Email and WebSocket join the guided Web UI channel setup** (#1544, after
|
||
#1519, thanks @shadowinlife). Both channels get hand-written field metadata —
|
||
localized labels, masked secrets — plus connection tests and per-channel hot
|
||
apply. Email's test probes the real IMAP login, mailbox select and SMTP login
|
||
without ever sending a message; WebSocket is a server channel with no remote
|
||
credentials, so its test validates the TLS cert/key material and binds every
|
||
resolved address locally — an address held by the already-running server reads
|
||
as the expected state, not a failure. Saving a WebSocket config hot-swaps the
|
||
server: connected clients (including the Web UI chat) briefly disconnect and
|
||
reconnect. A hand-written field 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, and a form save can no longer silently flip
|
||
the stored token requirement. The web config routes now build the WebSocket
|
||
adapter with the gateway services it requires; previously every WebSocket save
|
||
or test failed validation. Email's polling loop also skips stale-config
|
||
delete/move post-actions once a stop or hot swap begins, while the
|
||
already-fetched batch is still delivered. Both the probe and the polling loop
|
||
now send an RFC 2971 IMAP `ID` after login: NetEase mailboxes
|
||
(163/126/yeah.net) accept the login but reject the first `SELECT` with `Unsafe
|
||
Login` until the client identifies itself, so without this the Email channel
|
||
could not read the most common Chinese mailboxes. The identification is static
|
||
and carries no user data or secrets, and servers that ignore `ID` are
|
||
unaffected; verified end-to-end against a live 163.com account.
|
||
|
||
- **Bahasa Indonesia** UI locale and `README_id.md` (#1482, thanks @Yoruxyv).
|
||
The README count tests cover it, and `MANIFEST.in` now ships `README_es.md`
|
||
and `README_id.md`.
|
||
|
||
- **Portfolio valuation accepts any ISO-4217 currency** (#1510, thanks
|
||
@zeus229). A source whose currency has no FX rate fails with the currency
|
||
named and is excluded from the totals; the healthy sources stay in.
|
||
|
||
- **Gildata (恒生聚源) joins the A-share fallback chain as a token-gated source**
|
||
(#1474, thanks @sambazhu). The new `gildata` loader talks to the vendor's
|
||
raw-api MCP endpoint (one JSON-RPC POST per call, token in the Authorization
|
||
header) and serves A-share daily OHLCV through the `StockDailyQuote` tool with
|
||
`restorationStatus=1` — forward, split-AND-dividend adjusted bars, stamped
|
||
`split_dividend` in the price-caliber table after being measured against a
|
||
live payload. Volume arrives in 万股 and is converted to shares (`volume_units:
|
||
shares`); `avgprice`/`prevcloseprice` are ignored because the vendor keeps
|
||
them on a different adjustment basis than the adjusted OHLC. An unresolvable
|
||
symbol answers `rows: []` (never an error), so the chain keeps walking. Auth
|
||
is `GILDATA_TOKEN` (Settings page field or env; `GILDATA_BASE_URL` overrides
|
||
the endpoint, `VIBE_TRADING_GILDATA_MIN_INTERVAL` the 0.3s default spacing).
|
||
Without a token the loader reports unavailable and the chain skips it — no
|
||
behavior change for existing users beyond one reorderable entry at the chain's
|
||
tail. One migration note: a `MARKET_DATA_ORDER_A_SHARE` value saved before
|
||
this change is a permutation of the old 7-source chain, so it stops validating
|
||
once `gildata` joins — the Settings card flags it and the default order
|
||
applies until the saved order is re-saved (one click) with the new source
|
||
included.
|
||
|
||
- **A Robinhood account can be a read-only portfolio source** (#1428). The new
|
||
`robinhood-live-mcp-readonly` profile uses the same MCP server and OAuth
|
||
grant as the trading profile. Its connection reads exactly one account,
|
||
picked from Robinhood's own `get_accounts` list in the connection center or
|
||
with `vibe-trading connector select-account <id>`. Nothing is preselected,
|
||
and a source with no account errors before any broker call. Holdings come
|
||
from `get_portfolio` and `get_equity_positions` and are mapped only from the
|
||
shapes the reporter posted. A second page of positions, a null list or item,
|
||
or an unknown field is an error, not a shorter account. Positions show
|
||
quantity and cost but no price until the quote reply is mapped. An account
|
||
that also holds options, crypto, futures, event contracts, mutual funds or
|
||
fixed income fails the source instead of showing an equity-only view.
|
||
|
||
- **A weekly health check of every keyless data source** (#1627, closes #1623,
|
||
thanks @he-yufeng) — `python -m backtest.loader_health` fetches one liquid
|
||
symbol from each of the 15 keyless network loaders, each in its own process
|
||
with a temporary home, no inherited keys and the loader cache off, and
|
||
requires normalised OHLCV whose last bar is at most 14 days old. The
|
||
`Public loader health` workflow runs it every Monday and on demand. An
|
||
unreachable endpoint, a missing dependency, malformed bars and a stale
|
||
series all fail the lane; as first proposed, an unreachable endpoint counted
|
||
as a skip. An offline test fails when a new keyless loader has no canary
|
||
symbol.
|
||
|
||
- **OpenCode is a built-in provider, and each request carries the session id
|
||
OpenCode Go requires** (#1416, closes #1415, thanks @tonydo) —
|
||
`LANGCHAIN_PROVIDER=opencode` with `OPENCODE_API_KEY` / `OPENCODE_BASE_URL`,
|
||
in Web Settings and both CLI onboarding flows. OpenCode Go refused every call
|
||
without `x-opencode-session` (400 `MissingSessionID`); the adapter now sends
|
||
the run's session id to any `opencode*` provider or `opencode.ai` host, and
|
||
the compaction summary call, made on its own thread, carries the same id
|
||
(`fd6c7c7c`) instead of reaching OpenCode as a second conversation.
|
||
|
||
- **KIS (Korea Investment & Securities) is a connector for KOSPI and KOSDAQ
|
||
equities** (#1407, thanks @as950118) — paper (모의투자) and live sit on
|
||
separate hosts with separate OAuth tokens, so the paper-trade profile sends
|
||
orders to KIS's own paper server rather than a local simulator. Live is
|
||
read-only: no `kis-live-trade` profile exists, and `place_order` /
|
||
`cancel_order` refuse a non-paper config before anything else. A finishing
|
||
commit (`970c235f`) pins the paper sell and cancel TR_IDs, which no test
|
||
covered, and makes a balance or order walk that hits its 20-page cap raise
|
||
instead of returning a short position list that reads as complete.
|
||
|
||
- **Upbit is a KRW crypto connector, capped at paper plus read-only** (#1408,
|
||
thanks @as950118) — one key pair reads the same account whatever profile is
|
||
declared, so there is no runtime paper/live discriminator and no live order
|
||
path. Paper orders are simulated against a live Upbit quote: no quote means
|
||
no fill, a limit order fills only if immediately marketable, and cancel
|
||
acknowledges only ids the simulator issued. Needs
|
||
`pip install "vibe-trading-ai[upbit]"`.
|
||
|
||
- **Toss Securities is a read-only connector for Korean and US equities**
|
||
(#1409, thanks @as950118) — Toss documents no sandbox and no paper/live
|
||
discriminator, so, like Trading 212, it refuses every order and cancel,
|
||
paper included. Reads follow the published schema (`/api/v1/holdings`,
|
||
`/api/v1/orders`); closed-order history is paged to completion, and a walk
|
||
past its cap, or a `hasNext` page with no cursor (`b5ff36ba`), is an error
|
||
rather than a silently truncated list.
|
||
|
||
- **Scalable Capital is a read-only MCP connector that declares quotes only**
|
||
(#1417, thanks @cgycorey) — Scalable has no paper account and publishes tool
|
||
names without argument schemas. `scalable-live-mcp-readonly` withholds
|
||
`account.read` / `positions.read` because nothing maps the holdings reply
|
||
yet: declaring them made it a portfolio source whose refresh stored a
|
||
complete snapshot with zero positions. The 29 published tools are pinned
|
||
literally as 18 read and 11 write (unknown names resolve to write), and a
|
||
hand-enabled order tool is wrapped by the live order guard and refused.
|
||
|
||
- **The README's broker capability matrix is generated from the profile
|
||
registry** (#1629, thanks @he-yufeng) — one row per built-in profile (55
|
||
across 18 connectors): environment, transport, read-only or write-enabled,
|
||
declared reads, and whether placement requires a mandate. A test fails CI
|
||
when the block and the registry disagree; regenerate with
|
||
`PYTHONPATH=agent python -m src.trading.capability_matrix`. As merged it
|
||
folded each connector into one row; `dec2c0e6` renders each profile on its
|
||
own, so a paper order capability never reads as a live permission, and
|
||
says the rows are declarations, not runtime verification.
|
||
|
||
- **Run cards record the backtest's execution and cite metrics against the
|
||
saved CSV** (#1612, closes #1610, thanks @lorenzozanee) — `run_card.json`
|
||
(schema `1.0`) carries a trace with UTC start and end, status, and SHA-256
|
||
hashes of the config and the metrics, never the payloads. As merged, every
|
||
scalar metric was cited whenever `artifacts/metrics.csv` existed; after
|
||
`0244ecea` a metric is cited only when its value matches the file's single
|
||
data row and the file matches its manifest checksum. The Markdown card and
|
||
the Run Detail page show both sections.
|
||
|
||
- **A read-only extraETF export reader** (#1406, thanks @cgycorey) — the
|
||
Investments export becomes positions and the Transaktionen export typed
|
||
movements, so a `Dividende` row is never read as a holding and a
|
||
transactions file cannot pass as a portfolio. It fails closed on anything it
|
||
cannot read without guessing (unknown header or label, US-ordered numbers,
|
||
more than 8 decimals, one instrument twice under one Portfolio ID). Nothing
|
||
calls it yet; the portfolio container side is still open under #1170.
|
||
|
||
- **Strategy-store adaptation substrate** (#1391, thanks @modelpath-dev) — a
|
||
child strategy points at its parent through `derived_from` (SQLite schema 3,
|
||
an additive nullable column), and `AdaptationPatch` can change only
|
||
universe, name and position sizing, never `signal_definition`. The child
|
||
starts with no evidence and `UNVALIDATED`; `a7bd2e30` also clears the
|
||
parent's validator, approver and versions, and makes `register_adaptation`
|
||
require the parent's evidence, refusing a stale or insufficient parent.
|
||
Nothing outside tests calls it yet (#1149 stays open).
|
||
|
||
### Changed
|
||
|
||
- **`source="qveris"` refuses markets with splits and dividends** (#1494). It
|
||
picks a capability by search rank, which ignores adjustment: for 600519.SH
|
||
the top pick was FMP's non-split-adjusted EOD, and a tool named adjusted
|
||
returned raw closes. Stocks and ETFs are refused before anything is searched
|
||
or billed; crypto, forex, futures, macro and index bars are served as before.
|
||
Both QVeris budget gates reserve only a flat per-call quote now:
|
||
`1 credits/result` had reserved 1 credit for a call that billed 9.66, and
|
||
`qveris_execute` returns `quote_not_bounded` for any other shape.
|
||
|
||
- **A price index is stamped `raw`** (#1541). Eastmoney's fqt=1 equals fqt=0 on
|
||
every bar of the A-share indices measured, Tencent serves only `day` for the
|
||
SSE/SZSE ones, and Yahoo's adjclose equals close on ^GSPC / ^NDX / ^HSI, but
|
||
the per-source table stamped an A-share index additive and a Yahoo index
|
||
dividend-adjusted. `price_caliber` takes the symbol; A-share index codes are
|
||
matched per exchange, since 000001.SZ is Ping An Bank.
|
||
|
||
- **Portfolio valuation version 3** (#1510, thanks @zeus229): snapshots stored
|
||
under version 2 are no longer read, so portfolio history starts again after
|
||
upgrading.
|
||
|
||
- **Tencent, Eastmoney and AKShare A-share prices are labelled
|
||
`split_dividend_additive`** (#1497, #1493, thanks @cgycorey). Their forward
|
||
adjustment subtracts cash dividends from the level instead of scaling by a
|
||
ratio (on 600519.SH, qfq minus raw takes five values over 500 bars against 367
|
||
distinct ratios), so a basket mixing them with multiplicative sources warns,
|
||
and a run served only by them warns that its returns are not total returns.
|
||
Tencent's HK series is labelled `raw`: it serves no adjusted HK bars. The
|
||
prices themselves are unchanged.
|
||
|
||
- `connector account` renders Binance spot balances as Asset / Free / Locked /
|
||
Total, Futu's per-currency `assets`, and Trading 212's `cash` and `metadata`
|
||
(#1539); before, Binance printed blank rows and the other two printed "No
|
||
account summary returned."
|
||
|
||
- **The final-answer grounding gate no longer guesses what a number is from the
|
||
words around it.** Only a number's SHAPE is inferred — a decimal point, a
|
||
percent sign, a currency mark or a table cell makes it a measurement; dates
|
||
(including year-less "09-14"), years, security codes, line-leading ordinals
|
||
and code in a language-tagged fence are structure; a plain integer is checked
|
||
only as a price of an instrument quoted in the thousands — so a sentence gets
|
||
the same verdict in every language. A measurement that equals a price or
|
||
volume this session's tools returned needs nothing more. Any other one is
|
||
declared in a fenced `figures` block at the end of the answer, one line per
|
||
figure — `value | role | note | ref` — and checked by role: `observed` must
|
||
appear in the referenced call or tool (`ref` may name either), filtered to the
|
||
figure's own symbol, with a currency-marked figure compared only against
|
||
money-denominated values and a percent only against non-price ones; `derived`
|
||
needs an evaluable formula whose added or subtracted operands are all
|
||
observed, matching the figure at the precision and sign it was written with;
|
||
`proposed` is a price inside its own symbol's observed range or a derivation;
|
||
`cited` must name its source on the figure's own line, because the block is
|
||
stripped before anyone reads the answer; `count` covers counts, weights,
|
||
thresholds and probabilities, while a currency-marked figure, or one inside
|
||
its instrument's price range that no derivation uses as a factor, is checked
|
||
as observed when declared `count`. No role other than `observed` may sit in a
|
||
price column. The block is stripped from the released answer and never
|
||
streamed; the artifact keeps it. The price-word, level-word, derivation-phrase
|
||
and metric-subject catalogues are deleted with it (65 compiled regexes → 12,
|
||
all shape), and `src/agent/grounding.py` is now the `src/agent/grounding/`
|
||
package.
|
||
|
||
- **A rejected answer costs one correction round, not four, and is then
|
||
released with its failing figures cut rather than refused.** The correction
|
||
prompt lists every failing figure as written, with its declared role and what
|
||
the evidence says (the observed range, the nearest observed values, or what
|
||
its own formula evaluates to, in the figure's units), and names the three
|
||
ways out: declare, rewrite to an observed value, or remove. If the second
|
||
draft still fails, each failing figure is replaced by `(略※)` /
|
||
`(omitted※)` at its own span, bare-integer restatements of a cut figure are
|
||
swept too, a footnote says how many were omitted and why, and the whole
|
||
document — footnote included — must pass the same gate before release. The
|
||
canned fallback is left for an identity finding, a run that never observed a
|
||
price, or a cut that still fails. Drafts that triggered bounded
|
||
`search_symbol` / `get_market_data` recovery do not spend the correction
|
||
budget. The run stays `degraded` with a reason naming the redaction, the
|
||
release path's rechecks are not counted as rejected drafts, and the shipped
|
||
document is recorded under `released` in `artifacts/grounding_evidence.json`.
|
||
|
||
- **A draft missing only a source or currency word gets a data note appended,
|
||
not another model round.** The canonical symbol is deliberately not repaired
|
||
this way: it is the figure's subject, and a note naming it under an answer
|
||
about another instrument would footnote a misattribution.
|
||
|
||
- **The chat shows that the answer is being checked.** The agent loop emits
|
||
`grounding_status` (`{stage: "revising", round, issues}` after a rejection,
|
||
`{stage: "released_redacted", removed}` before a cut release), and the chat
|
||
page shows "Checking the figures in this answer (round N)…" until answer text
|
||
arrives, in all eight locales.
|
||
|
||
- MetaTrader 5 no longer picks a broker alias when several match
|
||
(`47a036d6`, with #1597 and #1598). Quotes, history, orders, symbol search
|
||
and the backtest loader took the shortest suffixed name (`EURUSDm` over
|
||
`EURUSDz`); they now refuse and ask for the exact native symbol. The
|
||
connector still tries a configured `symbol_suffix` first; the backtest
|
||
loader does not read it.
|
||
|
||
- `run_card.json` `schema_version` is `1.0` (was `0.1`), with the optional
|
||
`tool_traces` and `citations` keys from #1612.
|
||
|
||
- Broker connectors: 18, up from 14 (KIS, Upbit, Toss Securities, Scalable
|
||
Capital). `test_readme_counts.EXPECTED_BROKERS` is checked against the
|
||
profile registry, not incremented by hand.
|
||
|
||
- The opt-in loader cache is at version 7 (#1411, thanks @shadowinlife). Entries
|
||
written under version 6, which include Tencent series the forward walk
|
||
truncated, are never matched again and are fetched afresh.
|
||
|
||
- `VIBE_TRADING_BAOSTOCK_TIMEOUT` (default 30 seconds) bounds each BaoStock lock
|
||
wait, connection and complete message (#1615, thanks @he-yufeng). It is not a
|
||
deadline for a whole multi-symbol fetch.
|
||
|
||
- The `a_share` fallback chain is `tencent`, `mootdx`, `eastmoney`,
|
||
`baostock`, `akshare`, `tushare`, `gildata`, `local` (#1474, thanks @sambazhu).
|
||
|
||
- Factor values change for 23 alphas, so an IC, bench or comparison stored
|
||
before this release is not comparable with one computed after it:
|
||
`qlib158_cntp*` / `cntn*` / `cntd*` (15 factors; the first value lands one
|
||
bar later and windows touching a gap are NaN, #1459), `qlib158_wvma5` …
|
||
`wvma60` (5; every value, #1594), and `gtja191_103` / `133` / `177` (3;
|
||
direction and scale, #1604). Patell z for `market_adjusted` and
|
||
`mean_adjusted` event studies moves slightly as well (#1449, thanks @Shizoqua).
|
||
|
||
- **A grounding correction turn is offered no tools** (#1600, thanks @zeus229)
|
||
— after a rejected draft the model was handed every tool again, so revising
|
||
a figure could restart research. Unless the gate asks for recovery (an
|
||
unresolved symbol or a missing price), the correction turn gets none. A call
|
||
made anyway is not run and, since `47a036d6`, spends one of the two
|
||
correction rounds; at the cap the last real draft goes through the existing
|
||
cut-and-recheck release.
|
||
|
||
### Removed
|
||
|
||
- **The Requesty built-in provider.** Its capabilities were OpenRouter's, with
|
||
no adapter of its own. Same request shape: `LANGCHAIN_PROVIDER=openrouter`,
|
||
`OPENROUTER_BASE_URL=https://router.requesty.ai/v1`, and the Requesty key as
|
||
`OPENROUTER_API_KEY`.
|
||
|
||
### Fixed
|
||
|
||
#### Long research runs stop losing what they fetched
|
||
|
||
A user's two runs on the Codex provider (GPT-6 Sol) failed as "0 steps ·
|
||
no_progress": a risk-parity backtest after 1m03s and a comparison of A-share
|
||
and Hong Kong game companies after 4m50s. They were two different failures.
|
||
|
||
- **Context compaction follows the model's real context window** — compaction
|
||
fired at a fixed estimate of 40,000 tokens (characters / 4 of the messages,
|
||
not counting tool definitions), while the system prompt alone is about
|
||
12,500 of them. From the third step it cleared every tool result but the
|
||
last three, so a comparison across a dozen companies re-fetched the same
|
||
filings until the no-progress stop: reproduced on v0.1.15 with DeepSeek V4
|
||
Pro, seven A-share issuers' filings fetched four to five times each, stop at
|
||
iteration 16; with a larger budget the re-fetches went from 54 to 0. The
|
||
window now comes from `VIBE_TRADING_CONTEXT_WINDOW`, else a limit learned
|
||
from the provider's own context-length error during the run, else
|
||
`src/providers/context_windows.json` (272K for the Codex GPT-6 / GPT-5.6
|
||
models, 1M for DeepSeek V4, each entry naming its source), else 128K, and
|
||
`VIBE_TRADING_CONTEXT_MAX_TOKENS` (200K) caps what a prompt may cost. Usage
|
||
is read from the input tokens the provider reports. The three layers fire at
|
||
80 / 90 / 100% of what the system prompt and tool schemas leave, and clear
|
||
the oldest results first, only down to the line. A context-length error is
|
||
compacted and retried instead of failing the run. `TOKEN_THRESHOLD` is
|
||
deprecated and ignored with a warning; remove it from `.env`.
|
||
|
||
- **`read_file` says where the agent may read, and lists a directory** — the
|
||
1m03s failure: on the same prompt, a live DeepSeek V4 run went looking for
|
||
the backtest engine's source (`../../agent/src/backtest`, the checkout's
|
||
absolute path, `skills/../backtest/engine.py`) and every attempt got the same
|
||
"File not found or path escapes workspace". Each spelling was a new call and
|
||
none an observation, so eight rounds exhausted the no-progress budget in 93
|
||
seconds, before any backtest ran. The refusal now names the case
|
||
(`outside_readable_roots` or `not_found`), the readable roots (the run
|
||
directory and `skills/`) and where the engine's contracts are documented
|
||
(`load_skill`); a directory inside those roots returns its entries. The
|
||
boundary itself is unchanged.
|
||
|
||
- **A failed or cancelled run keeps its steps** — only a completed run saved
|
||
its tool trail, so the Web history showed "0 steps" for exactly the runs
|
||
someone would want to inspect.
|
||
|
||
- **A no-progress stop says which calls produced nothing, and why** — each
|
||
failing or repeated call is listed with its reason and count ("read_file
|
||
failed (… not found) ×3"), instead of a message about previous summaries
|
||
and artifact paths written for a different failure.
|
||
|
||
- **`get_market_data` fails when no requested symbol returned data** — an
|
||
envelope holding only `_unresolved` was a success, so a run whose sources
|
||
were all down counted it as progress. It is now `error_code:
|
||
no_market_data`; a partial result is still a success, and grounding still
|
||
records every symbol as unavailable.
|
||
|
||
- **Codex (ChatGPT sign-in) keeps the system prompt, its reasoning and its
|
||
call ids** — a system message the loop adds mid-run (a retry hint, a
|
||
write-the-file reminder) replaced the whole 49,000-character system prompt
|
||
for every later request; it is now sent as a developer message, as the
|
||
Codex CLI does. Reasoning items are carried from turn to turn the way the
|
||
Codex CLI replays them, and resent without them if the backend refuses;
|
||
tool calls keep the item ids the backend issued instead of a fabricated
|
||
`fc_0`; the prompt-cache key is stable for the session instead of changing
|
||
on every request. The default Codex model is `gpt-6-sol`: `gpt-5.4` is
|
||
refused for ChatGPT accounts.
|
||
|
||
#### A backtest report is checked against the backtest
|
||
|
||
A risk-parity versus equal-weight report ended in the canned refusal in 12 of
|
||
15 replayed runs. Replaying all 26 stored drafts through the gate (880 of 882
|
||
recorded rejections reproduced) showed that four in five refused figures were
|
||
values the backtest itself had written.
|
||
|
||
- **A backtest's own output grounds the report about it** — only metrics
|
||
whose names the gate knew (Sharpe, return, drawdown, volatility, win rate)
|
||
were evidence. Every scalar of the run card, the metrics row, the risk
|
||
X-ray, the Monte Carlo validation and the rebalance summary now is (lists
|
||
such as the 1,000 simulated Sharpes are not descended, and a file other than
|
||
the metrics must match the run card's SHA-256 manifest). A per-bar table
|
||
counts only for the rows a `read_file` showed, while it is still byte for
|
||
byte what the engine wrote; a file the model wrote is never evidence. A
|
||
figure may name the run it came from — `rp`, `rp/artifacts/metrics.csv`,
|
||
`risk_parity target_positions.csv`, `rp::sortino` — and a value from one
|
||
backtest does not ground a figure declared from another. Replayed:
|
||
`value_mismatch` 196 → 1, `not_in_referenced_call` 167 → 7. On the same
|
||
runs a real value declared with its run passes 97% of the time and the
|
||
same value moved 2–10% passes 0.7%; the undeclared path was deliberately
|
||
not widened (the whole metrics row there would have passed 16% of invented
|
||
values).
|
||
|
||
- **A comparison's arithmetic is read as it is written** — a formula glued to
|
||
its labels ("等权 Sortino 1.2115 − 风险平价 1.1329") or followed by a
|
||
bracketed aside was "not arithmetic"; an operand written as a percent was
|
||
read as a whole number, so "13.28% − 12.98%" anchored on nothing and
|
||
"0.666 × (1 − 3%)" computed the wrong value; a sum of squared weights
|
||
(HHI) could not be written at all; a formula's negative operand (a
|
||
drawdown) could never anchor; and a field two runs both hold anchored
|
||
neither. All now evaluate (only a literal square or cube is accepted as a
|
||
power), and the ones that still fail are the model's own arithmetic.
|
||
A declared value may be a fraction (`1/3`) or a multiple (`5.5x`).
|
||
|
||
- **A report that still fails after a completed analysis is released with the
|
||
failing figures cut** — naming a symbol made any request a market answer,
|
||
and a market answer with no fetched price was never released cut. A
|
||
completed analysis now is; a price question with neither prices nor an
|
||
analysis still gets the refusal, and the refusal after an analysis no
|
||
longer says the draft cited prices.
|
||
|
||
- **A draft ending in DeepSeek's closing tool-call tags is caught** —
|
||
`</||DSML||invoke>` slipped past the pattern for opening tags and reached
|
||
the gate as unreadable declaration lines.
|
||
|
||
- **A correction says what went wrong with a figure the model did declare** —
|
||
`0.3897` and `38.97%` stay two assertions, and so do `−1.51pp` and a formula
|
||
that computes `+1.51`, but the model is now told which value it declared
|
||
(also when it has the opposite sign) and that its formula runs the other way
|
||
round, instead of "not declared" or "its own note evaluates to 1.51%". In a
|
||
live run after the evidence fix, eight differences were still cut because
|
||
the second draft rewrote the numbers rather than the formulas. The
|
||
correction also asks for the corrected answer alone, in the user's
|
||
language: that draft had opened with "The rejection was because…", in
|
||
English to a user writing Chinese, and the sentence was released.
|
||
|
||
#### The grounding gate
|
||
|
||
- **A tail-risk figure names its own field once a session holds more than one**
|
||
(#1425, after #1444, thanks @he-yufeng). A call- or tool-scoped ref (`ref x1`)
|
||
pooled every tail-risk field that call returned, and an undeclared tail-risk
|
||
percent pooled every tail-risk value in the session, so an ES 95% could quote
|
||
the VaR 95% value and pass. The rule is read off the evidence, never off
|
||
prose: `tail_risk_identity()` takes (measure, confidence) from the field name
|
||
a tool returned, so `var_95` and `es_95` are two identities while `cvar_99`
|
||
and `es_99` are one. When a scope holds two or more and the figure's value
|
||
matches one of them, it is refused with `tail_risk_needs_field_ref`, whose
|
||
correction names every identity the session holds; a figure matching no
|
||
tail-risk value keeps its old reason, so a fabricated number is still a
|
||
mismatch rather than a missing ref. Accepted cost, stated in the issue: an
|
||
undeclared VaR 95% beside a VaR 99% is correct today and now costs one
|
||
correction round. The system prompt states the rule so a risk report declares
|
||
the ref on its first attempt.
|
||
|
||
- **Tail-risk figures keep their identity under a field ref** (#1444, thanks
|
||
@he-yufeng). A declared `ref` may name an evidence field by its full path
|
||
(`data.tail_risk.var_95`) or trailing part (`var_95`), or one call's field
|
||
(`q1::historical_var`); a VaR 99% quoting the 95% value, or ES quoting VaR, is
|
||
rejected under such a ref. A field two calls returned with different values is
|
||
ambiguous and the correction names the refs to choose from. A claim scoped to
|
||
a whole call or not declared is still matched against every tail-risk value it
|
||
can see (#1425 stays open).
|
||
|
||
- The grounding gate reads decimal commas (#1517, thanks @zeus229): `17,93 %` and
|
||
`1.410,00 CNY` ground against tool results, two unspaced numbers such as
|
||
`1400,1777` stay two numbers, and a written figure must match its evidence
|
||
within half a unit of its last written digit (a truncated `30.20%` for
|
||
30.2052% is now sent back for correction). One shape regex added (14 → 15).
|
||
|
||
- **A redacted answer keeps its row numbers and its labels** (#1471). When the
|
||
grounding gate released a draft with its unverified figures cut, a ranked
|
||
table came back with `(omitted※)` in place of 1, 2, 3, of the `12m` in its
|
||
headers and of "3 names" in the prose. Every integer in a table cell was a
|
||
measurement, so the rank column was cut; and each cut figure's digits were
|
||
then swept out of the rest of the page, single digits included. Three shape
|
||
rules, no word list: a column whose numbered cells read 1, 2, 3 … in row
|
||
order is the table's index; a plain integer in a cell that also holds a
|
||
word (`12m mean return`, `Mean beta (9 reported)`) is read as it would be
|
||
in a sentence, while a cell that holds only a number, any decimal, percent
|
||
or currency figure, and every cell under an OHLC column stay measurements;
|
||
and the sweep never keys on a single digit. An integer price of an
|
||
instrument quoted in the thousands is still checked inside a worded cell.
|
||
|
||
- **A real VaR result grounds the VaR it returned** (#1464). `quantlib_call`
|
||
records a scalar result under the function name. For `historical_var` and
|
||
`parametric_var` that name reduces to `var`, which the grounding gate accepts
|
||
only as a whole field name, so the result recorded no evidence and a correct
|
||
answer quoting it was rejected. Both names are now tail-risk aliases.
|
||
Variances such as `residual_var` stay unmapped.
|
||
|
||
- **Plain integers in prose are not price claims.** A list number ("输出原则
|
||
4"), a window length or a count was checked like a quoted price and could be
|
||
cut from a released answer.
|
||
|
||
- **A drawdown against an observed high passes wherever the endpoints are
|
||
written.** "较 5 月高点 1.053 元已回撤约 37%" was rejected when its endpoints sat
|
||
on another line; a `derived` declaration does not depend on layout.
|
||
|
||
- **Price-denominated indicator values are evidence, by registration.**
|
||
Moving averages, Bollinger bands and similar levels returned by
|
||
`technical_indicators` are registered per tool as observed price evidence;
|
||
RSI, MACD and other non-price leaves are not, and nothing is decided from a
|
||
field name's words.
|
||
|
||
- **A year-less date is a date, and "24.6M" is 24.6 million.** Two real runs
|
||
that followed the contract were still released redacted: "2026-09-11 /
|
||
09-14" in a table cell shipped as "2026-09-11 /(略※)-(略※)", and
|
||
"24.6M → 22.6M lots" as "(omitted※)M → (omitted※)M". A zero-padded MM-DD,
|
||
or one opened by a full date in the same cell or line, is structure (an
|
||
unpadded "| 11-12 |" is still checked, since it may be a price range). K / M
|
||
/ B / 千 / 万 / 亿 glued to a figure scale its comparison with the evidence,
|
||
never its shape, and a cut removes the mark with the figure. Replayed on
|
||
both runs' tool results, each second draft now passes as written.
|
||
|
||
- **Metadata counts are not metrics, and a tool's tail-risk result can ground
|
||
a VaR** (#1420, #1426). `return_observations`, `n_returns`, `return_window`,
|
||
`vol_lookback`, `max_drawdown_duration`, `aligned_days` and their `*_window` /
|
||
`*_lookback` / `*_obs` / `n_*` / `*_count` / `*_days` / `*_duration` families
|
||
no longer ground a percentage ("年化收益 81%" from 81 observations); `var`,
|
||
`var_95`, `var_99`, `cvar`, `es` and `expected_shortfall` leaves are tail-risk
|
||
evidence, so a correct VaR is no longer refused. A field's metric comes from
|
||
the head of its name, so `sharpe_sample_size` and `drawdown_threshold` are
|
||
no longer a Sharpe ratio or a drawdown.
|
||
|
||
- **A correction names the prints of the figure's own instrument** (#1433). The
|
||
"nearest observed" values come from that symbol (and its table column or
|
||
`ref`), not from every field of every symbol in the run.
|
||
|
||
- **A number is read the way the chat renders it** (#1418). Zero-width
|
||
characters, a full-width or Arabic decimal point, a no-break space before "%",
|
||
the U+2212 minus sign, a backslash escape ("0\\.888"), an HTML entity and an
|
||
ISO code glued to the digits ("CNY0.888") no longer split a price into
|
||
unchecked integers; a decimal comma ("0,666 CNY", "12,5 %") is a decimal while
|
||
a valid grouping ("-1,250.00") stays grouped, and an answer that writes a
|
||
marked decimal comma reads "2,237" and "−5,132%" beside it as decimals too.
|
||
Fences follow CommonMark, and an unterminated code fence no longer exempts the
|
||
rest of the answer.
|
||
|
||
- **A year is not a price shield.** "$2050", "1999 元" and a 2031 under a close
|
||
column are checked; a price placed in a date or code column is checked; a
|
||
compact date, a dotted date and a time are structure. 円 and 원 are currency
|
||
marks, and "52pp" / "5200bp" are measurements.
|
||
|
||
- **A declaration cannot relocate or launder a figure.** A figure the sentence
|
||
writes about one instrument cannot be declared another's close; a
|
||
currency-marked result cannot be derived from an RSI or a volume; `count`,
|
||
`derived` and `proposed` cannot sit in a price column.
|
||
|
||
- **A general answer with no tool evidence is cut, not refused.** A question
|
||
about no instrument ("印花税怎么收") whose figures the gate cannot check is
|
||
released with those figures omitted and a footnote, instead of a refusal
|
||
about prices it never mentioned; a market answer that observed no price
|
||
still falls back. A malformed figures block no longer forces the fallback
|
||
either: the block is dropped and the draft is checked as written.
|
||
|
||
- **The tool-call-syntax fallback reaches the chat once.** When a model answered
|
||
the forced-text iteration with tool-call markup in a run whose output is
|
||
buffered, the replacement message was streamed by its own branch and again as
|
||
the released answer, so the chat and the CLI showed it twice.
|
||
|
||
- **No unchecked number reaches the chat before the gate runs.** An unbuffered
|
||
answer (no instrument asked about, no tool evidence yet) streamed its first
|
||
draft live, numbers included, and a rejection then replaced it; the stream now
|
||
stops at the first measurement-shaped number, and a rejected draft's shown
|
||
prefix is cleared with `stream_reset`.
|
||
|
||
- **A trading plan in a bare code fence is checked.** Only a fence tagged with a
|
||
language holds code; an untagged fence is read as prose.
|
||
|
||
- **An integer price of an instrument quoted in the thousands is checked**
|
||
("600519.SH 最新收盘 1520"), within half to twice its observed range; a
|
||
window such as "200 日均线" stays unchecked.
|
||
|
||
- **A price declared `count` is checked.** A number inside its instrument's
|
||
observed price range may be a `count` only when a declared derivation uses it
|
||
as a factor.
|
||
|
||
- **A redaction footnote the model wrote itself is removed** before the real
|
||
cuts, so a released answer never carries two contradictory footnotes or a
|
||
marker for a cut that did not happen.
|
||
|
||
- **A figure about a second instrument is checked against that instrument's
|
||
evidence** (#1584, thanks @zeus229) — symbol resolution saw only
|
||
price-comparable records, so a line naming an instrument the session held
|
||
only fundamentals for took the answer's primary symbol and failed as
|
||
`not_in_referenced_call`. Any observed evidence with an explicit symbol now
|
||
resolves it; `47a036d6` scopes analysis metrics likewise, so a metric from a
|
||
call about another symbol, or several, cannot ground it.
|
||
|
||
- **A field ref may contain `|`, and a correction for an ambiguous field lists
|
||
the exact refs to use** (#1586, thanks @zeus229) — the `figures` parser
|
||
split on every `|`, cutting such a ref short; a declaration is four columns
|
||
now, the last taking the rest of the line. When several calls returned a
|
||
field, the correction lists each valid `call_id::field` (only the figure's
|
||
own symbol's since `47a036d6`), and a `tool_name::field` ref is refused as
|
||
`field_ref_needs_call_id` with that list.
|
||
|
||
- **A decimal-comma answer's dot-grouped integers are read as thousands**
|
||
(#1588, thanks @zeus229) — `AR$5.165; volumen 502.408` was read as two
|
||
decimals and failed as `figure_undeclared`. Once a document writes a decimal
|
||
comma, now including a `0,` with two or more digits, `5.165` is 5165 and is
|
||
still checked; without that evidence it stays 5.165. The PR's wider 1%
|
||
rounding band was not kept (`47a036d6`): at 0.5%, 0.82467 has to be written
|
||
`0,825`, not `0,82`.
|
||
|
||
- **Figures from `technical_indicators` keep the date the tool reported**
|
||
(#1590, thanks @zeus229) — the tool dates its snapshot as `latest_date`, a
|
||
key evidence ingestion did not read, so `latest_close`, the indicators and
|
||
the volume metrics landed in `grounding_evidence.json` with
|
||
`timestamp: null`. It is inherited now, below a row's own date fields and
|
||
above `as_of`; an undated payload stays undated.
|
||
|
||
#### Market data
|
||
|
||
- **`get_market_data` asks for `AAPL.US` instead of returning empty frames for
|
||
a bare US ticker** — only codes the market detector reads as US equities
|
||
are refused; a Chinese futures code (`RB0`, `IF2412`) or a joined crypto
|
||
pair (`BTCUSDT`) is served as written. The public-source health canary
|
||
probes Yahoo with `AAPL.US` for the same reason.
|
||
|
||
- **A mixed-market backtest falls back per market** — a symbol missing from
|
||
its primary source was retried along the fallback chain of the first
|
||
symbol's market, so a Hong Kong gap in an A-share-led universe was never
|
||
retried on a Hong Kong source. Each missing symbol now follows its own
|
||
market's chain, and the incomplete-data error lists which symbols were
|
||
served.
|
||
|
||
- **A dollar-quoted BYMA or TSX line, or a Hong Kong RMB or USD counter, no
|
||
longer enters a single-currency backtest** (#1576, after #1543, thanks
|
||
@Shizoqua, @zeus229). Both venues list USD lines beside their home-currency
|
||
ones. On 2026-09-24 Yahoo declared USD for GGALD.BA (4.20), SPYD.BA and
|
||
AAPLD.BA, and for DLR-U.TO and XUS-U.TO. `ar_equity` and `ca_equity` are each
|
||
one static-currency pool, so such a line was priced as pesos or Canadian
|
||
dollars. As on the LSE, the loaders now read the declared currency. They admit
|
||
a `.BA` line only in ARS and a `.TO` / `.V` line only in CAD. A trailing `D`
|
||
does not decide it: YPFD.BA is a peso line. Every other declared quote
|
||
currency is recorded in market-data provenance, and the grounding gate asks an
|
||
answer to name that currency rather than the one the suffix implies. Hong Kong
|
||
is decided by the code, because the Hong Kong chain starts with sources that
|
||
declare no currency: HKEX's Stock Code Allocation Plan assigns 80000-89999 to
|
||
products traded in renminbi and several 09xxx ranges (plus 10900-10999 and
|
||
41500-41599) to products traded in USD. So an RMB counter (80700.HK, 375.40
|
||
CNY on 2026-09-24) or a USD ETF (9834.HK) is refused in a Hong Kong or
|
||
cross-market backtest, is counted in its own currency by the mixed-currency
|
||
guard, and is quoted in that currency by the grounding gate, whichever source
|
||
served it. The table matched the currency Yahoo declared for all 24 codes
|
||
probed across the ranges.
|
||
|
||
- **Grounding identity and symbol search know every market the data layer
|
||
routes** (#1565, #1575, thanks @Shizoqua). `.BA`, `.L` and `.VN` symbols had no
|
||
canonical-symbol scan, venue or currency in grounding. Symbol search
|
||
labelled Indian, Korean, British, Vietnamese and Argentine results `global`.
|
||
A parity test now takes the market list from the backtest's own currency
|
||
table. An answer may write £, ₫ or AR$ for the currency it has to name.
|
||
|
||
- **A `local:` code in a backtest is served from your own dataset or not at
|
||
all** (#1467). The market-data tool and the README already treated
|
||
`local:AAPL.US` this way, but the backtest runner did not. With
|
||
`source="local"` it counted the served `AAPL.US` as missing and sent it down
|
||
the A-share network fallback chain. With `source="auto"` it ignored the
|
||
prefix and fetched the symbol from network loaders. The prefix now picks the
|
||
local loader only, a symbol the dataset lacks fails with
|
||
`incomplete data for source=local`, and the rest of the run sees the bare
|
||
symbol: market rules, artifacts (`ohlcv_AAPL.US.csv`, no colon in the file
|
||
name) and the run card. A `local:` code requested from a network source, or
|
||
one symbol requested both with and without the prefix, is refused up front.
|
||
|
||
- Block trades, margin trading and financial statements no longer read
|
||
Eastmoney rejecting a stale query (code 9501) as an empty result; all six
|
||
datacenter callers share `eastmoney_client.datacenter_rejection`.
|
||
|
||
- Lockup expiry: `free_shares` is the unlocking quantity
|
||
(`CURRENT_FREE_SHARES`), not the float; units are stated (万股 / 万元) (#1513,
|
||
#1501, thanks @lorenzozanee). The report is `RPT_LIFT_STAGE`; the old one
|
||
rejected six columns.
|
||
|
||
- Dragon-tiger seats come from the buy/sell detail reports, surface a provider
|
||
rejection, and rank within each listing reason (#1512, #1502, thanks
|
||
@lorenzozanee).
|
||
|
||
- Shareholder-count history returns real periods from `RPT_HOLDERNUM_DET`,
|
||
states each row's previous period, and warns when the average-holding join
|
||
fails (#1518, #1503, thanks @he-yufeng).
|
||
|
||
- QVeris resolves adjusted fields as a complete set, one family per response,
|
||
and no longer gives an unadjusted bar the adjusted volume (#1527, #1494,
|
||
thanks @cgycorey).
|
||
|
||
- **Tencent history walks backward from the end date** (#1411, closes #1410,
|
||
thanks @shadowinlife) — `fqkline` serves the *last* 500 bars of a window,
|
||
not the first: `sh000300` over 2018-01-01..2026-06-30 comes back as 500 bars
|
||
starting 2024-06-06 (verified live). The forward walk that shipped in 0.1.15
|
||
(#1154) therefore returned only the last 500 bars of any longer window, with
|
||
exit code 0. The cursor now moves back from `end_date`, and an error-shaped
|
||
or mid-walk empty reply is retried instead of being read as the start of
|
||
history.
|
||
|
||
- **A BaoStock server that stops answering no longer hangs the fetch** (#1615,
|
||
closes #1492, thanks @he-yufeng) — baostock 0.9.3 reads its socket with no
|
||
timeout, so a silent server blocked forever and a closed connection spun at
|
||
100% CPU. The connection, each complete message and the wait for the session
|
||
are now bounded by `VIBE_TRADING_BAOSTOCK_TIMEOUT` (30 seconds by default),
|
||
and a failed call is a per-symbol miss the chain moves past. After review,
|
||
BaoStock sessions also run one at a time per process, because the SDK keeps
|
||
one global socket, and a socket that failed mid-reply is closed so the next
|
||
query cannot read its late answer.
|
||
|
||
- **Concurrent first lookups no longer find an empty loader registry** (#1405,
|
||
closes #1403, thanks @alanwilhelm) — `_ensure_registered` marked itself done
|
||
before importing the loader modules, so a thread arriving during a cold
|
||
start returned with nothing registered and an available symbol came back
|
||
`_unresolved`. The flag is published after the imports now, under a lock,
|
||
and concurrent cold callers wait for them.
|
||
|
||
- **An explicit source that must not fall back no longer has its gaps filled**
|
||
(#1441, thanks @chiww) — `local`, `fmp`, `qveris`, `tickerall`, `nobitex`
|
||
and `wallex` were kept from degrading only when the loader was unavailable
|
||
as a whole. A symbol the named source lacked was still fetched down the
|
||
market's chain, so one run could mix a local snapshot with Tencent bars
|
||
behind two log lines. The run now fails naming the missing symbols, pinned
|
||
by a test for every source in that set.
|
||
|
||
- **The Stooq challenge warning names a variable that exists** (#1402, thanks
|
||
@chiww, thanks @chiww) — it told operators to set
|
||
`VIBE_TRADING_MARKET_DATA_ORDER_*`, which nothing reads, or to drop stooq from
|
||
the chain, which override validation refuses. It names `MARKET_DATA_ORDER_*`
|
||
now (for example `MARKET_DATA_ORDER_US_EQUITY`), suggests moving stooq to the
|
||
end, and says an override must be a permutation of the default chain.
|
||
|
||
- **QVeris no longer sends the end date as `indicators`** (#1496, thanks
|
||
@lorenzozanee, thanks @lorenzozanee) — end-date parameters were recognised by
|
||
substring, so any capability parameter containing `to`, such as `indicators`,
|
||
was given the end date. Only exact names are mapped now (`end`, `end_date`,
|
||
`enddate`, `to`, `to_date`, `until`, `until_date`, with hyphens folded). The
|
||
rest of #1494 was fixed separately.
|
||
|
||
- **`get_shareholder_count` answers again** (#1490, closes #1489, thanks
|
||
@cgycorey, thanks @cgycorey) — it asked `RPT_HOLDERNUMLATEST` for
|
||
`AVG_HOLD_AMT`, a column the report no longer has, so Eastmoney rejected the
|
||
whole request and the tool reported "no shareholder-count disclosure found"
|
||
for every A-share. It requests `AVG_MARKET_CAP`, the same per-account value
|
||
(the output key `avg_hold_amount` is unchanged), and a rejected request now
|
||
returns the datacenter's own message instead of an empty result.
|
||
|
||
- **`get_market_data` no longer answers `1M` with one-minute bars** (#1487,
|
||
closes #1480, thanks @shadowinlife) — intervals were matched through an
|
||
upper-case fold under which `1m` (a minute) and `1M` (a month) share a key,
|
||
so a monthly request was served minute bars without an error. An exact-case
|
||
match now wins before any fold. The PR refused `1M` and `1W`; since #1479,
|
||
in this same release, they return monthly and weekly bars built from daily
|
||
ones.
|
||
|
||
#### Backtest runner and engines
|
||
|
||
- **Backtests align calendars at any timestamp resolution** (#1560, thanks
|
||
@Shizoqua). The engine merged symbol calendars and placed prices through raw
|
||
nanosecond integers, but a local duckdb source serves microseconds. Such a run
|
||
was dated 1970-01-21, and beside a nanosecond source one symbol's closes came
|
||
back empty. The rebalance mask had the same unit mismatch.
|
||
|
||
- **Mean-variance style optimizers size a short by its own expected return**
|
||
(#1548, thanks @Shizoqua). The optimizers scored every position as if it were
|
||
held long, so the strongest shorts (most negative drift) looked worst and got
|
||
the least capital. `BaseOptimizer.optimize` now negates a short's column
|
||
before building the context, so `mu` is the position's expected return AND
|
||
`cov` is the position covariance `D Σ D`: a long and a short of two +0.92
|
||
correlated names hedge instead of reading as correlated (the old objective put
|
||
the whole book on the long, the new one splits it 0.514 / -0.486). Risk-parity
|
||
and max-diversification contexts get the position covariance with it;
|
||
volatility-only contexts are unchanged, since std(-r) == std(r).
|
||
|
||
- **Monte-Carlo validation annualises at the venue's bars per year** (#1546,
|
||
thanks @Shizoqua). It hardcoded 252 while `bootstrap_sharpe_ci` and
|
||
`walk_forward_analysis` in the same report used the resolved value, so a
|
||
crypto (365) or forex (260) run published two disagreeing Sharpes.
|
||
|
||
- **A futures order on a negative-price bar is no longer rejected outright**
|
||
(#1547, thanks @Shizoqua). `FuturesBaseEngine` dropped the `abs(price)` its
|
||
base class relies on under `allow_nonpositive_prices`, so size came out
|
||
negative and the caller's `size <= 0` guard refused every order.
|
||
|
||
- **A backtest aborted when a funding debit left cash below zero** (#1542).
|
||
CompositeEngine and CryptoEngine subtract crypto funding from capital with
|
||
no floor, and the next open then fitted at no scale, not even an empty plan:
|
||
the open-basket search kept the full-scale plan and the run died with
|
||
"planned order … exceeds available capital", and a rebalance raised
|
||
"insufficient capital for position rebalance". Such an open is now skipped
|
||
and reported once as `insufficient_capital`, the bar's reductions still
|
||
run, and a close whose loss exceeds its margin still aborts the bar (#1274).
|
||
|
||
- **One cash-starved open is one rejection, not twenty-six** (#1470). In
|
||
`position_adjustment="hold"`, the search that scales a basket down to the
|
||
cash available re-planned every open on each 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 read as 26 lot-rounding
|
||
failures. The trial plans are silent now; a sleeve that was a real order at
|
||
full scale and left the basket is reported once, after the search, as
|
||
`insufficient_capital`, which the rebalance path's dropped sleeves now use
|
||
too. `zero_size` means the lot rule and nothing else.
|
||
|
||
- **A-shares from a source without its own engine branch run under A-share
|
||
rules** (#1431, thanks @chiww) — for such sources the runner followed the
|
||
instrument's market only for US, HK, Canadian and UK equities, so
|
||
`600519.SH` served by `local`, `tencent`, `eastmoney`, `baostock`, `mootdx`
|
||
or `sina` ran on `CryptoEngine`: no stamp tax, T+1, price limits or
|
||
100-share lots, and a perpetual funding fee charged against the position.
|
||
They route to `ChinaAEngine` now.
|
||
|
||
- **The declared interval is checked against the bars that arrived** (#1432,
|
||
thanks @chiww) — a single-market run annualised at the interval it asked
|
||
for, but a loader can serve coarser bars (the local loader cannot turn a
|
||
daily file into `1H`), and CAGR, Sharpe and volatility then scale by the
|
||
ratio. The runner compares the median bar spacing of the longest price frame
|
||
with the declared interval; past a 1.5x gap either way it annualises as the
|
||
interval the spacing matches, or from the calendar when bars four or more
|
||
days apart match none, and the report is written to the run card beside the
|
||
caliber warning. Median spacing replaced a bars-per-calendar-year count,
|
||
which measures five daily bars starting on a Monday at 456 a year.
|
||
|
||
- **Options annual return is annualised over the elapsed bars** (#1458, thanks
|
||
@Shizoqua, thanks @Shizoqua) — the options engine raised growth to
|
||
`bars_per_year / (n - 1)` where the shared `calc_metrics` uses `bars_per_year
|
||
/ n`, which exaggerated annual return in both directions, most on short runs.
|
||
Both use `n` now.
|
||
|
||
- **An India short is covered against the buy-side circuit band** (#1608,
|
||
thanks @Shizoqua) — the India engine never passed the held position's
|
||
direction to the shared band check, so covering a short, which is a buy, was
|
||
tested against the lower band a long's sale uses, and a cover at a locked
|
||
upper circuit filled. The direction is passed now, in single-market and
|
||
composite runs alike.
|
||
|
||
#### The backtest tool and what a run leaves behind
|
||
|
||
- **The backtest tool honours `VIBE_TRADING_TOOL_TIMEOUT_SECONDS` and keeps a
|
||
timed-out run's output** — the engine subprocess had a hardcoded 300-second
|
||
limit regardless of the setting (default 1,800; 0 disables it), and a
|
||
timeout discarded everything the engine had printed. The partial stdout and
|
||
stderr are now written to the run's `logs/` and the last 2,000 characters
|
||
returned with the error.
|
||
|
||
- **Runs from the `backtest` tool record `state.json`** (#1413, closes #1412,
|
||
thanks @shadowinlife) — only the agent runtime wrote it and
|
||
`refresh_strategy_evidence` refuses a run without one, so every run from the
|
||
`backtest` tool, or the MCP tool that delegates to it, was rejected as run
|
||
status unknown despite complete artifacts. The tool now records success,
|
||
failure with the exit code, or a timeout, through the same `RunStateStore`
|
||
writer; a config that fails validation records nothing, since the engine
|
||
never ran.
|
||
|
||
- **The `backtest` tool names the directory it looked in** (#1429, thanks
|
||
@cgycorey, thanks @cgycorey) — a missing `config.json` or
|
||
`code/signal_engine.py` was reported with no path, so a caller whose `run_dir`
|
||
resolved somewhere unexpected could not tell that from an empty directory.
|
||
Both errors now name the resolved directory and say where the file belongs.
|
||
|
||
- **Dict- and list-valued metrics reach the run card** (#1424, thanks
|
||
@cgycorey, thanks @cgycorey) — the card kept only scalar metrics, so
|
||
`unfilled_plan_rejections_by_symbol`, which names the rejected sleeve and
|
||
the reason, was dropped while the anonymous count beside it stayed.
|
||
Non-scalar metrics now go to a `structured_metrics` block, shown in the Run
|
||
Card tab and the Markdown card, with `metrics` unchanged; a value over 4 KiB
|
||
of card JSON, or one that cannot be serialised, is left out and named under
|
||
`_omitted`.
|
||
|
||
- **A text column in `metrics.csv` no longer drops a run's metrics from the
|
||
reply** (#1579, thanks @Shizoqua) — the session service converted the whole
|
||
row at once and swallowed the error, so a run with an explicit benchmark,
|
||
whose `benchmark_ticker` is text, lost every metric without a log line. Each
|
||
field is converted on its own now, and only the unconvertible one is
|
||
skipped.
|
||
|
||
#### Alpha Zoo
|
||
|
||
- **24 alphas emitted a value computed from a missing bar** (#1463, #1523,
|
||
#1534, #1452, thanks @Shizoqua). A comparison over an operand that is NaN
|
||
because its window holds the gap is False, and the #1463 sweep, which nudged
|
||
the bar by a tick, could not see that. A second oracle compares the gapped run
|
||
with the gap-free one. Comparison gates and `np.fmin`/`np.fmax` now mask on
|
||
their inputs' reach (`factors.base.observed_over`) rather than on their
|
||
operands' NaN, so a correlation undefined on complete data keeps its old
|
||
verdict; the six fmax/fmin alphas converted on 2026-09-18 had lost up to 87%
|
||
of their post-warmup values in a 5-symbol universe that way and are restored.
|
||
On gap-free data no finite value changed.
|
||
|
||
- **Twenty alphas no longer fill a missing input with a constant** (#1463).
|
||
A halt left a comparison reading 0 or False, a `.where(cond, 0)` reading a
|
||
flat day, or an `np.fmax` returning the other side, and the value then fed
|
||
every rolling window that reached back to the gap. Those cells are NaN now.
|
||
On gap-free data every finite value is bit-identical to before. A
|
||
perturbation sweep over all 462 alphas counts 35 still carrying a missing
|
||
bar forward, down from 55, and every one of them is accounted for: 28 are
|
||
recursive statistics (the GTJA `SMA(A, n, m)` smoothers and one running
|
||
product), which by the policy set on #1463 skip a missing observation and
|
||
continue from their last state — the operator layer's header now states
|
||
that exception, and a test pins it; 3 declare their own partial window; 4
|
||
are the sweep's own rank-tie artifact. Nothing rewarms a smoother after a
|
||
gap, and the registry's look-back mask was measured and rejected: it hid
|
||
4,270 legitimate cells to remove 41% of the fabricated ones.
|
||
|
||
- **Asset growth is year over year on the daily panel** (#1564, thanks
|
||
@Shizoqua). Derived fundamentals run on a panel densified to the price
|
||
calendar, where a one-row lag compared yesterday with today: 0% on ordinary
|
||
days and a one-day spike at each filing. The lag is now 365 days, as-of.
|
||
|
||
- **A factor's IC ratio is unset for a negative baseline** (#1549, thanks
|
||
@Shizoqua). Dividing two negative IC means gives a positive ratio, so a
|
||
rolling IC that got further below zero reported as improvement. The signal
|
||
itself took the worst of the available metrics and was not fooled; the
|
||
published number was.
|
||
|
||
- **qlib158 `CNTP` / `CNTN` / `CNTD` stop counting a missing close as a flat
|
||
day** (#1459, thanks @cgycorey) — the 15 factors (three families × 5 / 10 /
|
||
20 / 30 / 60 windows) turn `close > prev close` into 0/1 and average it,
|
||
and a NaN comparison reads `False`. The registry mask from #1377 blanks the
|
||
gap bar itself, but every later window that still held the gap counted the
|
||
halt as a day that neither rose nor fell, so `cntd` read closer to zero
|
||
than the observed days say. The comparison is NaN now wherever either close
|
||
is missing: a gap blanks `window + 1` rows and the value returns, equal to
|
||
the gap-free run, once it scrolls out. The same rule moves the first value
|
||
from row `window - 1` to row `window`, matching each factor's
|
||
`min_warmup_bars`, because the first bar has no prior close. On sparse
|
||
panels this can trip the registry's >95% NaN guard; each factor's `notes`
|
||
states from what missing-bar fraction.
|
||
|
||
- **qlib158 `WVMA` takes the absolute return in its numerator, as upstream
|
||
does** (#1594, thanks @Shizoqua) — `wvma5` … `wvma60` computed
|
||
`ts_std(ret·v) / ts_mean(|ret|·v)`, a signed numerator over an absolute
|
||
denominator, where qlib's formula takes `Abs` on both. Every value of the
|
||
five factors changes. The finishing commit (`a5d47c20`) points the
|
||
attribution header at the upstream file that actually defines WVMA
|
||
(`qlib/contrib/data/loader.py`, MIT) instead of `handler.py`, and pins the
|
||
formula against an independent scalar oracle, a gap and all-zero volume.
|
||
|
||
- **GTJA alphas 103, 133 and 177 score a fresh extreme high, not low**
|
||
(#1604, thanks @Shizoqua) — `HIGHDAY` / `LOWDAY` count bars since the
|
||
extreme, but the port put the 0-based window index, counted from the oldest
|
||
bar, in their place. Alpha 103 read a low set today as 5 and a low 19 bars
|
||
old as 100: the recency signal was inverted. The PR restored the direction
|
||
one bar off (95 and 0). The finishing commit (`0244ecea`) counts days since
|
||
the extreme with today as 0, so alphas 103 and 177 run from 5 to 100 over
|
||
a 20-bar window, and takes the most recent bar on a tie. It also corrects
|
||
`gtja191/LICENSE.md` to cite the Guotai Junan report of 2017-06-15 by Li
|
||
Chen and Liu Fubing, where the zoo had named a 2014 report.
|
||
|
||
- **A custom zoo root no longer replaces the bundled alpha of the same name**
|
||
(#1468, fixes #1465, thanks @cgycorey) — `Registry(zoo_root=...)` loaded
|
||
custom alpha files into `sys.modules` under the bundled module's own name.
|
||
Once a custom root had computed its own `qlib158/cntp5.py`, a default
|
||
`Registry()` in the same process computed the custom code too. The import
|
||
cache is keyed by module path and zoo root now; module names and relative
|
||
imports inside a custom root are unchanged. No shipped caller passes
|
||
`zoo_root`; it is the path tests and plugins use.
|
||
|
||
#### Quant Library and analysis tools
|
||
|
||
- **A strict-bench OOS split needs two IC observations on each side** (#1559,
|
||
thanks @Shizoqua). A split outside the loaded prices is refused. An alpha
|
||
whose IC series leaves fewer than two observations on either side is skipped,
|
||
with the counts in the reason. The IC series ends one forward-return horizon
|
||
before the prices, so a split one bar from the end used to publish verdicts
|
||
measured on nothing.
|
||
|
||
- **Quant inputs are checked before they are used** (#1555-#1558, thanks
|
||
@Shizoqua). HRP aligns a supplied correlation matrix, and the covariance's own
|
||
rows, to the covariance's column labels; swapping two rows used to move the
|
||
weights with no error. Purged cross-validation refuses an unordered or
|
||
duplicated timestamp index, and a label that ends before it starts. Impact
|
||
models and every fixed-income entry point refuse NaN and infinities instead of
|
||
returning NaN prices, durations and curves.
|
||
|
||
- **Shadow-account overtrading uses the whole trading window** (#1563, thanks
|
||
@Shizoqua). A long hold that closed before later short trades no longer
|
||
shrinks the span.
|
||
|
||
- **Event-study z-statistics no longer over-reject** (#1466). A CAR's standard
|
||
error summed each day's variance, ignoring that every day is forecast with
|
||
the same estimated parameters, so the days' abnormal returns are correlated.
|
||
On null data, the standardised CAR's variance was 1.11 with a 120-day
|
||
estimation window and 1.41–1.46 with a 30-day one. It is now 1.02 and 1.07,
|
||
which is only the t correction for an estimated residual variance. Patell and
|
||
BMP inherit the fix; `market_adjusted` was already right.
|
||
|
||
- Shadow account: an `other`-market rule no longer matches every symbol (#1538,
|
||
thanks @Shizoqua), and overtrading PnL no longer double-counts trades already
|
||
explained (#1475).
|
||
|
||
- Quantlib: comps refuses negative bridge magnitudes like the DCF bridge (#1535,
|
||
thanks @Shizoqua); accrued interest is exact on coupon dates under 30/360 and
|
||
rejects an unknown day count there too (#1509); Kyle's lambda fits an
|
||
intercept (#1477).
|
||
|
||
- ML strategy: a single-class training window is skipped and the previous model
|
||
keeps predicting (#1522, thanks @Shizoqua). Memory: two entries with one title
|
||
and different types keep separate index rows, and `forget` names the type when
|
||
ambiguous (#1525). Swarm run and task files use unique temp names (#1536).
|
||
|
||
- **Patell z uses each normal-return model's own degrees of freedom** (#1449,
|
||
thanks @Shizoqua) — the `df / (df - 2)` correction in `event_study`
|
||
assumed the market model's two estimated parameters for every event, so
|
||
`market_adjusted` (none estimated) and `mean_adjusted` (one) over-stated
|
||
the variance and reported a slightly smaller |z|, and a larger p-value,
|
||
than their own fits support. The market model is unchanged. The finishing
|
||
commit (`844b13b9`) has `estimate_market_model` read its residual degrees
|
||
of freedom from the same table, so the two cannot drift apart.
|
||
|
||
- **Microstructure estimators refuse NaN and infinite inputs** (#1451,
|
||
thanks @Shizoqua) — a non-finite price came back from
|
||
`roll_effective_spread` as a `0.0` spread, `kyles_lambda` returned `nan`,
|
||
and `vpin` produced a series from NaN or negative volumes. All three raise
|
||
now, and `vpin` also refuses a non-finite `bucket_size`.
|
||
`amihud_illiquidity` drops an infinite row the way it already dropped a NaN
|
||
one; before, an infinite return made the ratio infinite and an infinite
|
||
volume counted as a zero-illiquidity day. The accept side is pinned too
|
||
(`c6010208`): constant prices are a zero spread, all-zero order flow a zero
|
||
lambda, and zero-volume bars leave VPIN unchanged.
|
||
|
||
- **Copula pseudo-observations give ties one rank and leave NaN missing**
|
||
(#1453, thanks @Shizoqua) — the double `argsort` ranked tied values by
|
||
position (`[1, 1, 3]` became 0.25 / 0.5 / 0.75) and sorted a NaN last,
|
||
handing it the column's top rank and counting it in the normaliser.
|
||
`pseudo_observations` uses average ranks now, keeps a NaN as NaN with each
|
||
column normalised by its own count of observations, and refuses infinite
|
||
values.
|
||
|
||
- **`spread_term_structure` keeps tenor `5` and `5.0` in one column** (#1507,
|
||
thanks @Shizoqua) — the column name was built from the raw key, so issuers
|
||
quoting the same tenor as an int and as a float were split across
|
||
`5Y_spread_bp` and `5.0Y_spread_bp`, each NaN for the other group. The
|
||
reference-curve lookup already treated the two as one tenor.
|
||
|
||
- **Purged cross-validation refuses a timestamp label index it cannot map**
|
||
(#1556, thanks @Shizoqua) — label end times passed as a Series are turned
|
||
into positions with `searchsorted`, which assumes a sorted index. An
|
||
unordered or duplicated index mapped label ends to the wrong rows with no
|
||
error, so the purge protected the wrong rows; it raises now. The finishing
|
||
commit (`250ef746`) closes the neighbouring gap: a timestamp label ending
|
||
before its own start was clipped up to it (ends at positions `[1, 0, 3, 3]`
|
||
came back as `[1, 1, 3, 3]`) and now raises, as the positional form already
|
||
did. A `NaT` end stays allowed and purges through the last observation.
|
||
|
||
- **Market-impact models refuse infinite inputs** (#1557, thanks @Shizoqua)
|
||
— NaN was already refused, but positive infinity passed every positivity
|
||
check: an infinite ADV priced a `linear_impact` or `sqrt_impact` order at
|
||
zero impact, and an infinite price, traded volume or `bps` returned an
|
||
infinite buy fill. Price, volume, ADV, `bps`, impact coefficient,
|
||
volatility and `eta` must all be finite now.
|
||
|
||
- **The correlation-analysis cointegration examples call the Quant Library**
|
||
(#1404, thanks @Shizoqua) — the skill's Engle-Granger example carried its
|
||
own statsmodels fit and raised `KeyError` on an unnamed Series, because it
|
||
looked the hedge ratio up by the series' name. The PR named the regressor;
|
||
the finishing commit (`bd29a187`) replaced the local fit with
|
||
`find_hedge_ratio`, `cointegration_test` and `adf_test` from
|
||
`src.quantlib.timeseries`, and the half-life example with
|
||
`compute_half_life`. The example now inherits their refusal to pair two
|
||
legs on different indices positionally, keeps its result keys and adds
|
||
`half_life`.
|
||
|
||
- **`portfolio_risk_xray` works on windows longer than 250 bars** (#1462,
|
||
closes #1461, thanks @he-yufeng) — the tool fetched prices under the shared
|
||
250-row cap. Above it `fetch_market_data` returns a sampled envelope rather
|
||
than the bar list the tool parses, so a window of 251 bars or more failed
|
||
with "no close prices returned for any requested symbol", which is about
|
||
anything over a year of daily bars. It requests every row now
|
||
(`max_rows=0`, as `technical_indicators` does); volatility, VaR/ES and
|
||
drawdown need consecutive bars regardless.
|
||
|
||
- **`benford_check` reads the leading digit from the number as written**
|
||
(#1591, thanks @Shizoqua) — `financial_rigor`'s Benford check took the
|
||
digit from `10 ** frac(log10(v))`, which lands just under an integer for
|
||
some exact round values, so `5000` counted as a leading 4. Among the
|
||
integers 1 to 100,000, twelve were misread (8, 30, 40, 50, 80, 500, 600,
|
||
900, 5000, 6000, 9000, 50000). The PR rounded before truncating, which
|
||
would have read `4999.9999999` as a 5; the finishing commit (`d0ef98a6`)
|
||
takes the first significant digit of a `Decimal` built from the supplied
|
||
value instead, with no float round-trip. Zero, NaN and infinite values are
|
||
still skipped.
|
||
|
||
- **The sentiment scorer reads "low" as bearish only** (#1448, thanks @Shizoqua,
|
||
thanks @Shizoqua) — "low" was in both the bullish and the bearish word lists,
|
||
so it cancelled itself and `shares tumbled to a new low today` scored neutral.
|
||
It was the only word in both; a test now keeps the lists disjoint.
|
||
|
||
- **Head-and-shoulders detection checks shoulder symmetry on negative
|
||
series** (#1456, thanks @Shizoqua) — the `pattern` tool divided the
|
||
shoulder gap by the signed shoulder average. On a series below zero the
|
||
ratio went negative and always passed the 5% test, so clearly asymmetric
|
||
shoulders were flagged as a pattern. It divides by the absolute average
|
||
now, as `double_top_bottom` already did.
|
||
|
||
#### Live trading, brokers and the strategy store
|
||
|
||
- **Robinhood live trading reads and trades the account the mandate names**
|
||
(#1442). The runner, the pre-trade gate and the commit-time ceiling fetch
|
||
called Robinhood with no `account_number`, and read replies one level too
|
||
shallow (`data.positions` where Robinhood nests `data.data.positions`). So
|
||
every reconciliation aborted, and the gate refused every order because it
|
||
could not read positions. The tests stayed green only because their fake
|
||
replies had a shape Robinhood never sends. A Robinhood mandate is now bound
|
||
at commit to one account from `get_accounts`: listed, not deactivated, and
|
||
open to agentic trading. The Web confirm dialog and the CLI both ask for
|
||
it, and never take it from the agent-written proposal. Every runner and gate
|
||
call sends that account, and an order naming another account is refused.
|
||
Held positions, which Robinhood reports without a price, are priced the way
|
||
a quantity order already was, and one that cannot be priced fails the
|
||
exposure check closed. Existing Robinhood mandates must be committed again
|
||
with an account.
|
||
|
||
- **SDM decay metrics treat NaN and infinite bench results as missing**
|
||
(#1447, thanks @0xouzm) — three stored `sharpe=inf` rows read as `healthy`,
|
||
so a non-dry `sdm_decay_scan` moved a strategy in monitoring back to active,
|
||
and `-inf` read as critical. SQLite keeps infinities as written, so the
|
||
persistent store was affected too. `compute_decay_metrics` now drops
|
||
non-finite values, independently for IC and Sharpe, before the
|
||
three-observation minimum.
|
||
|
||
- **A mandate narrowed to zero exposure is stored as zero** (#1593, thanks
|
||
@Shizoqua, thanks @Shizoqua) — narrowing `max_total_exposure_usd` or
|
||
`account_funding_usd` to `0` at commit is legal, but the cap mapping used `x
|
||
or default`, so the saved mandate got the order cap back as its exposure
|
||
limit: the commit widened what the user had just narrowed. An explicit zero
|
||
now persists, and an order checked against that mandate breaches
|
||
`max_total_exposure_usd`.
|
||
|
||
- **A malformed Robinhood `buying_power` is a shape error, not an unknown**
|
||
(#1526, thanks @lorenzozanee) — a non-null `buying_power` that was not an
|
||
object (a bare number, string or list) was read as unknown and passed; it
|
||
now raises, so the portfolio source errors instead of carrying the reply.
|
||
An omitted or null value stays unknown and zero stays valid. As merged the
|
||
PR also refused an omitted `buying_power`; `46892136` restored that case to
|
||
unknown, which is what the PR description specified.
|
||
|
||
- **Interval and event triggers carry `market=None` again** (#1599, thanks
|
||
@woshi77777stars, thanks @woshi77777stars) — `Trigger.market` was both a
|
||
dataclass field and a classmethod factory, so every non-market trigger carried
|
||
the bound method as its `market`, and `dataclasses.asdict()` + `json.dumps()`
|
||
raised on it. `due_now()` was unaffected. The factory is bound after the class
|
||
body (`d0ef98a6` keeps subclass binding intact), and a repo-wide static test
|
||
fails on any dataclass or NamedTuple field shadowed this way.
|
||
|
||
- **With an MT5 profile selected, `search_symbol` resolves the broker's own
|
||
symbols** (#1597, closes #1589, thanks @lorenzozanee) — it returned no MT5
|
||
candidate for an explicit symbol such as `XAUUSD`, so the identity gate
|
||
blocked `trading_quote`, `trading_history` and `get_market_data`. Search
|
||
now reads the configured terminal's catalog by `terminal_path` with no login
|
||
credentials. After `47a036d6` every query is answered from that terminal
|
||
while MT5 is selected, a web-search hit is never substituted, and an
|
||
ambiguous alias is refused.
|
||
|
||
- **Backtests attach to the running MT5 terminal without seeing its
|
||
credentials** (#1598, thanks @he-yufeng) — the sandbox's ephemeral HOME had
|
||
no `mt5.json`, so the loader attached bare and a forex backtest could fall
|
||
through to akshare while `get_market_data` used MT5 in-process. The sandbox
|
||
now gets only `terminal_path` and `timeout` (validated by `47a036d6`), never
|
||
`login`, `password` or `server`, and a bare attach warns that the SDK may
|
||
pick another installation logged into another account.
|
||
|
||
- **Dhan paper orders reject fractional share quantities** (#1595, thanks
|
||
@Shizoqua, thanks @Shizoqua) — `0.5` passed the positive check and truncated
|
||
to `0`, returning a successful simulated fill for zero shares, and `1.5`
|
||
silently became `1`. Only whole numbers of at least one share are accepted;
|
||
`a5d47c20` parses with `Decimal`, so `1.00000000000000001` is refused too, and
|
||
NaN, infinity or text return an error instead of raising.
|
||
|
||
#### Swarm workers
|
||
|
||
- **A swarm worker can load only the skills its spec lists** (#1414, thanks
|
||
@shadowinlife, thanks @shadowinlife) — `SwarmAgentSpec.skills` was read only
|
||
to filter the prompt, so `load_skill` loaded any skill a worker named. The
|
||
worker's `load_skill` now refuses a skill outside the list and names the ones
|
||
allowed. An empty list stays unrestricted, as the prompt filter treats it; the
|
||
main agent is unchanged.
|
||
|
||
- **A swarm tool that declares `run_dir` gets the directory the model named,
|
||
confined to the agent's workspace** (#1430, thanks @cgycorey) — the worker
|
||
overwrote every call's `run_dir` with the workspace, so `backtest`, exposed
|
||
to 19 preset agents, never saw the directory it was given. A relative path
|
||
now resolves under the workspace, an absolute one inside it is kept, and one
|
||
outside is refused with an error naming the workspace. Tools that do not
|
||
declare `run_dir` still get the workspace.
|
||
|
||
- **Every swarm worker that writes its summary also writes its message log**
|
||
(#1439, thanks @cgycorey) — `messages.json` is the only record of the
|
||
arguments the model asked a tool for, and the completed, token-limit,
|
||
content-filter and incomplete exits wrote `summary.md` without it, so the
|
||
most common outcome left no log. Both files now go through one writer. A
|
||
failed LLM call still writes neither.
|
||
|
||
#### Agent loop, providers and goals
|
||
|
||
- **An explicit provider header survives an ambient twin** (#1573, #1568, thanks
|
||
@zeus229). When `OPENAI_CUSTOM_HEADERS` names a header the provider also sets
|
||
(a `user-agent` beside the Kimi / NVIDIA / OpenCode `User-Agent`), openai
|
||
3.19.2's case-insensitive merge dropped the provider's value. That turned CI
|
||
red for every PR on 2026-09-24. The first fix proposed would have sent both
|
||
headers under openai 2.53, the version the lock file and the Docker image
|
||
install. Every ambient spelling is now omitted and the explicit value re-set
|
||
after the omits, which holds under both.
|
||
|
||
- **Read-only results lost to context compaction are restored, not refetched**
|
||
(#1488, thanks @zeus229). A successful read-only call whose payload compaction
|
||
removed is replayed from the run's own cache, at most six times a run; past
|
||
the cap it runs again, as it did before, instead of being refused with "use
|
||
the previous result" for a payload the model cannot see. The cap is checked
|
||
per restore, and any successful write empties the cache, so a regenerated file
|
||
is read afresh. `read_url` opts in; `no_cache=true` always fetches.
|
||
|
||
- **OpenAI's gpt-5.6 models run with their reasoning on the agent's default
|
||
configuration** (#1473). `/v1/chat/completions` refuses function tools for
|
||
`gpt-5.6-terra` and `gpt-5.6-luna` unless `reasoning_effort` is `none`,
|
||
including when no effort is configured and the model applies its own
|
||
default, so a plain `LANGCHAIN_PROVIDER=openai` setup failed on its first
|
||
tool call with `Function tools with reasoning_effort are not supported`.
|
||
The adapter now treats that refusal the way it treats a rejected
|
||
`temperature` or `stream_options`: the request is retried on
|
||
`/v1/responses`, where the effort travels as `reasoning.effort`, and the
|
||
model is remembered for the rest of the process so later calls go there
|
||
first. Nothing changes for a model whose chat endpoint accepts tools, and
|
||
`LANGCHAIN_USE_RESPONSES_API=true` still selects the route up front and
|
||
skips the one failed request.
|
||
|
||
- **A research goal that is an order is still refused** (#1562, thanks
|
||
@Shizoqua). The execution filter no longer rejects research that mentions
|
||
shares or coins ("should the fund sell its GOOGL shares"). It refuses an
|
||
objective that opens with buy or sell, puts a quantity right after the verb,
|
||
or writes 买入 / 卖出 with a quantity and a unit. The first version of the change
|
||
had made "Buy 100 shares of NVDA" an accepted goal.
|
||
|
||
- **Research goals refuse an order that puts "now" or "immediately" before
|
||
the verb** (#1605, thanks @Shizoqua) — the execution filter matched only
|
||
the verb-then-cue order, so `Immediately buy TSLA`, `Now sell GOOGL` and
|
||
`Please immediately short TSLA` were accepted as research objectives. The
|
||
mirrored rule requires the cue to sit directly before the verb, with only
|
||
separators and an optional `please` between them. The first version allowed
|
||
up to 40 characters there and also refused `Now research whether to buy
|
||
AAPL`; the finishing commit (`0244ecea`) keeps that and similar research
|
||
phrasings accepted. The check runs on a goal's objective, on each of its
|
||
criteria and on an objective update.
|
||
|
||
- **The tail a full compaction keeps counts a thinking model's reasoning**
|
||
(#1582, thanks @Shizoqua) — the layer-3 tail is sized to a 20K-token budget,
|
||
but the sizing read `content` and tool-call arguments and skipped
|
||
`reasoning_content`, so on reasoning-heavy turns the kept tail could run far
|
||
past its budget and compaction freed less than intended. The reasoning text
|
||
is counted now, as the loop's token estimate already counted it.
|
||
|
||
- **`technical_indicators` results are restored after compaction instead of
|
||
fetched again** (#1601, thanks @zeus229) — the tool is read-only but
|
||
`repeatable`, and a repeatable tool is replayed from the run's memory only if
|
||
it opts in; it did not, so an identical request after compaction re-ran the
|
||
market-data fetch. It opts in now, like `web_reader`, within the loop's
|
||
existing replay budget.
|
||
|
||
- **Token counts and Polymarket CLOB token ids are no longer redacted as
|
||
credentials** (#1606, thanks @Shizoqua) — the `token` credential marker is a
|
||
substring match, so `token_budget`, `token_usage`, `total_input_tokens`,
|
||
`total_output_tokens` and `clob_token_id` read `[redacted]` in traces, event
|
||
previews and the live audit ledger. Since `0244ecea` each is released only in
|
||
its expected shape (a non-negative integer, a map of counts, a decimal
|
||
uint256 id); any other value under those names stays redacted.
|
||
|
||
- **A remote MCP tool with a malformed `properties` schema still gets a valid
|
||
one** (#1607, thanks @Shizoqua) — `normalize_mcp_tool_schema` dropped a
|
||
non-object `properties` such as a string or a list and then skipped the `{}`
|
||
fallback, so the tool reached the model as an object schema with no
|
||
`properties` key at all. It gets `{}` now, unless the top level composes
|
||
`anyOf` / `oneOf` / `allOf`.
|
||
|
||
- **The Copilot credential fallback re-reads the token after 60 seconds**
|
||
(#1619, closes #1617, thanks @lorenzozanee) — when neither
|
||
`COPILOT_GITHUB_TOKEN` nor `OPENAI_API_KEY` is set, `get_llm_credentials`
|
||
falls back to `GH_TOKEN` / `GITHUB_TOKEN` or `gh auth token`, and it cached
|
||
that answer for the life of the process. The cache now expires after 60
|
||
seconds. Its consumer is provider diagnostics: Copilot requests go through
|
||
the SDK client, which resolves credentials each time it is created.
|
||
|
||
#### Channels, memory and configuration
|
||
|
||
- **The Email channel verifies TLS certificates by default** (#1544, thanks
|
||
@shadowinlife). The implicit-SSL paths (`IMAP4_SSL` / `SMTP_SSL`) in both the
|
||
connection probe and the polling/send adapter used Python's default unverified
|
||
context (`CERT_NONE`, no hostname check), so the mailbox password travelled to
|
||
a server whose certificate was never checked — an active network attacker
|
||
between the operator and their mail provider 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. A new `verify_tls` field (default
|
||
`true`, surfaced in the guided Email setup in all nine locales) is the
|
||
documented opt-out for self-signed or internal-CA servers. **Upgrade note:** a
|
||
mail server with such a certificate connected before this change and now fails
|
||
the TLS handshake until `verify_tls` is set to `false` for it.
|
||
|
||
- **Plain IMAP no longer sends the mailbox password in clear text** (after
|
||
#1544). With `imap_use_ssl` off, the Email channel and its connection test
|
||
logged in over an unencrypted connection, and the guided setup put that
|
||
switch one click away. A new `imap_use_tls` (on by default, the IMAP twin of
|
||
`smtp_use_tls`) upgrades the connection with STARTTLS before `LOGIN`; a
|
||
server that offers no STARTTLS is reported as a network failure before the
|
||
password is sent. `verify_tls` now governs STARTTLS on IMAP and SMTP as well
|
||
as implicit SSL, so a self-signed server can opt out on any path. **Upgrade
|
||
note:** an `imap_use_ssl: false` setup against a server without STARTTLS
|
||
stops connecting until `imap_use_tls` is set to `false` for it, which sends
|
||
the password in plain text again.
|
||
|
||
- **Settings-write routes reject cross-site browser requests** (#1544, thanks
|
||
@shadowinlife). `require_settings_write_auth` did not apply the cross-site
|
||
guard that its siblings `require_auth` and `require_event_stream_auth` enforce
|
||
on unsafe methods, so a malicious web page could attempt a CSRF write against
|
||
any settings route — including the new `POST /channels/email/test`, which
|
||
merges stored credentials with a caller-supplied patch and could be steered to
|
||
send 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 (CLI/curl, no `Origin`) are unaffected.
|
||
|
||
- Channel adapters log through stdlib again: `{}` placeholders, napcat's 17
|
||
sites and loguru-only `.opt()` calls lost their messages (signal's
|
||
`_safe_handle` raised instead of swallowing) (#1533, #1531, thanks
|
||
@shadowinlife). Slack DMs from unapproved senders get a pairing code (#1524,
|
||
thanks @Shizoqua); Discord no longer pings `@everyone` or roles (#1537);
|
||
duplicate-reply suppression covers integer message ids and is bounded (#1476);
|
||
the WebSocket channel starts again (from #1521).
|
||
|
||
- **Two memories sharing a title keep separate index rows and links** (#1545,
|
||
thanks @Shizoqua). Under `VT_MEMORY_HIERARCHY` an entry lives at
|
||
`{memory_type}/{slug}.md`, so two entries with one title had the same
|
||
`path.name`: the second `add()` overwrote the first's `MEMORY.md` row, and the
|
||
semantic-link block beside it excluded the other entry as "self" and wrote an
|
||
ambiguous target. Both key on the path relative to the memory dir now; a
|
||
sidecar written earlier, with a bare filename or an absolute path, is still
|
||
read.
|
||
|
||
- `/agent.json`, `/agent.yaml` and `/agent.yml` are gitignored at the runtime
|
||
root; the Feishu QR login already wrote channel credentials there.
|
||
|
||
- **Signal channel: stripping the bot's mention no longer removes the wrong
|
||
character after an emoji** (#1478, thanks @Shizoqua) — Signal reports a
|
||
mention's `start` and `length` in UTF-16 code units, and the channel sliced
|
||
the Python string with them directly. Each character outside the Basic
|
||
Multilingual Plane (most emoji) before the mention shifted the slice by
|
||
one, so the mention placeholder stayed in the text handed to the agent and
|
||
the neighbouring character was cut instead. Offsets are converted to string
|
||
indices before a mention is stripped or tested as a leading mention.
|
||
|
||
- **A file received over DingTalk is saved inside its download directory
|
||
whatever it is named** (#1506, thanks @Shizoqua) — the adapter joined the
|
||
inbound `fileName` to the per-sender download path unsanitized, so a name
|
||
with `../` segments, or an absolute path (which replaces the whole path when
|
||
joined), could write outside it. The name goes through `safe_filename` now,
|
||
as the other adapters already sanitize a remote file name.
|
||
|
||
- **A channel that fails to load logs which one and why** (#1580, thanks
|
||
@Shizoqua, thanks @Shizoqua) — two channel-registry log calls had two `%s`
|
||
placeholders and one argument, so an external plugin that failed to import, or
|
||
a built-in channel skipped for a missing SDK (at debug level), printed `---
|
||
Logging error ---` instead of its name and the exception.
|
||
|
||
- **The README says which process owns Telegram polling** (#1583, closes
|
||
#1553, thanks @lorenzozanee) — Telegram allows one `getUpdates` poller per
|
||
bot token. `vibe-trading serve` owns the channel runtime, and
|
||
`channels start` asks the running server (`VIBE_TRADING_API_URL`, default
|
||
`http://127.0.0.1:8000`) to start it rather than polling itself, so the
|
||
conflict to avoid is a second server process with the same token enabled.
|
||
|
||
- **Memory garbage collection no longer compresses an entry it just archived**
|
||
(#1450, thanks @Shizoqua) — the compression pass reused the entry list read
|
||
before archiving, so an entry just moved to `archive/` was compressed from a
|
||
path that no longer existed and logged a failure. `451c554a` skips by whether
|
||
the file still exists, not by id: ids are six hex characters two files can
|
||
share, and an archive step that failed leaves its file to be compressed.
|
||
|
||
- **An unparseable optional numeric setting falls back to its default instead
|
||
of failing config load** (#1592, thanks @Shizoqua) — the coercion that drops
|
||
a bad `int` / `float` env value skipped `int | None` fields, so
|
||
`ANTHROPIC_MAX_TOKENS=not_a_number` raised while building `EnvConfig`.
|
||
Optional fields get the same fallback now; a parseable value outside the
|
||
field's bounds (`ANTHROPIC_MAX_TOKENS=0`) is still rejected.
|
||
|
||
#### Web UI and locales
|
||
|
||
- **Leaving a chat cancels its pending history scrolls** — a scroll scheduled
|
||
while a conversation's history loaded could still fire after leaving it,
|
||
and a history load still in flight could schedule another.
|
||
|
||
- **A welcome-screen quick action stays highlighted after you click it**
|
||
(#1500, thanks @shadowinlife) — the first chip was styled as selected by its
|
||
position, whatever had been clicked, so the chip you chose never lit up. The
|
||
highlight now follows the click and moves with the next one, and each chip
|
||
reports `aria-pressed`.
|
||
|
||
- **Broker SDK diagnostics no longer name Longbridge for every broker**
|
||
(#1436, thanks @tingkk) — the shared `sdk_missing` and `network_unreachable`
|
||
messages on the Runtime page and in the chat connector status said
|
||
Longbridge whichever broker raised them, so an unreachable Futu OpenD gateway
|
||
sent the user to check Longbridge. They now name the affected broker's SDK,
|
||
service or local gateway, in every locale.
|
||
|
||
## [0.1.15] — 2026-09-09
|
||
|
||
Rolls up 551 commits / 162 merged pull requests since 0.1.14, from 35
|
||
contributors.
|
||
|
||
The theme of this cycle is data that says what it is. A price frame now
|
||
carries the adjustment caliber it was served under. A factor propagates the
|
||
gaps in its inputs instead of filling them. A loader that cannot serve a
|
||
market no longer claims it, and a source that fails to refresh is an error
|
||
rather than a smaller portfolio. Three of those were the same bug wearing
|
||
different clothes: a default that silently substitutes a plausible value for
|
||
a missing one, which is indistinguishable downstream from a real
|
||
observation. Three new markets, a fourteenth broker and fifteen Quant
|
||
Library additions land alongside.
|
||
|
||
### Added
|
||
|
||
- **UK equity market** (#1206, thanks @cgycorey) — LSE `.L` and `.IL`
|
||
symbols end to end: market data, Yahoo-backed financial statements and
|
||
indicators, trade-journal inference into `uk_equity`, and its own engine
|
||
path. SDRT is modelled as what it actually is — a 0.5% duty on the
|
||
**purchase** side only, not a symmetric round-trip cost. Charging both
|
||
sides overstated the cost of every round trip by half, which is exactly
|
||
the size of edge a mean-reversion strategy lives on.
|
||
- **Zerodha Kite Connect** (#1193, thanks @ashutoshsinghpr7) — a fourteenth
|
||
broker connector for Indian equities. Kite exposes no runtime paper/live
|
||
discriminator — no account-id format, host separation, demo flag or trade
|
||
environment — so under the red line it is capped at paper plus read-only:
|
||
`place_order` and `cancel_order` hard-refuse any non-paper config at the
|
||
first line, and no `*-live-trade` profile exists to select. That now
|
||
covers Longbridge, Dhan, Shoonya and Zerodha; Trading212 goes further and
|
||
refuses all order placement including paper.
|
||
- **Read-only multi-broker portfolio** (#1072, thanks @goatyyc; onboarding
|
||
contracts in #1250) — one aggregated snapshot across every enabled
|
||
connection instance, reachable four ways: the Web `/portfolio` page, REST
|
||
under `portfolio_routes.py`, the `portfolio_summary` agent tool, and
|
||
`vibe-trading portfolio show | refresh | sources`. Three design points
|
||
that are not incidental: a source that fails to refresh is an **error
|
||
excluded from the totals** (status `error`, `last_success_at`,
|
||
`complete=false`), never a carried-forward cache, so a partial snapshot
|
||
cannot read as a smaller portfolio; every `remote_mcp` read passes
|
||
`interactive_oauth=False`, so aggregation can never pop an auth prompt;
|
||
and `analysis_context()` carries `risk_xray_args` so `portfolio_risk_xray`
|
||
is *fed* rather than reimplemented. Eligibility is one rule —
|
||
`is_portfolio_connection_profile` requires readonly **and** account.read
|
||
**and** positions.read — under which the IBKR official-MCP profile, which
|
||
advertises only discovery, is correctly not a portfolio source.
|
||
- **Quant Library, fifteen additions** (thanks @santhreal) — Heston (1993)
|
||
stochastic volatility option pricing (#1195); Hierarchical Risk Parity
|
||
allocation (#1196); Archimedean and Gaussian copula analytics (#1197);
|
||
market-microstructure analytics — VPIN, Roll spread, Amihud illiquidity,
|
||
Kyle's lambda (#1198); analytical single-barrier options with cash-rebate
|
||
handling (#1163) plus finite-difference sensitivity Greeks for them
|
||
(#1203); an ISDA standard-model single-name CDS valuation engine with
|
||
hazard-rate conversions (#1164); Key Rate Duration decomposition across
|
||
benchmark tenors (#1165); the Vasicek credit portfolio loss model and
|
||
spread DV01 (#1167); cross-sectional factor Information Coefficient time
|
||
series (#1166); Ornstein-Uhlenbeck exact calibration with half-life
|
||
(#1161); drawdown distribution analytics with the Ulcer and Pain indices
|
||
(#1162); Corrado rank and Cowan generalized sign non-parametric
|
||
event-study tests (#1160); multi-factor risk decomposition with Euler
|
||
marginal contributions, reporting `unmatched_weight` rather than
|
||
normalising it away (#1159); and group-purged k-fold cross-validation for
|
||
panel data (#1168). The layer's rule holds throughout: a formula has
|
||
exactly one implementation, and skills import it rather than carrying the
|
||
source in a `SKILL.md` code block.
|
||
- **Brazilian Portuguese locale** (#1327, thanks @nandofmike) — the eighth
|
||
shipped language, landing with full key parity. The parity suite globs
|
||
`locales/*.json` instead of listing languages, so a new locale is covered
|
||
the day its file lands; German had previously shipped with perfect parity
|
||
and zero coverage because that list was hand-written.
|
||
- **Nobitex and Wallex** (#1263, thanks @Emad211) — read-only Iranian
|
||
exchange sources on keyless public UDF endpoints, explicit-source only.
|
||
Both sit in `_NO_NETWORK_FALLBACK_SOURCES` and are barred from the crypto
|
||
fallback chain on purpose: they are the only Toman-quoted sources and
|
||
declare `markets={"crypto"}` purely to be reachable, so degrading an
|
||
unavailable `BTCIRT` request into that chain would hand back a
|
||
USDT-quoted series as if it were Toman.
|
||
- **Binance USD-M evidence path** (#1229, #1230, thanks @honginp; #1248,
|
||
thanks @lorenzozanee) — read-only USD-M account snapshots, drift evidence
|
||
artifacts comparing simulated fills against recorded ones, and
|
||
deterministic tolerance calibration derived from those recordings rather
|
||
than assumed. The point is that the tolerance has a provenance: it is
|
||
measured against real fills, not chosen.
|
||
- **Offline evals harness** (#1271, thanks @AirHua-byte) — verifies a run's
|
||
*persisted* artifacts (prompt preservation, tool calls and results,
|
||
identity, evidence, provider usage, iteration budget, `RunManifest`
|
||
integrity) against a versioned case/verdict schema, with no LLM, network,
|
||
broker or MCP call: it reads files only. Absent instrumentation emits
|
||
`NOT_EVALUABLE` rather than passing, which is the whole point — a missing
|
||
field must not read as good behaviour.
|
||
- **Anthropic prompt caching** (#1366, thanks @averatec0773) — an explicit
|
||
cache breakpoint on the system block so the static tools+system prefix is
|
||
read from cache instead of resent at full price, with cache usage recorded
|
||
in the usage envelope. Native adapter only, behind
|
||
`VIBE_TRADING_ANTHROPIC_PROMPT_CACHE`; set it to 0 if a compatible proxy
|
||
rejects the `cache_control` request parameter.
|
||
- **Per-market data-source priority** (#1231, thanks @sambazhu) — a Settings
|
||
card and `MARKET_DATA_ORDER_*` env overrides that reorder a market's
|
||
fallback chain. Validated as a multiset permutation of the default, so
|
||
reordering passes while adding, dropping or duplicating a source is
|
||
refused and the default stands. The override is applied as a setitem on
|
||
the same dict object, because `runner.py` and `market_data.py` import
|
||
`FALLBACK_CHAINS` by name and a rebind would leave them on the old order.
|
||
- **Calendar-triggered partial rebalancing** (#1277, thanks @thisisjun786)
|
||
and a **data window separated from the evaluation window**, so warm-up
|
||
bars stop counting as evaluation and a strategy is not graded on the bars
|
||
it needed to become defined.
|
||
- **Agent-confirmed scheduled research** (#1187, thanks @AirHua-byte) and
|
||
each monitor's latest verdict rendered on the job list (#1156, thanks
|
||
@he-yufeng), so a recurring job's state is legible without opening it.
|
||
- **Swarm replay and retry** (#1158, thanks @cgycorey; `vibe-trading swarm
|
||
retry --resume` in #1194, thanks @SiMinus) — resume a failed or cancelled
|
||
DAG run while keeping the artifacts of tasks that already completed,
|
||
rather than paying for the whole graph again.
|
||
- **Order-plan rejection reporting** (#1245, thanks @lorenzozanee) — an `_on_plan_rejected` hook plus a run-level report of
|
||
the trades the engine wanted but could not take. A rejected plan
|
||
previously vanished, so a strategy that was being silently throttled by
|
||
buying power looked identical to one that simply had no signal.
|
||
- **Read-only Binance-connector crypto identity** (#1242, thanks
|
||
@pengpengyi92) and public venue-catalog pair resolution, so a crypto pair
|
||
resolves without requiring a broker connection at all.
|
||
- **Multi-file drag-and-drop and paste uploads** in the Web chat (#1179,
|
||
thanks @AirHua-byte).
|
||
|
||
### Fixed
|
||
|
||
#### Missing data stops being filled with plausible values
|
||
|
||
- **The Alpha Zoo NaN contract, enforced at the registry** (#1377, closes
|
||
#1376, thanks @cgycorey) — `factors/base.py` states the contract in its own
|
||
header: NaN is preserved through warm-up and missing data, no silent
|
||
`fillna(0)`. An alpha that branches with `np.where` on a **comparison**
|
||
violates it without looking like it does, because a NaN comparison is
|
||
`False` and the ternary falls through to its constant. Blanking every
|
||
declared input on one bar across the whole zoo, **84 of 462 alphas returned
|
||
a number anyway** — and since `dropna()` is the mechanism that keeps a gap
|
||
out of an IC, each fabricated ±1 was consumed as a real signal.
|
||
`Registry.compute` now masks the output to NaN wherever a declared
|
||
dependency is missing on that bar, and the same sweep afterwards finds
|
||
**none**. Individually fixed on the way there: gtja191_004 (#1371),
|
||
gtja191_003 and gtja191_059 (#1372), gtja191_069 (#1373), gtja191_019 and
|
||
gtja191_086 (#1379), alpha101_007 and alpha101_051 (#1380), alpha101_009
|
||
and alpha101_046 (#1381), alpha101_021 and alpha101_024 (#1382),
|
||
alpha101_010/023/027 (#1383), and alpha101_049's warm-up mask (#1374) —
|
||
thanks @Shizoqua. **Stated rather than quietly carried:** the warm-up half
|
||
is still open. Measured the same way, **52 alphas still emit a value inside
|
||
their own declared `min_warmup_bars`** (`gtja191_154` declares 198 and
|
||
emits at row 0), because on those bars the inputs are present and only the
|
||
rolling window is undefined. A registry-level warm-up mask would also blank
|
||
rows for alphas whose declaration merely over-declares by two or three
|
||
bars, so that half needs a direction call rather than 52 pull requests.
|
||
- **`pct_change()` forward-fills by default** (#1397, #1398, #1399, #1400,
|
||
thanks @Shizoqua; #1172) — pandas still defaults `pct_change()` to
|
||
forward-filling, so a missing close becomes a **0.0% return that never
|
||
happened**, and the next bar reports a two-day move as one day. Four PRs
|
||
each fixed one call site; sweeping the skills exhaustively found **24 sites
|
||
across 10 files**, two of them inside the very files those PRs were
|
||
editing. The class is closed repo-wide now: the SDM strategy template
|
||
(#1397), cross-market volatility weights (#1398), multi-factor TopN
|
||
selection which now excludes assets with no factor observations rather than
|
||
ranking them at zero (#1399), and sentiment-factor orthogonalization which
|
||
no longer regresses on zeros it filled in itself (#1400). Alpha Zoo returns
|
||
stopped forward-filling gaps in #1172.
|
||
- **Survivorship-bias disclosure reaches the report** (#1289, thanks
|
||
@bonyohana) — the flag was computed and then not rendered in the HTML bench
|
||
report, which is the same failure mode one level up: a caveat that exists
|
||
in the data and not in what the reader sees.
|
||
|
||
#### Prices say which caliber they are
|
||
|
||
- **Adjustment caliber stamped on served frames** (#1317, closes #1301,
|
||
thanks @he-yufeng) — a frame now carries what its prices actually mean.
|
||
A caliber is recorded only where it was measured against a live payload or
|
||
pinned by the loader's own endpoint choice; anything unmeasured resolves to
|
||
`unknown` on purpose, because origin-side adjustment is invisible from
|
||
loader code — Yahoo serves split-adjusted quotes with zero adjustment logic
|
||
in this repo.
|
||
- **The loaders that still booked a dividend as a loss** (#1287, #1288,
|
||
thanks @he-yufeng; #1320, thanks @lorenzozanee) — Yahoo, FMP and Tiingo
|
||
paths serve adjusted prices, and an unresolved symbol now retries down the
|
||
fallback chain instead of stopping at the first source that lacks it.
|
||
- **Explicit sources answer as themselves** (#1276, #1342, #1185, #1316,
|
||
thanks @lorenzozanee, @he-yufeng, @cgycorey) — FMP moved to the Stable
|
||
endpoint and no longer silently falls back when named explicitly; stooq's
|
||
anti-bot challenge is flagged rather than parsed as data, which is the
|
||
difference between "no data" and "a JavaScript page interpreted as prices";
|
||
codes, date range and source are validated before the fetch rather than
|
||
after it; and symbol search is aligned with fetch for FX pairs and indexes,
|
||
so the thing you searched is the thing you get.
|
||
- **Tencent history pagination** (#1154, thanks @BigFishEmily) — long ranges
|
||
were silently truncated to the first page.
|
||
|
||
#### Futures reach the right engine, and now have a source at all
|
||
|
||
- **Classification** (#1369, #1389, thanks @Shizoqua; #1396, thanks
|
||
@he-yufeng; and `349bf202` on the global side) — digit-prefixed and
|
||
month-code-colliding product codes, Tushare's own exchange spellings
|
||
(`SHF`/`CZC`/`CFX`/`GFE`), case folding at extraction rather than per
|
||
lookup, and dated global contracts carrying their venue (`CL2412.NYMEX`,
|
||
`ESZ4.CME`) each fell through every pattern to the `a_share` default. A US
|
||
crude contract was therefore run under T+1, with no shorting, settled in
|
||
CNY.
|
||
- **Coverage** (#1395) — the loader chain was the last link, and it was
|
||
empty. It named `tushare` and `akshare`, and **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,
|
||
while `akshare._fetch_one` ends with `# Default: try A-share`. AKShare now
|
||
serves Chinese contracts off the token-free Sina daily endpoints, dated and
|
||
main-continuous, verified live across SHFE (RB2601, RB0), CFFEX (IF2512,
|
||
T2512), GFEX (SI2601) and ZCE (MA2605). A global contract returns empty and
|
||
reaches `local` rather than being priced as an equity. Trimming tushare's
|
||
`markets` set alone would not have worked: `resolve_loader` walks the chain
|
||
and never consults `markets`, so tushare would have stayed first in line.
|
||
The main continuous contract (`RB0`) is routed too — a dated contract lives
|
||
about one year, so any backtest longer than a contract cycle has to name
|
||
the rolled series.
|
||
- **China-futures margin provenance** (`306aca2c`, closes #1393) — a run now
|
||
states which products were priced on a generic default instead of a table
|
||
entry. `rb`'s real margin rate is 0.10, the same number as the default, and
|
||
"looked up" must not read like "assumed".
|
||
|
||
#### Live trading fails closed
|
||
|
||
- **Broker reads that are not answers** (#1212, #1232, thanks @he-yufeng;
|
||
#1244, #1254, thanks @cgycorey) — a position read returning an API error,
|
||
and error envelopes arriving mid halt-sweep, were both treated as
|
||
successful empty results. The sweep now latches only the episodes it
|
||
actually swept, under per-episode latch files, and only after attempted
|
||
side effects.
|
||
- **State that has to survive a restart** (#1233, thanks @he-yufeng; #1213,
|
||
#1221, #1222, thanks @Elfsa-Miranda) — the flatten latch persists across
|
||
runner restarts, and unresolved Alpaca submissions are owned before broker
|
||
writes and recovered by **exact client ID**, including fills, so a restart
|
||
mid-submit cannot produce a duplicate or an orphan.
|
||
- **Order sizing and timing** (#1312, #1361, #1253, thanks @he-yufeng and
|
||
@cgycorey; #1209) — buy-limit orders are sized at the worse of quote and
|
||
limit on both transports, market-triggered ticks are skipped while the
|
||
market is closed, and Alpaca position quantity is signed by side before it
|
||
reaches the mandate gate.
|
||
- **The mandate gate narrows in both directions** (#1285, thanks @Jackzigen)
|
||
— the change enforcing narrowing semantics for non-numeric and list
|
||
adjustments also rejected `"none"` as an invalid type, which made the most
|
||
conservative mandate unsubmittable. Nine CI checks were green over it: the
|
||
eight new tests were all `raises` assertions, and a one-way suite cannot
|
||
see that something which should have been allowed was refused.
|
||
- **Four connector gaps from the #1207 minor batch** (#1388, thanks
|
||
@he-yufeng) — an MT5 order above the symbol's volume cap was silently
|
||
clamped and sent while the below-minimum side errored (it fails closed
|
||
naming the cap now); an eToro limit order with no `limit_price` went out as
|
||
an MIT with no `TriggerRate` and rested untriggered while reading as
|
||
accepted; futu K-lines inherited an undeclared adjustment caliber; and bare
|
||
`BTCUSDT` was routed through A-share rules — T+1 and no shorting, on a
|
||
perpetual.
|
||
- **Report audit fails closed when nothing was verified** (#1362, thanks
|
||
@cgycorey) — an audit that checked zero claims returned PASS.
|
||
|
||
#### Backtest execution semantics
|
||
|
||
- **Options** (#1299, #1305, thanks @cgycorey; #1306, thanks @he-yufeng;
|
||
#1177) — signals fill on the **next** bar rather than on the date they were
|
||
computed from, which had let a strategy trade at the price that generated
|
||
its own signal; HV warm-up bars use the configured default IV instead of
|
||
an undefined one; short option legs hold margin and gate opens on buying
|
||
power, so a naked short could no longer sell premium it could never cover;
|
||
and an explicit expiration is validated against the dates actually
|
||
available rather than silently resolved to a neighbour.
|
||
- **Shorts, rebalancing and accounting** (#1298, #1344, #1281, thanks
|
||
@cgycorey) — 1x shorts liquidate on adverse moves in non-strict mode; an
|
||
over-committed rebalance basket scales to fit instead of aborting on a
|
||
commission overdraft; and requested target changes are separated from
|
||
executed rebalance fills (closes #1275), so the rebalance count shown to a
|
||
reader is the number of fills rather than the number of intentions.
|
||
- **Market rules through composite state** (#1309, #1332, #1370, thanks
|
||
@he-yufeng and @Shizoqua) — A-share and India rules are enforced through
|
||
composite state rather than per-engine, a halted position marks at the last
|
||
traded close instead of entry cost, and the sub-engine's active symbol is
|
||
synced before **every** composite dispatch rather than once per bar.
|
||
- **Perpetuals and forex** (#1307, thanks @he-yufeng; #1226, thanks
|
||
@P1Piyush) — funding settles by bar span on 8h+ intervals rather than once
|
||
per bar, and forex metals are treated as metals for pip size, lot size and
|
||
spreads. A 50-ounce gold order had been rounded down to zero.
|
||
- **Symbol routing into the engines** (#1351, thanks @cgycorey) — dotted US
|
||
class shares (`BRK.B.US`, `BF.B.US`) fetched an empty chart.
|
||
- **Cross-market annualization** (#1239, thanks @cgycorey) — risk x-ray
|
||
honours `bars_per_year=None` instead of assuming a trading calendar that
|
||
does not apply.
|
||
- **ML walk-forward label leakage** (#1392, thanks @Shizoqua) — future labels
|
||
were not purged from the training window in the example pipeline.
|
||
- **Intraday fundamentals lookahead** (closes #1387) — an announcement date
|
||
carries no time of day, so on an intraday frame a filing became visible
|
||
from the first bar of its own announcement day: a full session of
|
||
lookahead. Sub-daily frames are refused now, with
|
||
`fundamental_subdaily="next_day"` as the explicit opt-in.
|
||
|
||
#### Shadow account
|
||
|
||
- (#1217, #1310, #1311, #1314, #1356, thanks @he-yufeng) — PnL is derived
|
||
from runner metrics and fails closed when unknown rather than reporting a
|
||
number it cannot support; attribution is scoped to the pool currency with
|
||
mutually exclusive buckets; short lots are modelled in FIFO pairing with
|
||
legs restated across corporate actions; the result cache is keyed by window
|
||
**and** journal hash rather than by shadow alone, so editing a journal no
|
||
longer returns the previous answer; and cash-dividend journal rows are
|
||
parsed and booked into real PnL.
|
||
|
||
#### Grounding gates
|
||
|
||
- **Both directions** (#1326, #1338, #1375, #1378, thanks @cgycorey) —
|
||
full-width brackets no longer split a clause; analysis metrics are gated on
|
||
a *completed* analysis result (#1336); a metric claim formatted as a
|
||
generic `| Metric | Value |` table no longer escapes the gate that rejects
|
||
the same claim in prose; and the mirror-image error is fixed too — a price
|
||
word used as a formula variable is not a price claim, so `close/SMA50 > 1`
|
||
is no longer read as an asserted price (#1354).
|
||
- **Vocabularies completed on both sides** (#1346, #1357, thanks @saju01) —
|
||
percentage-point deltas (`3.6pp`, `3.6 个百分点`, `250 基点`) were read as
|
||
quoted prices, and the adjacent-CJK boundary is now exercised directly.
|
||
The English gate had been the leaking side: `\bclose\b` does not match
|
||
"closed", so a fabricated `The stock closed at 412.35.` passed while the
|
||
identical Chinese claim was correctly rejected. Both vocabularies are
|
||
complete now and a language-parity suite asserts the two reach the **same**
|
||
verdict per case instead of checking each in isolation.
|
||
- **Identity** (#1265, #1280, #1282, thanks @aminak58; #1384, thanks
|
||
@laiyierjiangsu; #1269, thanks @AirHua-byte; #1333, thanks @Shizoqua) —
|
||
bare joined crypto pairs (`BTCUSDT`) went unrecognised while spot platinum
|
||
resolved to a crypto pair that does not exist; FX, metals and futures
|
||
symbols route through the right identity path (`GC=F` and `XAUUSD` had been
|
||
classified as China A-shares); `search_symbol("XAUUSD")` returned exactly
|
||
one candidate — a Swedish Bitcoin ETP — which then locked the run's
|
||
instrument identity, so a metal or FX query is an exact instrument
|
||
assertion now; a pasted broker code (`HK.00700`, `US.AAPL`, `SH.600519`)
|
||
scanned as no symbol at all; market context stated earlier in a
|
||
conversation can narrow resolution behind
|
||
`VIBE_CONTEXTUAL_IDENTITY_CONSTRAINTS`; and generic evidence timestamps are
|
||
preserved rather than dropped.
|
||
|
||
#### The agent loop keeps what it still needs
|
||
|
||
- (#1341, #1358, thanks @saju01; #1349, thanks @he-yufeng; #1352, thanks
|
||
@cgycorey) — the loop discarded tool results the model was still using, and
|
||
microcompaction cleared pairs it then could not reconcile. A session that
|
||
had successfully fetched fundamentals, fund flow, margin and research data
|
||
had those results cleared to free context, and the de-duplication ledger
|
||
went on refusing to re-fetch them: **44 blocked retries in one run**, ending
|
||
in "fundamental data not retrieved" for data the model had already
|
||
received. Clearing a result now re-opens that tool, the ledger keys on
|
||
arguments rather than tool name, results are reconciled by exact call
|
||
identity, and the token budget counts tool-call arguments instead of
|
||
scoring a 100 KB payload as ten tokens.
|
||
- **The no-progress budget counts observations, not activity** (#1363,
|
||
thanks @be-student) — a run that keeps issuing tool calls without a new
|
||
successful observation stops after `NO_PROGRESS_LIMIT` iterations with a
|
||
visible recovery answer and a `failed` terminal state. An identical call
|
||
identity is refused from its **second** failure, not its first: only a
|
||
successful *mutating* call clears the ledger and a research run may have
|
||
none, so refusing on the first failure made a transient rate limit or
|
||
timeout permanent for the whole run.
|
||
- **Run-stall watchdog and bash tree-kill timeout** (#1169, thanks @wiliao)
|
||
— plus a compaction verification ledger, so a stalled run is detected
|
||
rather than waited on.
|
||
|
||
#### Providers and transports
|
||
|
||
- (#1225, thanks @he-yufeng; #1246, #1247, thanks @lorenzozanee; #1330,
|
||
thanks @averatec0773; #1334, #1348, thanks @Shizoqua; #1182, thanks
|
||
@AirHua-byte) — real token usage is requested on streamed calls instead of
|
||
being estimated; a temperature rejection self-heals on the
|
||
OpenAI-compatible path, and the Anthropic self-heal strips the *relocated*
|
||
`extra_body.temperature` rather than only the original position; stream
|
||
retry delays escalate and honour `Retry-After`; Codex token usage and
|
||
response model metadata are propagated rather than discarded; and shared
|
||
HTTP clients survive cleanup instead of being closed out from under an
|
||
in-flight request.
|
||
- **Dependency caps that were load-bearing** (#1313, thanks @he-yufeng;
|
||
`4abd6cc2`) — fastmcp is capped below 4.0.0 until the constructor/snapshot
|
||
adaptation lands, and mcp below 1.30, which broke IBKR OAuth discovery. The
|
||
second was diagnosed as a red CI on tests nothing in the diff had touched;
|
||
adapting the fixtures would have hidden a genuinely broken broker path.
|
||
|
||
#### Web UI, CLI and surfaces
|
||
|
||
- (#1257, #1258, #1259, thanks @iagop03; #1319, thanks @guestccc) — a stale
|
||
`streamingSessionId` is cleared on session reopen, the main region scrolls
|
||
and long run prompts collapse, a non-backtest run points at Studio instead
|
||
of a blank dashboard, and the Vite dev server proxies `/api` so a fresh
|
||
checkout works without a manual proxy step.
|
||
- **Session recovery across restart** (#1180, thanks @AirHua-byte; #1340,
|
||
thanks @averatec0773) — streamed assistant text is checkpointed to
|
||
`partial_response.json` at ≥0.25s intervals with an atomic `os.replace`,
|
||
and every pending or running attempt on disk is reconciled at service
|
||
construction into an explicit `interrupted` transcript entry. The reply is
|
||
committed before `attempt.json` so recovery can finish either side of that
|
||
write. The attempt start persists too, so the elapsed clock survives
|
||
navigation, reload and history.
|
||
- **Windows and desktop** (#1261, thanks @Emad211; #1284, thanks
|
||
@lorenzozanee; #1359, thanks @birdxs) — cross-platform locks and path
|
||
handling, EPERM handled in the desktop updater's shutdown checks, and
|
||
Docker actions moved to Node.js 24 compatible versions.
|
||
- **Swarm and MCP plumbing** (#1175, #1331, #1176, #1335, thanks @Shizoqua;
|
||
#1210, thanks @pengpengyi92; #1173; #1345, thanks @cgycorey) —
|
||
`cancel_run()` is threaded into the in-flight worker rather than only
|
||
marking the DAG; goal sessions are isolated per MCP connection rather than
|
||
per process; `goal_id`/`expected_goal_id` default to the current goal;
|
||
`beginTime`/`endTime` are forwarded through `get_research_reports`; worker
|
||
retries use bounded backoff; the MCP stdio registry path has coverage; and
|
||
investment-committee researchers get the fundamental data panel their
|
||
prompts already assumed (#1343).
|
||
- **Portfolio OAuth lifecycle** (#1211, #1251, thanks @goatyyc) — the
|
||
reconnect lifecycle is isolated, and reconnect failures are classified
|
||
rather than collapsed into one opaque error.
|
||
- **IBKR** (#1186, thanks @sykuang; #1190, thanks @goatyyc; #1178, thanks
|
||
@c020627) — public MCP OAuth flow support, verified read tools enabled, and
|
||
the official read-only seed pointed at `/mcp-public`.
|
||
- **Futu HKD valuations** (#1228, thanks @JaxonHu1024) and futu
|
||
`connection_state` handling.
|
||
- **Feishu QR login credentials persist** (#1188, thanks @AirHua-byte).
|
||
- **Persistent memory cleans up after itself** (#1174, thanks @Shizoqua) —
|
||
garbage collection and compression left the FTS index and the semantic
|
||
links pointing at entries that no longer existed, so a cross-session recall
|
||
could surface a row whose body had been collected.
|
||
- **Strategy store and discovery** (#1347, thanks @ethanstoner; #1368,
|
||
thanks @Shizoqua) — `created_at` ties are broken so history is really
|
||
newest-first, and position size is resolved per regime rather than once for
|
||
the whole run.
|
||
- **Valuation and credit refuse non-finite inputs** (#1184, #1215, #1386,
|
||
thanks @Robin1987China) — comps, three-statement inputs and every
|
||
credit-risk function reject non-finite values rather than propagating them.
|
||
The three Archimedean copula CDFs returned wrong extreme values under
|
||
strong dependence — Clayton `0.0`, Gumbel `1.0`, Frank `inf` — and are
|
||
computed in log space now, verified against a 2500-digit reference across
|
||
θ ∈ [-5000, 5000] (closes #1385). Note that the first fix covered only
|
||
θ > 0; θ < 0 overflowed to `nan` from |θ| ≈ 355 and the rewrite dipped
|
||
below the Fréchet bound as θ → 0, both caught before merge.
|
||
- **Cross-validation edges** (#1204, #1220, thanks @santhreal and
|
||
@he-yufeng) — multi-segment test sets are supported, empty training
|
||
partitions are guarded, and rows between combinatorial test blocks are
|
||
pinned as still trainable.
|
||
- **Skill and documentation links** (#1252, #1328, thanks @ethanstoner;
|
||
#1189, thanks @youngjincho02-arch) — a skill-relative reference link
|
||
resolves in `read_file`, the skill-name prefix is dropped so links resolve
|
||
on GitHub, and the `historical_var` quantile formula in the risk-analysis
|
||
skill is corrected. Verification note for the first two: the agent reads
|
||
through `ReadFileTool` rooted at `src/skills/`, not through GitHub's
|
||
renderer, so the two conventions had to be checked against the real
|
||
consumer.
|
||
- **Dependencies** (#1321, #1322, #1323, #1324) — dockerhub-description
|
||
4.0.2 → 5.0.0, docker/login-action 3.7.0 → 4.6.0, 15 npm minor/patch
|
||
updates and 43 pip minor/patch updates, via Dependabot.
|
||
|
||
### Changed
|
||
|
||
- The `futures` fallback chain is now `["akshare", "local"]`.
|
||
`resolve_loader` walks `FALLBACK_CHAINS` and never consults a loader's
|
||
`markets` set — the only `.markets` read is the explicit-source fallback —
|
||
so trimming that set alone would have left `tushare` first in line,
|
||
constructed and available on any `TUSHARE_TOKEN`, returning empty frames
|
||
before `akshare` was ever asked.
|
||
- `tushare` no longer declares the `futures` market. Serving it needs
|
||
`pro.fut_daily`, which sits behind a points tier nothing here implements.
|
||
- China futures main continuous contracts (`RB0`, `IF0`) now classify as
|
||
`futures` rather than falling to the `a_share` default. The rule is
|
||
generated from the product whitelist rather than from a width heuristic,
|
||
so an ordinary ticker ending in `0` keeps its own market.
|
||
- `test_release_version_consistency.py` did the job it was written for. The
|
||
bump missed the `app.version` mock in
|
||
`frontend/src/components/layout/__tests__/Layout.test.tsx`, and the guard
|
||
named it — that site has been in its enumeration since it shipped with
|
||
0.1.14, which is the point of enumerating declaration sites rather than
|
||
spot-checking the ones somebody remembers. The locale check globs its
|
||
directory, so `pt-BR.json` was covered the day it landed without anyone
|
||
extending the test. The version named in a `test_cli_update.py` comment is
|
||
de-versioned here — a version string in a comment has no guard at all and
|
||
goes stale at the next bump.
|
||
- Reader-facing counts were re-measured against the code rather than
|
||
incremented by hand: 74 MCP tools, 107 registry tools, 10 backtest engines,
|
||
90 bundled skills, 462 alphas, 27 data sources, 30 swarm presets, 14 broker
|
||
connectors. None had drifted this cycle.
|
||
|
||
## [0.1.14] — 2026-08-20
|
||
|
||
Rolls up 272 commits / 74 merged pull requests since 0.1.13.
|
||
|
||
### Added
|
||
|
||
- **Options Lab** (#1096, closes #1095, thanks @shadowinlife) — a Web UI page
|
||
with four surfaces: expiry payoff diagram, spot×IV scenario P&L matrix,
|
||
portfolio Greeks cards, and a live US options chain. The math is not new: two
|
||
read-only HTTP endpoints wrap the existing `options_payoff` /
|
||
`get_options_chain` tools, so the page and the MCP surface compute from the
|
||
same test-pinned `src/quantlib/options.py`. Research only — it places no
|
||
orders.
|
||
- **Factor Research tab** in Run Detail (#1099, thanks @shadowinlife) — IC
|
||
statistics cards, the daily IC series with its mean line, quantile-group
|
||
equity curves, and an IC correlation heatmap, all rendered from the
|
||
`factor_analysis` artifacts a run already writes. The new read-only
|
||
`GET /runs/{run_id}/factor` endpoint scans the run's `artifacts/` tree and
|
||
computes the pairwise IC time-series correlation matrix, which existed
|
||
nowhere before; a `has_factor_artifacts` flag gates the tab so runs without
|
||
factor output do not show an empty panel.
|
||
- **Positions tab** in Run Detail (#1097, closes #1098, thanks @shadowinlife) —
|
||
portfolio-book structure from the existing `positions.csv`: a per-symbol
|
||
weight pie/treemap with a date slider, sector and asset-class net-exposure
|
||
bars, and a weight-evolution stacked area. The pie is **gross** composition
|
||
(absolute weights including shorts and cash) while the bars are **net**
|
||
exposure per industry, so a long/short pair in one sector nets to zero on the
|
||
bars while both legs stay visible on the pie. The two charts answer different
|
||
questions on purpose.
|
||
- **Tearsheet tab** in Run Detail (#1091, closes #1090, thanks @shadowinlife) —
|
||
monthly-returns heatmap (years × months with a full-year column), annual
|
||
returns bar chart, top-5 drawdown table, and an equity curve annotated with
|
||
the ranked drawdown zones. Everything is derived client-side from the
|
||
`equity.csv` rows the run response already carries: no backend change, no new
|
||
dependency, and month axis labels come from `Intl.DateTimeFormat` rather than
|
||
per-locale month keys.
|
||
- **Interactive backtest research dashboard** (#1084, thanks @AndyLongest) — an
|
||
optional report-style view for a completed backtest alongside the existing
|
||
candlestick and trade-detail views: headline return/risk/trading KPIs,
|
||
normalized equity versus benchmark, drawdown with rolling Sharpe, realized
|
||
trade P&L, a trade ledger, and the complete metric table. CLI backtests get a
|
||
quiet local report-server bootstrap so `Full Report` routes to the dashboard
|
||
without a manual server step.
|
||
- **Strategy Discovery** (#978 Phase 1 and #1007 Phase 2, refs #969, thanks
|
||
@shadowinlife) — a read-only facade answering "what strategies exist and what
|
||
state are they in?" across the Alpha Zoo and the SDM strategy store, with
|
||
per-`(strategy, regime)` evidence rows rather than scenario tags. Ships
|
||
`list_strategies` / `query_strategies` / `get_strategy_evidence` as agent and
|
||
MCP tools behind an evidence gate (minimum-trade, coverage and
|
||
cost-breakeven thresholds, each with an explicit warning), plus a startup
|
||
guard that drops the routing text when any advertised tool is not actually
|
||
registered. Phase 2 adds the population path — `refresh_strategy_evidence`
|
||
as an agent tool, an MCP wrapper and
|
||
`vibe-trading strategy-evidence refresh --manifest` — with the
|
||
`backtest-diagnose` Hard-Gate Checklist as the ingestion gate (stable
|
||
`hard-gate:*` skip tokens) and atomic rebuilds. Freshness is computed at read
|
||
time from the evidence-window age (`fresh`/`aging`/`stale`); stale rows fail
|
||
closed out of default recommendations behind an `include_stale` opt-out, and
|
||
`sdm:*` rows mirror the SDM lifecycle.
|
||
- **Scheduled research now delivers itself.** A finished briefing leaves the
|
||
run through an outbox rather than waiting to be read: the outbox row is
|
||
claimed under a lease before sending so a crash mid-delivery cannot double
|
||
post, the send is wired to both the session and the channel runtimes, and the
|
||
push happens on the terminal event. Delivery is configured and watched from
|
||
Market Watch.
|
||
- **Each monitor's latest verdict is persisted on its job** (#1152, refs #943,
|
||
thanks @he-yufeng) — the run's terminal briefing is parsed server-side into a
|
||
structured `last_verdict` record and the `/scheduled-runs` list carries it
|
||
inline, so the list renders a verdict without re-parsing free text per row or
|
||
going N+1 on sessions. The five research playbooks grow a strictly additive
|
||
`## Verdict` tail — one `- SYMBOL: STATE - reason` line per tracked symbol,
|
||
each playbook declaring its own state vocabulary. Parsing is deliberately
|
||
strict: an ad-hoc prompt lands permanently at `no_verdict_section` and renders
|
||
as nothing rather than a warning, a run that tracked nothing is a real "no
|
||
calls" answer rather than an absence, and a malformed section degrades to
|
||
`contract_violation` instead of showing a wrong verdict. The prior record is
|
||
embedded one level deep at write time and no deeper.
|
||
- **Futu connector — seven extended read-only endpoints** (#1135, thanks
|
||
@549236606-oss) closing the gap between what the SDK exposes and what the
|
||
connector consumed: `get_rehab` (dividend/split/rights adjustment factors, so
|
||
backtests stop reading dividend gaps as price moves), `get_capital_flow`
|
||
(historical super/big/mid/small inflow buckets), `get_capital_distribution`
|
||
(today's in-flow versus out-flow snapshot), `get_history_deals` (fill records
|
||
for true cost-basis reconstruction, capped at the SDK's 360-day window),
|
||
`get_acc_cash_flow`, `get_financials` (income/balance/cash-flow statements),
|
||
and `get_earnings_calendar` with EPS and revenue consensus. Each routes
|
||
through the same `_quote_ctx` / `_trade_ctx` / `_assert_gateway` envelope as
|
||
the original five, so missing data, missing privileges and a missing OpenD all
|
||
degrade to a clean fail-closed payload instead of an SDK stack trace. The
|
||
`futu-api` SDK stays lazily loaded.
|
||
- **Vietnam equities (HOSE)** as a backtest market (#1033, thanks @ngoanpv).
|
||
`.VN` matched no entry in `_MARKET_PATTERNS`, so `_detect_market` fell back to
|
||
`a_share` and executed Vietnamese bars under China A-share rules — and asking
|
||
for Yahoo explicitly did not help, because both Yahoo loaders gated on suffix
|
||
and rejected `.VN` before building the request. `.VN` now resolves through the
|
||
Yahoo loaders with a `["yahoo", "yfinance", "local"]` fallback chain and runs
|
||
on a dedicated `VietnamEquityEngine`.
|
||
- **Offline USD-M account reconciliation** (#1106, relates #1030, thanks
|
||
@honginp) — immutable Binance USD-M account and position snapshot contracts
|
||
that compare an exchange observation against the existing `AccountState` and
|
||
`RiskSnapshot` without mutating either, reporting numeric, structural,
|
||
missing-symbol and unexpected-symbol drift deterministically. Liquidation-engine
|
||
validation is explicitly left unassessed rather than silently assumed. This is
|
||
the Shadow 1 slice: contracts and fixture-only offline reconciliation.
|
||
- **Novita AI** as a built-in OpenAI-compatible provider (#1059, thanks
|
||
@jax-novita) — registered in the provider registry with `NOVITA_API_KEY` /
|
||
`NOVITA_BASE_URL` and wired through capabilities, CLI onboarding,
|
||
`.env.example` and the README provider list, so it is selectable from the
|
||
built-in list instead of hand-configured as a custom endpoint.
|
||
- **GitHub Copilot** as a provider through the official `github-copilot-sdk`
|
||
(#990, supersedes #899, thanks @sykuang), with SDK-managed authentication from
|
||
`COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN`, stored Copilot CLI
|
||
credentials or `gh` credentials, plus LangChain-compatible invoke, streaming,
|
||
reasoning and tool-call handling and Settings/preflight integration. This
|
||
implements the boundary #899 was closed on: no borrowed OAuth client ID and no
|
||
editor-impersonation headers.
|
||
- **tickerall hosted MetaTrader 5 data source** (#968, closes #897, thanks
|
||
@miguelangelo78) — a broker's MetaTrader 5 candle feed over the hosted
|
||
TickerAll HTTP API, so forex and metals backtests run on any OS with no local,
|
||
logged-in MetaTrader 5 terminal. Purely opt-in: `is_available()` is False
|
||
unless `TICKERALL_API_KEY` and `TICKERALL_ACCOUNT_ID` are set, and the loader
|
||
never joins an automatic chain.
|
||
- **A drift tolerance band for rebalance mode** — a rebalance that would move
|
||
the book by less than the band is skipped rather than executed for a rounding
|
||
difference.
|
||
- **Spanish and German locales** (#1087 thanks @daviddaco1, #1117 thanks
|
||
@1psconstructor). Spanish ships a full key-for-key `es.json` plus
|
||
`README_es.md`, making Spanish the sixth README; German ships `de.json` with
|
||
the full UI key set. Both register in `SUPPORTED_LANGUAGES` and
|
||
`localeLoaders` on the existing lazy-loading path, and the locale-parity and
|
||
interpolation-variable tests cover them.
|
||
- **Desktop update safety boundary** (#1101, refs #1016, thanks @QCYTSN) — a
|
||
strict PID-scoped backend/watchdog shutdown result for a future update
|
||
handoff, dormant Windows candidate verification, interrupted-attempt recovery
|
||
primitives, and a documented, tested rejection matrix for tampered, unsigned,
|
||
invalid-signature, wrong-publisher and downgraded candidates. The signed
|
||
`0.3.0 → 0.3.1` run itself remains blocked on a signing identity; this is the
|
||
part that could be made executable before a certificate exists, including the
|
||
proof that desktop cleanup never kills unrelated Python processes.
|
||
- **Docker images carry the Feishu and Telegram channel dependencies** (#1088,
|
||
thanks @birdxs), with a manually triggered workflow that builds and pushes to
|
||
both GHCR and Docker Hub and syncs the project description to the Docker Hub
|
||
page.
|
||
- **Strict alpha t-stats** in the bench JSON and HTML report (#1085, thanks
|
||
@jay79-boop), so the strict-mode number is readable from the artifact instead
|
||
of only the console.
|
||
- **Offset paging on the MCP surface** for SEC filings and financial statements
|
||
(#1138), and `load_skill` routed through the registry so an oversized skill
|
||
pages instead of being truncated at the transport limit (#1137).
|
||
|
||
### Changed
|
||
|
||
- **The MCP surface grows to 74 tools** (from 70 in 0.1.13).
|
||
- **`smartmoneyconcepts` is now an opt-in `[smc]` extra, not a base
|
||
dependency** — and the stale `<3.14` cap is gone from both the project
|
||
metadata and the distribution manifest. The package pulled in
|
||
`numba` → `llvmlite`, and llvmlite ships no macOS x86_64 wheel from 0.46
|
||
onward, so as a base dependency it turned every Intel-Mac install into a
|
||
source build that needs CMake (discussion #1035). It backs exactly one skill
|
||
example, whose `SKILL.md` already tells the reader to install it. Intel-Mac
|
||
users on Python 3.11–3.13 can now `pip install vibe-trading-ai` without a
|
||
toolchain.
|
||
- **CI runs the test job on both the floor and the ceiling of
|
||
`requires-python`**, so a version-specific break at either end is caught
|
||
before release rather than by a user.
|
||
- **Vite moves from 6.4.3 to 8.2.0** in the frontend dev toolchain (#1022).
|
||
- **Market-data provenance now declares per-market volume units** (#1065,
|
||
#1131, thanks @shadowinlife), and `get_market_data` carries that provenance
|
||
over MCP rather than dropping it at the transport boundary.
|
||
- **The Ollama runtime URL is normalized at source** (#1074) instead of being
|
||
patched at each call site.
|
||
|
||
### Fixed
|
||
|
||
- **A run's grounding gate stops refusing answers it can support.** The
|
||
identity/price recovery path now re-fetches missing evidence within a bounded
|
||
budget instead of rejecting outright (#1092, thanks @Shizoqua); identity
|
||
constants in rate formulas are no longer read as unsupported quotes (#1083,
|
||
thanks @AndyLongest); line-leading ordered-list markers are masked before
|
||
number extraction (#1063, thanks @zzz607); ISO dates that run straight into
|
||
CJK text are recognised as dates (#1132, thanks @Robin1987China); a
|
||
report-style date cell parses as a date and a US CSV stem resolves; a
|
||
dash-form trading day reads as a date and both bounds of a ranged level are
|
||
masked; an order line is read as an instruction rather than an observed
|
||
quote; and an "@" level is masked only when a quantity or an order label
|
||
accompanies it, so a bare price is still held to the evidence standard.
|
||
- **Agent run reliability** (#1105, thanks @wiliao) — grounding
|
||
false-rejections, the final-answer gate and LLM timeouts, together with
|
||
prompt wording and support/resistance masking (#1060).
|
||
- **`build_registry()` no longer returns a partial registry in silence**
|
||
(#1129, thanks @er-s-an): a construction failure is reported instead of
|
||
leaving the caller with a short tool list that looks complete.
|
||
- **Backtest correctness**: `excess_return` stays consistent with the corrected
|
||
`benchmark_return` (#1058, thanks @Shizoqua) and the corrected benchmark
|
||
fields are rounded to the metrics contract; the engine reports actual
|
||
post-fill positions rather than the requested ones (#1082, thanks
|
||
@AndyLongest); hold mode says so when it drops a requested resize; the
|
||
archive no longer mixes two runs' artifacts into one bundle; and importing
|
||
the runner module no longer loads `.env` as a side effect.
|
||
- **Quantlib numerics**: `xirr` and money-weighted return survive long-horizon
|
||
discount underflow instead of crashing (#1119, thanks @pengpengyi92); DCF
|
||
refuses non-finite inputs rather than returning a silently negative share
|
||
price (#1121, thanks @Robin1987China); a zero-volatility option discounts its
|
||
forward value (#1066), the fixed-income curve keeps decay inside the
|
||
requested bounds (#1076), event studies anchor to the prior session (#1078),
|
||
and cross-validation aligns label ends to the prior observation (#1079) — all
|
||
thanks @pengpengyi92. `technical_indicator` RSI uses Wilder EWM smoothing
|
||
(#1056, thanks @Shizoqua).
|
||
- **Swarm**: worker prompts are ordered for prompt-cache-friendly prefixes
|
||
(#1057, thanks @Echoandelementwebsites); worker artifacts are isolated between
|
||
retry attempts (#1053) and a path-shaped agent id is rejected before the retry
|
||
`rmtree`; raw `ok`/`success` tool-result envelopes are rejected (#1052) and
|
||
oversized tool results truncate with the shared notice (#1110) — thanks
|
||
@Shizoqua; tool-less agents are no longer instructed to call `write_file`
|
||
(#1144, thanks @Echoandelementwebsites); and the per-task `ChatLLM` is closed
|
||
to stop a pooled-connection leak (#1145, thanks @cgycorey), with the same
|
||
treatment for the one-shot clients in auto-title and image vision (#1153).
|
||
- **Connectors and market data**: baostock volume is normalized to board lots
|
||
behind a cross-source consistency guard (#1067, thanks @shadowinlife); the
|
||
IBKR market-data tier is selectable and starved quotes report as `no_data`
|
||
(#1075, thanks @jay79-boop), with the requested and the applied tier reported
|
||
separately; eToro gains runtime UI parity for SDK connector status (#1051) and
|
||
fixes crypto browse and flat market-data quotes (#1070, thanks @ofeksh-tr);
|
||
the `tencent` loader builds its SSL context from the certifi CA bundle, since
|
||
the new GlobalSign Atlas R3 root is absent from Python's default store and HK
|
||
quotes failed every retry (#1113, thanks @x-lambda); and the East Money
|
||
research-report endpoint gets the time parameters it now requires (#1077,
|
||
thanks @zzz607).
|
||
- **CLI**: `connector orders` renders broker_sdk rows — the renderer only knew
|
||
the nested IBKR shape, so a flat row left every column but Account empty — and
|
||
stringified SDK enums print as `BUY` rather than `OrderSide.BUY`, while
|
||
class-B tickers such as `BRK.B` and decimal values are left intact (#1150,
|
||
thanks @nstavros); direct SDK account diagnostics render (#1073); the `show`
|
||
subcommand dispatches its `run_id` instead of the `--show` flag (#1147, thanks
|
||
@cgycorey); and a Docker Codex OAuth EOF explains itself (#1054, thanks
|
||
@zhiwuyazhe-fjr).
|
||
- **Providers**: reasoning effort is honoured in chat completions (#1025, thanks
|
||
@cgycorey) and passed through to the Anthropic adapter (#1115, thanks
|
||
@straun-repo).
|
||
- **A remote MCP failure reports the server's own explanation** instead of a
|
||
bare status code, so a rejected token reads as a rejected token (refs #1126).
|
||
- **Scheduled research**: an in-flight delivery is no longer overwritten
|
||
(#1140, thanks @Shizoqua), and the `scheduled-runs` DELETE returns an empty
|
||
`Response` for its 204 (#1068, thanks @ofeksh-tr).
|
||
- **Tools and agent**: unsupported ticker-plus-name symbol queries are marked
|
||
skipped rather than failed (#1114) and recovery steering is delivered as user
|
||
messages with inline system tags (#1112) — thanks @lorenzozanee;
|
||
prediction-market fields reject non-finite values and the envelope stays
|
||
strict JSON (#1136, thanks @Shizoqua); `gross_profit` is derived from revenue
|
||
minus COGS when the SEC tag is absent (#1111, thanks @cgycorey); every
|
||
compacted message folds through the summarizer; and the autopilot preserves
|
||
backtest validation evidence when linking hypotheses (#1139, thanks
|
||
@Shizoqua).
|
||
- **Onboarding**: `.env.partial` is created with owner-only permissions (#1086,
|
||
thanks @lukiod) and written atomically, so a failed save cannot destroy
|
||
recovery state.
|
||
- **Memory**: the fallback tokenizer's minimum is aligned with the FTS5
|
||
sanitizer (#1071, thanks @Shizoqua).
|
||
- **Research reports** reject a reversed window instead of reporting it as
|
||
missing coverage.
|
||
- **Web**: inferred strategy labels are marked as inferred in the run dashboard
|
||
(#1134, part 1 of #1094, thanks @fixXxerTech).
|
||
- **The test suite no longer escapes its sandbox into the real config root**
|
||
(#1118, closes #1116, thanks @lorenzozanee). A full run had been appending
|
||
synthetic `order_rejected` records to the live, hash-chained audit ledger at
|
||
`~/.vibe-trading/live/audit.jsonl` and writing a `live/audit_chain.jsonl`
|
||
holding a single NUL byte. It was reported on Windows, but the escape was not
|
||
Windows-specific — a default macOS environment leaked the same way. The
|
||
config root is now sandboxed before collection, through one knob at the tail
|
||
of the resolution chain rather than a per-test override, and `os` patches are
|
||
confined to the module under test.
|
||
hash locks are portable across supported platforms (#1102); Windows desktop
|
||
packaging inputs are stabilized (#1104); the Docker build workflow is pinned
|
||
and the image stays on hash-locked dependencies; the alpha-bench SP500 panel
|
||
carries the GICS sector through; a frontend test waits on the sector
|
||
re-render rather than on the spinner disappearing; and the Spanish locale says
|
||
"demo", not "papel", for paper trading.
|
||
|
||
## [0.1.13] — 2026-08-10
|
||
|
||
Rolls up 408 commits / 162 merged pull requests since 0.1.12.
|
||
|
||
### Added
|
||
- **`src/quantlib` — a tested finance-math layer** (265 functions across 19
|
||
modules): one
|
||
implementation each of Black-Scholes price/greeks/IV inversion, bond math +
|
||
Nelson-Siegel/Svensson, Altman Z + Merton/KMV, stationarity/cointegration/
|
||
GARCH/bootstrap, VaR/CVaR/EVT, Brinson-Fachler attribution, event studies,
|
||
multiple-testing control, and purged cross-validation. Skills now *import*
|
||
these instead of carrying formulas inside markdown code blocks. Reachable
|
||
from the CLI, the Web UI, the REST API and MCP through the read-only,
|
||
pure-compute `quantlib_call` tool, so the finance math works where `bash` is
|
||
gated off — module allowlist, `__all__`-only, `export_*` refused.
|
||
`attribution` and `impact` were allowlisted but carried no `__all__`, so the
|
||
tool listed zero functions for them; both now export, and a guard test fails
|
||
when any allowlisted module exposes nothing. Econometrics needs the
|
||
`stats` extra (`statsmodels`/`arch`), which the functions lazy-import and
|
||
name.
|
||
- **Valuation engine** (`src/quantlib/valuation/`): `run_dcf` (FCFF bridge,
|
||
WACC build, dual terminal value that cross-checks each method against the
|
||
other's implied multiple and implied g, mid-year discounting, net-debt
|
||
bridge, WACC×g grid), `run_comps` (EV bridge, LTM + calendar-year
|
||
calendarisation, multiple matrix — a peer with a non-positive denominator is
|
||
*excluded and reported*, never averaged in as a negative multiple), a linked
|
||
three-statement projection with a hard balance assertion, explicit revolver
|
||
plug and an iterated interest↔debt circularity that must converge or raise,
|
||
plus input-hashed versioned artifacts with xlsx/pptx export. One shared rule
|
||
in `contracts.py`: **a missing input makes a model NOT RUNNABLE and is never
|
||
silently defaulted.**
|
||
- **Typed entity + irregular dated cash-flow substrate** (`src/entities/`) —
|
||
the non-bar ingestion path for NAVs, capital calls and coupons, deliberately
|
||
parallel to the bar engines so a `nav` column can never reach one and get
|
||
priced as a close. Surfaced by the read-only `cashflow_performance` tool
|
||
(XIRR / MOIC / DPI / TVPI / TWR / Modified Dietz / MWR over an irregular
|
||
dated series).
|
||
- **`orderbook_depth`** (read-only): crypto L2 ladder via ccxt — spread bps,
|
||
depth imbalance, and the impact cost of a stated notional walked through the
|
||
real book in both directions. `fully_filled` and `timestamp_source` must be
|
||
read before quoting anything; equities still have no depth source.
|
||
- **Governance wired into every run** (`src/governance/`): a run manifest hashes
|
||
the prompt, skill contents, tool registry and package versions so "what
|
||
methodology produced that number" is answerable, and the audit ledger is
|
||
hash-chained + fsynced so editing or deleting a record is detectable (an edit
|
||
that recomputes its own hash is caught one record later via
|
||
`prev_hash_mismatch`). All 30 swarm presets were re-audited — a deliverable
|
||
no granted tool can compute is now declared as such instead of invented.
|
||
- **Four read-only data tools, all on free public sources**:
|
||
`get_institutional_holdings` (SEC 13F-HR; manager / ticker-holders /
|
||
top-managers modes with quarter-over-quarter position diffs, and cover-page
|
||
unit detection because pre-2023 filings report in thousands),
|
||
`etf_holdings` (cross-market look-through — SEC N-PORT for US, and for
|
||
A-shares the semi-annual/annual reports that carry the *full* book rather
|
||
than the quarterly top ten: 510300 returns 342 rows / 98.66% of net assets
|
||
vs 10 rows / 22.74%; `coverage` distinguishes `full_portfolio` from
|
||
`top_n_disclosed` and every response stamps the report period),
|
||
`prediction_market` (event-contract search/event/market/history with prices
|
||
surfaced as labelled implied probability), and `research_papers`
|
||
(arXiv + OpenAlex search/read with source-anchored claim extraction that
|
||
marks what a source does not state instead of inferring it).
|
||
- **Institutional research surface**: six slash commands — `/comps` `/dcf`
|
||
`/attrib` `/memo` `/earnings` `/screen` — each carrying a step skeleton and
|
||
an arithmetic-consistent worked example (the Brinson decomposition sums
|
||
exactly to active return; the earnings bridge sums exactly to the EPS delta).
|
||
- **Investor lenses** as a standalone skill (`src/skills/investor-lenses/`):
|
||
named-investor reasoning frameworks as stackable analysis overlays, decoupled
|
||
from the data layer. Each lens is an operating procedure — priority signals,
|
||
disqualifying conditions, typical misuse — not a biography, and names no tool.
|
||
- **Five ready-to-schedule research playbooks** (premarket brief, earnings-season
|
||
tracker, portfolio checkup, A-share money flow, institutional-holdings diff),
|
||
reachable three ways: auth-gated REST routes beside the existing CRUD, a
|
||
`vibe-trading playbook` CLI subcommand, and a `/playbook` slash command.
|
||
Templates state their data needs in natural language rather than naming
|
||
tools, so coverage can grow without editing them, and every one mandates
|
||
naming a missing input instead of filling it from memory.
|
||
- **Desktop shell** — a source-first Electron host that owns the existing
|
||
backend lifecycle (random loopback port, per-launch secret, five-locale
|
||
startup recovery, owned-process cleanup) (#923). Windows packaging assembles
|
||
a checksum-pinned embedded Python 3.12 runtime with x64 NSIS review/signing
|
||
paths, plus Electron `safeStorage` for an allowlisted credential set — the
|
||
renderer can set or clear secrets but never read them, plaintext config
|
||
migrates once, decrypted values reach only the owned backend, and both
|
||
unsigned-review and signed builds fail closed on the wrong signature state
|
||
(#1015). No installer artifact was published from that PR.
|
||
- **Canadian equities end to end** (#1024, #1019, #1037, closes #952): `.TO`/`.V`
|
||
symbols classified in CAD, routed Yahoo → yfinance → local, executed under
|
||
Canada-specific GlobalEquity rules, benchmarked against `XIC.TO`, with
|
||
mixed-currency aggregation refused.
|
||
- **Korea equity (KRX: KOSPI/KOSDAQ)** as the 9th backtest engine (#693, thanks
|
||
@JungHoonGhae) — execution-time ±30% band on a unified tick grid, structurally
|
||
long-only, config-driven 2026 0.20% securities transaction tax, optional
|
||
`pykrx` loader that rejects non-daily intervals so the runner falls through.
|
||
- **eToro connector** with path-separated demo/real profiles (#989); live
|
||
risk-increasing actions remain mandate-gated and audited.
|
||
- **OpenBB Workspace bridge** (#817, thanks @shugaoye) — optional `openbb`
|
||
extra registering `/agents.json` + an authenticated `/v1/query`, each request
|
||
replaying the supplied history in an ephemeral session. `pro.openbb.co` is
|
||
not a default CORS origin; opt in via `VIBE_TRADING_EXTRA_CORS_ORIGINS`.
|
||
- **Read-only Taiwan snapshot** tool (#848, thanks @TSENGCHIENFENG), registered
|
||
only when `VIBE_TW_STOCK_DB` points at a schema-valid snapshot.
|
||
- **Options strategy analytics** (#946, rebuilt from #883, thanks @he-yufeng) —
|
||
analytic expiry P&L extrema, exact breakevens including continuous zero-P&L
|
||
intervals, engine-aligned entry commissions, and spot × IV scenarios through
|
||
Agent and MCP.
|
||
- **Read-only `sentiment` tool** (#939, thanks @Robin1987China): local lexicon
|
||
scoring for arbitrary text plus the crypto Fear & Greed Index from
|
||
Alternative.me's free, no-auth API.
|
||
- **Read-only `technical_indicators` tool** (#921, refs #920, thanks
|
||
@Robin1987China) — RSI/MACD/Bollinger/SMA/EMA through the existing loaders.
|
||
- **Timezone-aware scheduled research** (#954, closes #953, thanks @ngoanpv):
|
||
jobs take an optional IANA `timezone` and evaluate cron on that zone's wall
|
||
clock, so a cadence survives DST — a spring-forward gap is skipped and a
|
||
fall-back ambiguous time runs once. Cron fields gain comma lists and ranges
|
||
(`1,3-5`); jobs without a timezone keep UTC semantics; the web UI gains a
|
||
**Scheduled** page in all five locales.
|
||
- **`vibe-trading update`** (#1020) — checks PyPI for a newer release and
|
||
upgrades in place, installing the exact release it checked and verifying
|
||
fresh metadata without downgrading. Editable/source checkouts are detected
|
||
and answered with the right manual instruction (`git pull` + reinstall)
|
||
instead of a pip upgrade that would silently replace the dev install.
|
||
- **ModelScope** joins the built-in providers through its official
|
||
OpenAI-compatible hosted-inference endpoint, default `Qwen/Qwen3.5-27B`
|
||
(#1011, thanks @honginp).
|
||
- **Live model discovery** in Settings (#924, thanks @QCYTSN) — configured
|
||
providers are queried on demand with stable warning codes and five-locale
|
||
controls, and each reply records and reloads the immutable provider/model/
|
||
reasoning identity that actually served it.
|
||
- **`alpha_zoo` + bounded `alpha_bench` on MCP** (#979, thanks @cgycorey) with
|
||
horizon/result/output-path limits and safe report creation; **QVeris**
|
||
discovery/inspect/execute also join the MCP surface (#976, closes #964,
|
||
thanks @shadowinlife), with the cost quote read from the marketplace instead
|
||
of trusted from the caller.
|
||
- **The MCP surface grows to 70 tools.** Six read-only analytics tools that had
|
||
reached the agent but never MCP are now mirrored onto it: `quantlib_call`,
|
||
`cashflow_performance`, `orderbook_depth`, `sentiment`,
|
||
`technical_indicators` and `get_fundamentals`. Order-placing tools stay
|
||
structurally un-exposed — the mirrored registration path refuses any class
|
||
whose `is_readonly` is not `True`, so an order tool cannot reach MCP even if
|
||
someone adds it to the source list.
|
||
- **Memory Tier 2: structural organization** (#815, thanks @shadowinlife) —
|
||
four independently-gated modules (H-MEM hierarchical routing
|
||
`VT_MEMORY_HIERARCHY`, A-MEM BM25 semantic linking `VT_MEMORY_LINKS`,
|
||
TF-IDF auto-compression `VT_MEMORY_COMPRESSION`, FTS5 index with CJK bigram
|
||
tokenization `VT_MEMORY_FTS_INDEX`), a one-line `VT_MEMORY=off|on|full`
|
||
preset replacing seven individual flags, dependency-conflict warnings, lazy
|
||
FTS5 rebuild on first search, and 9 integration + 23 benchmark tests. All
|
||
off by default (#733, closes #732).
|
||
- **SDM lifecycle tools** `sdm_register` / `sdm_status` / `sdm_decay_scan`
|
||
(#457, thanks @shadowinlife) backed by a SQLite artifact store
|
||
(`UNIQUE(name, universe)`, WAL) and an IC/Sharpe decay state machine
|
||
(active → monitoring → decayed → disabled).
|
||
- **`alpha bench --strict`** (#796, closes #773, thanks @he-yufeng) finally
|
||
wires the strict same-universe random-control + OOS gate that shipped
|
||
unreachable since 0.1.9; `alpha bench` also discloses CSI300/SP500 sources,
|
||
counts, degraded fallbacks and survivorship bias (#859, closes #845), with
|
||
the `_meta` disclosure forwarded to CLI/HTML (#841, closes #797, thanks
|
||
@AmirF194).
|
||
- **Risk x-ray artifacts** (`risk_xray.json`/`.md`) emitted by every portfolio
|
||
backtest with headline concentration/vol/drawdown metrics (#900, thanks
|
||
@he-yufeng), plus rebalance-notes artifacts and turnover metrics (#795).
|
||
- **Composable optimizer weight constraints** (#818, thanks @he-yufeng);
|
||
`calc_metrics` reports tracking error and benchmark beta.
|
||
- Opt-in **atomic same-direction rebalancing** in backtests with immutable fill
|
||
evidence (#951), and opt-in `position_adjustment=rebalance` for strict USD-M
|
||
historical backtests preserving collateral, funding, fees, realized P&L,
|
||
liquidation behavior and fill evidence across increases and reductions
|
||
(#1019, thanks @honginp).
|
||
- **USD-M perpetual realism**: `perpetual_strict` settles historical funding
|
||
before fills and executes isolated/cross margin breaches as real liquidations
|
||
(#903, #889, thanks @honginp), margin state contracts (#798), historical
|
||
funding rates actually consumed rather than fetched-and-ignored (#819, thanks
|
||
@g0rdonL), and ordered fill/funding/risk/liquidation event persistence plus a
|
||
fidelity summary (#936).
|
||
- **Final report aggregator stage** for the `quant_strategy_desk` swarm preset
|
||
(#1048, thanks @Robin1987China).
|
||
|
||
### Changed
|
||
- **Rebuilt WebUI** — the guided-minimalism overhaul: no first-frame flash, one
|
||
durable activity object per turn with a live reasoning whisper and a
|
||
reload-safe tool trail, LLM-written session titles, full five-locale parity.
|
||
- Sessions, runs, swarm runs and uploads now live under `~/.vibe-trading`
|
||
(relocatable via `VIBE_TRADING_HOME`) with a one-time automatic migration
|
||
(#925, closes #904, thanks @MuggleJinx).
|
||
- The frontend moves to **Node 22 + React Router 8**, clearing a high-severity
|
||
advisory; Electron transitive advisories were patched separately.
|
||
- Daily price bands are judged **at execution time** — from a pre-fill base
|
||
price against the prospective fill price — never from the decision bar's own
|
||
close (#676, thanks @tyj147454413-cmd).
|
||
- A session runs **one attempt at a time** (a second concurrent send is refused
|
||
with HTTP 409), and a user stop is its own terminal state distinct from a
|
||
failure, both live and on reload.
|
||
- Trace redaction is now **sink-aware**: `content` is released only in the
|
||
tool-RESULT sink and stays redacted in the fail-closed ARGUMENTS sink used by
|
||
tool-call arguments and the live audit ledger (`env` is never released);
|
||
results also get their string leaves pattern-scrubbed (#675, #913, #911,
|
||
thanks @santhreal). Trace records are fsynced and sidecars made durable
|
||
before the record that references them (#662).
|
||
- Oversized tool results are paged by whole record with an explicit total
|
||
instead of being cut mid-JSON; the system prompt drops redundant tool prose.
|
||
- Dependency locks refreshed and verified across Python and the frontend
|
||
(#949, #948, #850–#852, #1021, #1023, #1026, #1027); the breaking MCP 2.0
|
||
bump remains unmerged pending a complete lock/runtime migration (#950).
|
||
|
||
### Fixed
|
||
- **The identity/grounding gate stops refusing answers it has the evidence
|
||
for.** This was the single most user-visible defect in the 0.1.12 line — a
|
||
well-formed run would spend minutes on real tool calls and then return
|
||
*"cannot safely confirm instrument identity or price evidence"*. Root causes,
|
||
each fixed and covered by two-sided guard tests:
|
||
- `.SS` and `.SH` were treated as different instruments, so **every Shanghai
|
||
ticker was permanently ambiguous**. Symbols are now canonicalized
|
||
(`.SS`≡`.SH`, `sh600519`≡`600519.SH`, `700.HK`≡`00700.HK`,
|
||
`BTC/USDT`≡`BTC-USDT`) before comparison, and candidates are collapsed by
|
||
canonical symbol before the ambiguity test.
|
||
- A **failed side query could demote an already-locked identity**; aggregate
|
||
status now keeps a lock unless something genuinely conflicts.
|
||
- Yahoo returns HTTP 400 for every CJK query, which was recorded as a source
|
||
*failure* and escalated to blocking `invalidated`. Non-ASCII queries are
|
||
now declined up front with an explicit skip marker, and a resolution with
|
||
clean sources and no failures resolves to `not_found` rather than
|
||
`invalidated`. All resolver skip markers were unified on one prefix, with a
|
||
cross-module contract test.
|
||
- A hardcoded per-tool whitelist decided which bare tickers could be matched,
|
||
blocking 11 of the 17 documented argument spellings; matching is now by
|
||
uniqueness against the authorized set.
|
||
- Chinese-language answers were rejected for writing `雅虎`/`腾讯` instead of
|
||
the ASCII loader name, and `元` instead of `人民币`. Source and currency
|
||
aliases now cover the user's language.
|
||
- Thousands separators split a clause mid-number, so `¥1,309.22` was compared
|
||
as `1` against the observed range and reported as a price conflict.
|
||
- Conceptual questions with no instrument at all, and comparison reports,
|
||
were dead-ended by the gate.
|
||
- Follows the narrower 0.1.12-line fixes for numbers that were never prices —
|
||
confidence scores, indicator readings, moving-average windows, year-less
|
||
dates like `8/5`, percentage ranges, and a plan's own trigger levels
|
||
(`close ≥ 6.45` is a condition, not a quote) (#1001, #983, #955) — and a
|
||
many-candidate shortlist now counts as an answer rather than a stalled
|
||
resolution. A quote outside recorded OHLC evidence is **still refused**.
|
||
- Ordering, evidence and serialization repairs from the same family: a
|
||
canonical symbol/venue must be locked before a market-sensitive tool-call
|
||
batch starts so a resolver and its consumer cannot race (#887/#886); tool
|
||
envelopes with `ok: false` / `success: false` are no longer recorded as
|
||
successful calls; NaN/inf volume and close no longer crash
|
||
`format_grounding_block` (#1043, thanks @santhreal); the latest close is
|
||
paired with its correct date; and bash-written run-dir OHLC CSVs count as
|
||
observed evidence (#1037, thanks @wiliao).
|
||
- **Sandbox gap closed**: generated strategy code can no longer import the
|
||
broker layer, nor reach `socket`/`subprocess`/`os.system`/`ctypes` through a
|
||
renamed binding — both were accepted before. `src.quantlib` still imports.
|
||
- **SEC reporting periods are keyed on their `(start, end)` span.** A 10-Q files
|
||
the true quarter and the year-to-date frame under the same end date and
|
||
fiscal period, so `period="annual"` had been returning a single quarter for
|
||
AAPL FY2018–2020 (a 4.2× understatement) and every fiscal-Q4 slot in a
|
||
quarterly series carried the full-year figure. `get_fundamentals("AAPL.US")`
|
||
no longer answers `ok:true` with an all-null panel. PIT fundamentals also
|
||
dedup restated rows and stop the snapshot regressing to an older fiscal
|
||
period on a late restatement (#772, closes #771, thanks @klmtseng).
|
||
- **Tushare A-share prices are corporate-action adjusted** in both the factor
|
||
bench and backtests — a raw close-to-close return across an ex-date was off
|
||
by up to 47 percentage points (300750.SZ, 2023-04-26) — and the CSI300 bench
|
||
masks each date to its point-in-time index membership.
|
||
- Cross-market composite backtests **refuse a mixed-currency code set** instead
|
||
of summing CNY, USD and KRW into one equity curve; Shadow splits mixed
|
||
markets by settlement currency without invented FX aggregation (#997).
|
||
- Option legs are marked at the volatility they were opened at, removing a
|
||
fabricated day-zero P&L of up to +93% of premium; options **partial-close**
|
||
honors the requested quantity instead of flattening the lot.
|
||
- `bar_returns` no longer erases the real move across a trading halt longer
|
||
than the forward-fill window — the resumption move was silently recorded as
|
||
0, understating volatility and inflating Sharpe — and an `inf` prior price
|
||
can no longer read as a clean −100% (#895, thanks @darkknight4563).
|
||
Buy-and-hold returns are sign-safe (#872), the rolling correlation matrix no
|
||
longer forward-fills missing closes (#873, thanks @ddy4633), indicators use
|
||
consecutive unsampled history (#1005), negative-equity drawdown and empty
|
||
insolvent cross accounts are handled (#958, #959), and `inf` from
|
||
`pct_change` on zero equity is replaced (#1041, thanks @santhreal).
|
||
- **Annualisation now covers all 24 data sources** at every interval, with a
|
||
coverage test that fails CI when a loader lands without entries (#891, closes
|
||
#884, thanks @Robin1987China). A 19-PR **interval-normalization sweep**
|
||
accepts lowercase `1h/4h/1d/1w` everywhere, fails fast on unsupported
|
||
intervals instead of silently returning daily bars, maps Yahoo `4H`→`1h`, and
|
||
keeps `1H`/`4H` as hour bars across the Tiger/Alpaca/OKX/Shoonya/Longbridge
|
||
connectors (#812–#838, #778–#794, thanks @santhreal); lowercase `4h` returns
|
||
true four-hour bars (#1013).
|
||
- **Market-data routing**: partial results complete the missing symbols through
|
||
the fallback chain and fail closed instead of silently shrinking the backtest
|
||
universe (#689, closes #681, thanks @xkam7ar); HK fallback routing repaired
|
||
with a new Tencent HK source (#1000); yfinance crypto routes to the crypto
|
||
engine (#970); OKX bars use `history-candles` with rate-limit retry (#644);
|
||
a Binance loader joins the crypto fallback chain (#643).
|
||
- **Providers**: Claude models that deprecate `temperature` (opus-4-7, opus-5,
|
||
sonnet-5) work — the adapter drops the field when the API rejects it, retries
|
||
once, and remembers the model, so no per-release patch is needed (#890,
|
||
closes #856, thanks @yagnikpipaliya). The whole Kimi K-series auto-forces
|
||
`temperature=1` (#701, thanks @sambazhu); endpoint resolution falls back to
|
||
each provider's canonical base URL; mapping-shaped Responses stream events are
|
||
accepted (#1034, thanks @cgycorey); OpenAI Codex OAuth gets a separate
|
||
synchronized credential store and one-shot 401 recovery (#1014); proxy opt-out
|
||
covers sync and async clients (#995); long model slugs stay readable (#1006).
|
||
- **MCP**: dataclass results no longer crash on a false
|
||
`Circular reference detected` (#849, #922, thanks @Echoandelementwebsites);
|
||
`factor_analysis` is realigned to the registered tool's real CSV contract so
|
||
calls no longer die on `KeyError` (#715, closes #635, thanks
|
||
@Robin1987China); list/dict arguments tolerate JSON-string clients (#993);
|
||
the network guard accepts IPv6 and case-variant hosts (#750); the specs cache
|
||
key includes remote server identity (#1049, thanks @Shizoqua); nested results
|
||
serialize cleanly.
|
||
- **Swarm**: `ok:false` / `success:false` tool failures are detected, not just
|
||
`status:error` (#1028, thanks @Shizoqua); MCP discovery is cached (#704).
|
||
- **Scheduled research** isolates malformed records (#1003), fixes
|
||
interval-timezone validation (#1004), and retries transient dispatch failures
|
||
with capped exponential backoff.
|
||
- **Memory**: entries are written and recovered with their `.md` suffix (#984);
|
||
FTS5 ranking applies importance decay (#1032, thanks @Shizoqua); exact
|
||
index-anchor matching and a respected result bound (#956, #957, thanks
|
||
@santhreal).
|
||
- **Two quantlib modules were allowlisted but unreachable.** `attribution`
|
||
(Brinson-Fachler) and `impact` (market-impact models) carried no `__all__`,
|
||
and `quantlib_call` dispatches on `__all__` alone, so the tool listed **zero**
|
||
functions for both while the package docstring advertised them. Both now
|
||
export, and a guard test fails when any allowlisted module exposes nothing —
|
||
the previous test only asserted each module *imports*, which could never
|
||
catch this.
|
||
- **The published MCP manifest under-reported the server.** `SKILL.md`'s tool
|
||
count was derived by counting `@mcp.tool` decorators, which ignores every
|
||
tool registered through the mirrored path — so four institutional-research
|
||
tools were live over MCP and absent from the manifest, and the contract test
|
||
asserted that absence was *correct*. Both tests now measure
|
||
`mcp.list_tools()`.
|
||
- **Resource leaks and robustness**: the HTTP throttle sweeps stale bucket
|
||
entries interval-aware, preserving rate-limit spacing (#1047), the rate
|
||
limiter no longer grows unboundedly with unique client IPs (#1039), the
|
||
event bus notifies and removes subscribers on clear (#1046), `_json_loads`
|
||
is guarded against corrupted JSON in database columns (#1045), and Shoonya
|
||
tolerates empty-string numeric fields (#1038) — all thanks @santhreal.
|
||
- **Connectors**: the `connector` CLI loads `~/.vibe-trading/.env` so
|
||
env-sourced broker credentials resolve again (#902, closes #901, thanks
|
||
@MuggleJinx); IBKR moves to a thread-local connection pool with snapshot
|
||
quotes, fixing hangs under parallel agent runs (#636, thanks @MikeCer); MT5
|
||
`trading_history` coerces numpy scalars so JSON serialization no longer dies
|
||
on `int64` (#776, closes #774, thanks @shadowinlife); CLI balances show a
|
||
real account label (#843, closes #846).
|
||
- **Docker's hash-locked install works again** with a new CI lock check (#858,
|
||
closes #847), and saving Agent LLM settings from the Web UI no longer returns
|
||
HTTP 500 on Windows — the POSIX-only `os.fchmod` hardening is platform-guarded
|
||
(#561, thanks @CRui5in).
|
||
- Non-interactive `vibe-trading run` injects a host session id: research-goal
|
||
tools previously failed on every call while the run still reported success
|
||
(#885). The agent now stops when evidence is sufficient (#1010).
|
||
- The **vn.py export** skill is repaired for the vn.py 4.x layout, where
|
||
`vnpy.app.cta_strategy` no longer exists upstream — templates import from
|
||
`vnpy_ctastrategy` (#869, thanks @y85998607).
|
||
- An **encoding and parsing batch**: UTF-16 BOM decoding in the document reader
|
||
and trade-journal CSVs, currency symbols stripped before numeric coercion,
|
||
`BTCUSDT`-style symbols inferred as crypto, CJK characters preserved in skill
|
||
directory slugs, Eastmoney/Futu/Tonghuashun Excel-serial dates normalized,
|
||
blank/NaN symbol rows skipped, and skill frontmatter parsed at EOF
|
||
(#862–#868, #811, #749, #861, thanks @santhreal and @Robin1987China).
|
||
- QQ replies retain source message IDs (#1008); Feishu/CLI markdown table edges
|
||
and indent-preserving channel message splits are fixed (#867).
|
||
- The daily-bar validator can opt in to **non-positive prices** — opening on
|
||
negative bars while still rejecting zero (#816, closes #571, thanks
|
||
@darkknight4563).
|
||
- Sandboxed runs retain their canonical root (#1012, #1017); persisted run
|
||
state is fsync-durable (#645); Portfolio Studio artifacts are surfaced in run
|
||
detail (#980, #982, #966, #973).
|
||
|
||
## [0.1.12] — 2026-07-22
|
||
|
||
### Added
|
||
- **User swarm-presets directory**: preset YAMLs dropped into
|
||
`~/.vibe-trading/swarm/presets/` are discovered alongside the bundled
|
||
roster (same-name files override it — the same rule as user skills) and
|
||
survive `pip install -U`. `list_presets()` entries now carry a
|
||
`source: "user" | "bundled"` field; explicitly named user presets run
|
||
through `run_swarm(preset_name=...)`, while keyword auto-routing stays
|
||
limited to the curated table. Preset names are validated to a single path
|
||
segment before any filesystem lookup.
|
||
- **Security hardening**: all 10 findings from the 2026-07-10 external audit
|
||
closed (#476, tracking discussion #468) — Docker multi-stage rebuild with
|
||
digest-pinned base images, AST-hardened backtest sandbox (blocks
|
||
network/subprocess/eval/os.environ/unsafe-open reachable from generated
|
||
code, including inside nested function bodies), short-lived single-use SSE
|
||
auth tickets replacing a long-lived key in the URL/logs, hardened Compose
|
||
(`read_only`, dropped capabilities, `no-new-privileges`, resource limits),
|
||
auth + rate limiting on `/correlation`, security headers (CSP
|
||
Report-Only, `X-Content-Type-Options`, `Permissions-Policy`), `/live` +
|
||
`/ready` health split, hash-locked dependencies wired into the Docker
|
||
build, GitHub Actions pinned by commit SHA, and an HMAC-authenticated
|
||
factor cache.
|
||
- Opt-in **TAP mode** for Alpaca (#377, thanks @0xZKnw) — routes all broker
|
||
egress through a self-hosted TAP proxy so the agent process never holds
|
||
the raw API key, with writes blocking on human approval.
|
||
- Realized portfolio turnover (`avg_turnover` / `total_turnover`) surfaced
|
||
in backtest metrics for every optimizer (#478, thanks @Robin1987China).
|
||
- **Frazzini-Pedersen betting-against-beta** academic factor (#480, thanks
|
||
@YogeshModi24) — Alpha Zoo: 460 → **461**.
|
||
- **MetaTrader 5 connector + data source** (Exness-style MT5 brokers,
|
||
Windows-only `pip install "vibe-trading-ai[mt5]"`). Broker connectors:
|
||
11 → **12** — full read surface plus order placement against a locally
|
||
running terminal, with a bidirectional identity guard (paper profile ⇔
|
||
demo `trade_mode`, login pinned, contest rejected), connector-level
|
||
`max_order_volume`/`max_order_notional_usd` guards on demo AND live, and
|
||
hedging-safe position close by ticket. The live mandate gate gains
|
||
`forex`/`cfd` instrument vocabulary (schema v1 unchanged) and a lot-aware
|
||
`quantity_notional_usd` sizing hook so USD caps bind on lot-sized orders
|
||
(0.1 lot EURUSD ≈ $10,800, never 0.1 × quote). Market-data sources:
|
||
20 → **21** — the `mt5` loader heads the forex fallback chain (broker-exact
|
||
symbols with Exness suffix discovery, 1m–1D bars), `get_market_data` learns
|
||
forex/metal symbol routing (`EUR/USD`, `XAUUSD.FX` previously fell through
|
||
to tushare), and akshare's forex path accepts the canonical slash form so
|
||
degradation off-Windows keeps working.
|
||
- **Strategy Development Manager** skill (#457, thanks @shadowinlife, closes
|
||
#455) — `sdm_register` / `sdm_status` / `sdm_decay_scan` turn academic
|
||
papers and broker research into registered factors/strategies with a
|
||
persistent SQLite artifact store (`UNIQUE(name, universe)`) and automated
|
||
IC/Sharpe decay monitoring driving an active → monitoring → decayed →
|
||
disabled lifecycle. Pluggable OCR for `read_document` (local RapidOCR by
|
||
default; cloud Qwen-VL is explicit opt-in only via
|
||
`VIBE_TRADING_OCR_ENGINE=qwen-vl`, never auto-selected). Skills: 86 → **87**.
|
||
- **Requesty** as an OpenAI-compatible LLM gateway provider (#474, thanks
|
||
@Thibaultjaigu) — same `provider/model` naming and capability shape as
|
||
OpenRouter, wired through CLI onboarding, provider menu, and Settings.
|
||
- Binance USD-M perpetual routing, slice 1 of #462 (#470, thanks @honginp) —
|
||
explicit `BTC-USDT-PERP` symbol contract with execution/mark price
|
||
separation, fail-closed when the two aren't timestamp-synchronized.
|
||
- **Correlation regime timeline** (#756, thanks @ebujinovch, closes #719) — a
|
||
new additive `GET /correlation/regime` endpoint plus an opt-in "Regime
|
||
timeline" strip on the Correlation tab: rolling pairwise correlations reduce
|
||
to an edge-density series, causally smoothed, and run through a two-threshold
|
||
hysteresis state machine that marks FUSED episodes ("when did the market fuse
|
||
into one bloc?"). Descriptive risk context, not a trading signal; shares
|
||
`/correlation`'s auth + rate-limit budget. Backed by the **correlation-regime
|
||
skill** (#557, thanks @ebujinovch).
|
||
- **Three new LLM providers** — SiliconFlow CN + Global (#565, thanks @UNHNQ),
|
||
iFlytek Spark (#537, thanks @FenjuFu), and a **native Anthropic Messages API**
|
||
adapter (#695, thanks @jelech; `pip install "vibe-trading-ai[anthropic]"`).
|
||
MiniMax now exposes its regional API endpoints (#731, thanks @octo-patch).
|
||
- **Historical USD-M funding settlements** for Binance perpetuals (#716, thanks
|
||
@honginp); maintenance brackets are supplied as a validated, versioned
|
||
artifact rather than a live authenticated fetch (#757, thanks @honginp), so a
|
||
plain `-PERP` backtest stays zero-credential.
|
||
- **Pluggable OCR** for `read_document` with optional LLM-vision extraction and
|
||
a configurable text-density threshold (#548, thanks @shadowinlife) — local
|
||
RapidOCR by default; cloud engines are explicit opt-in, never auto-selected.
|
||
- New academic factor `academic_corr_rewire` (#705, thanks @ebujinovch) and the
|
||
fundamental zoo wired into the `_VALID_ZOOS` whitelist (#707, thanks
|
||
@sambazhu). Binance crypto fallback loader (#643) and bounded OKX history
|
||
fetches with rate-limit handling (#644, thanks @tyj147454413-cmd).
|
||
- QVeris premium-track hardening — session budget applied to backtest data
|
||
calls (#685) and atomic credit accounting (#686, thanks @xkam7ar).
|
||
|
||
### Changed
|
||
- Correlation tab accepts bare tickers like `AAPL,SPY` and walks the full
|
||
loader fallback chain instead of failing with `Fetched: []` (#472, thanks
|
||
@yxhuang, closes #471).
|
||
- `local` loader honors the requested interval via OHLCV resampling instead
|
||
of silently returning daily bars (#467, thanks @Shizoqua).
|
||
- Provider credentials are resolved through one centralized path, fixing a
|
||
gateway misroute (#563, thanks @shadowinlife, closes #549/#553). When no
|
||
`*_BASE_URL` is set, the backend now falls back to the provider catalog's
|
||
canonical `default_base_url` (the same default Web Settings already used), so
|
||
a CLI / manual-`.env` user reaches the right endpoint instead of defaulting
|
||
to `api.openai.com`.
|
||
- Signal alignment is vectorized for an ~80× speedup on wide panels (#698,
|
||
thanks @shadowinlife); swarm workers cache MCP tool-discovery specs to avoid
|
||
redundant RPC round-trips (#704, thanks @shadowinlife).
|
||
|
||
### Fixed
|
||
- Explicit `source: local` backtests now route US/HK equities to the
|
||
global-equity engine instead of the crypto default, and explicit benchmarks
|
||
are fetched through the configured source's loader — `local` fails closed
|
||
(no yfinance fallback) so offline runs stay offline (#550).
|
||
- Loading `.env` now invalidates an `EnvConfig` singleton cached during early
|
||
CLI imports, so the welcome panel, `/settings`, and dotenv diagnostic report
|
||
the configured provider and model consistently (#541).
|
||
- FastMCP transport imports work across both module layouts (#469, thanks
|
||
@roberttidball).
|
||
- Portfolio optimizers no longer include the decision bar's close-to-close
|
||
return in weights executed at that bar's open (#487, thanks @YZY0108).
|
||
- Backtest turnover metrics now use actual filled and rounded position sizes;
|
||
targets rejected by market rules no longer inflate reported turnover.
|
||
- End-of-backtest liquidations now apply exit slippage and include their
|
||
commission in the final reported equity.
|
||
- Open-price rebalances no longer use the decision bar's close for sizing or
|
||
depend on whether a replacement symbol sorts before the position it closes.
|
||
- Preflight (`vibe-trading run`) no longer resolves provider/model against a
|
||
stale `EnvConfig` snapshot cached before dotenv loads (#479, thanks
|
||
@ananaymital, closes #477).
|
||
- Switching providers no longer leaves a stale `OPENAI_BASE_URL` from a
|
||
previous configuration silently overriding the newly-resolved endpoint
|
||
(#484, thanks @Bortlesboat, closes #482).
|
||
- **Strict-JSON / finite-number hardening** across the backtest + tools stack
|
||
(thanks @santhreal): risk ratios stay finite when equity crosses zero mid-path
|
||
(#765) or annualizes an explosive path (#739/#740); scalar backtest metrics
|
||
(#766), factor IC std (#767), and pattern trend-slope (#764) emit strict
|
||
RFC-8259 JSON (`null`, never bare `NaN`/`Infinity`); Black-Scholes helpers
|
||
treat non-positive spot/strike as intrinsic (#744).
|
||
- **Loader / data correctness** — yahoo `1m` stays minute bars instead of being
|
||
uppercased to monthly (#761, @santhreal); the composite engine falls back to
|
||
the first available sub-engine for unknown symbols (#734, thanks @Marnie0415);
|
||
mootdx history that doesn't reach the requested start is rejected (#692, thanks
|
||
@xkam7ar).
|
||
- **Session / journal robustness** — one corrupt `session.json` (#762) or a
|
||
schema-bad `messages.jsonl` line (#763) no longer aborts listing/reading;
|
||
Excel float-stringified A-share codes (#770), unicode-dash PDF page ranges
|
||
(#769), and `export KEY=` dotenv lines (#768) all parse correctly (@santhreal).
|
||
- **Native `zai` provider on glm-5.1** (#758) — endpoints that stream zero
|
||
chunks fall back to a non-streaming invoke instead of raising, and an HTML
|
||
error page surfaces an actionable base-URL hint.
|
||
- Partial market-data results are completed through the loader fallback chain
|
||
instead of silently shrinking the universe (#689, closes #681).
|
||
- Cancellation is honored before the first AgentLoop iteration (#641, thanks
|
||
@xkam7ar, closes #638); streaming output no longer triggers an `insertBefore`
|
||
DOM race in the frontend (#717, thanks @Marnie0415); codex stream HTTP
|
||
failures are classified for correct retry (#663, thanks @tyj147454413-cmd).
|
||
- Robinhood connector `account_number` wiring and remote-MCP display shape
|
||
(#726, thanks @nareshkps).
|
||
- Broad reliability fixes across packaging, web, scheduler, swarm, and CLI
|
||
(#584, thanks @xkam7ar).
|
||
|
||
## [0.1.11] — 2026-07-11
|
||
|
||
### Added
|
||
- **Indian equity (NSE/BSE) as a first-class market** (#305, thanks
|
||
@muku314115). A dedicated `IndiaEquityEngine` — T+1 delivery, no overnight
|
||
shorts (opt-in intraday), configurable circuit bands, 1-share lots, and a
|
||
config-driven STT / stamp-duty / exchange / SEBI / GST cost stack — with
|
||
`.NS`/`.BO` symbol routing (`yahoo → yfinance → india_broker → local`), an
|
||
opt-in read-only Shoonya/Dhan `india_broker` data bridge, and 255
|
||
alpha101/qlib158 factors opted into the new `equity_in` universe. Backtest
|
||
engines: 7 → **8**; market-data sources: 19 → **20**.
|
||
- **Fundamental factor layer, Phase 1.** PIT-safe SEC fundamentals flow into
|
||
dense daily `fund:*` factor panels — filed-date anchoring, first-filed
|
||
restatement policy, true-quarter `(start, end)` frame selection with Q4
|
||
synthesis (so YTD/annual frames can't contaminate TTM), and rolling TTM —
|
||
plus a `get_fundamentals` tool and 4 quality/value factors in a new
|
||
`fundamental` zoo family. Alpha Zoo: 456 → **460** across **5** families.
|
||
- **Research Autopilot Phase 3 — the loop closes** (#267, thanks
|
||
@Robin1987China). `scaffold_signal_engine` writes a contract-correct signal
|
||
engine from a hypothesis and `link_autopilot_backtest` runs it, completing
|
||
hypothesis → signal-engine → backtest end to end.
|
||
- **4 canonical academic alphas** (#277, thanks @Robin1987China) — Jegadeesh
|
||
short-term reversal, George–Hwang 52-week high, Amihud illiquidity, and
|
||
Harvey–Siddique co-skewness join the academic family (452 → 456), with a
|
||
**central OHLC-invariant guard** at the runner fetch boundary dropping
|
||
malformed bars from every loader (#274, thanks @Shizoqua).
|
||
- **Scheduled research runs end to end** (#278 closing #254, thanks
|
||
@mvanhorn). A default-off background executor
|
||
(`VIBE_TRADING_ENABLE_SCHEDULER`) fires due interval/cron jobs through the
|
||
session runtime, on top of a crash-safe atomic job store, 3 auth-gated
|
||
`/scheduled-runs` REST routes, a Reports library, and post-backtest
|
||
attribution. Route test coverage followed in #452 (thanks @Robin1987China).
|
||
- **IM channel runtime — research delivery over 16 adapters.** The agent
|
||
session runtime now attaches to 16 built-in message adapters (WebSocket,
|
||
Telegram, Slack, Discord, Matrix, WhatsApp, Signal, QQ/NapCat,
|
||
WeChat/WeCom, Feishu, DingTalk, email, MS Teams, MoChat), dependency-gated
|
||
with install hints, configurable via `AgentConfig.channels`, and surfaced in
|
||
REST (`/channels/*`), CLI (`vibe-trading channels ...`), and Web Settings —
|
||
in all 5 UI locales.
|
||
- **QVeris optional premium data track.** The 19 free sources stay the
|
||
default; an explicit-only QVeris mode (Settings → QVeris or
|
||
`vibe-trading data mode paid`) unlocks 63+ providers behind 3 key-gated
|
||
tools (`qveris_search` / `qveris_inspect` / `qveris_execute`) with
|
||
preview-by-default and a session budget gate. Never enters auto-fallback.
|
||
- **Trading 212 read-only connector** (#321, thanks @mvanhorn) — 11 brokers
|
||
total. Trading 212 exposes no runtime paper/live discriminator, so the
|
||
connector is fully read-only: `place_order`/`cancel_order` hard-refuse
|
||
every order, paper included. Live order guards also gained an opt-in,
|
||
broker-agnostic `PreTradeAdvisoryInterface` that records advisory reviews
|
||
without bypassing the mandate gate.
|
||
- **Turnover-aware portfolio optimizer** (#466, thanks @Robin1987China) —
|
||
fifth optimizer: mean-variance utility with an L1 penalty on weight changes
|
||
versus the previous rebalance (SLSQP, long-only simplex), so the portfolio
|
||
only trades when expected improvement outweighs churn. Optimizers: 4 → **5**.
|
||
- **`analyze_image` vision tool** (#464, thanks @fei-moss) — send a local
|
||
chart / K-line screenshot / app screenshot to the session model as a
|
||
multimodal message and get a semantic read (complements `read_document`'s
|
||
OCR). Path-validated against the allowed file roots; requires a
|
||
vision-capable model. Tools: 71 → **72** free-mode (75 with QVeris).
|
||
- **Value-investing toolkit** (thanks @sambazhu): financial-rigor +
|
||
report-audit tools, 4 skills, and a `value_investing_committee` swarm
|
||
preset. Swarm presets: 29 → **30**.
|
||
- **CN-friendly search fallbacks** — `web_search` gains China-reachable
|
||
backends in the ordered no-key engine chain.
|
||
- **Provider roster additions**: Kimi for Coding as a distinct provider
|
||
(#435, thanks @yxhuang), opencode provider mappings (#444, thanks
|
||
@imsankz), Codex OAuth default model bumped to `openai-codex/gpt-5.4`
|
||
(#446, thanks @morluto).
|
||
- **SKILL.md manifest guard test** (#461, thanks @asahikiko) — the packaged
|
||
skill's capability counts (skills / presets / zoo / sources / MCP tools /
|
||
engines) are now asserted against source, so distribution paperwork can't
|
||
silently drift again.
|
||
|
||
### Changed
|
||
- **`api_server.py` modularization completed** — 1,103 → 371 lines (#424
|
||
closing #331, thanks @shadowinlife), after route slices for channels,
|
||
settings, and the remaining route groups, plus a shared compat layer with
|
||
session-service writeback fixes.
|
||
- **Centralized environment variable management** (#440 closing #438, thanks
|
||
@shadowinlife) — every env var flows through a single Pydantic `EnvConfig`
|
||
schema, enforced by an AST-based CI gate that rejects raw
|
||
`os.getenv`/`os.environ` outside `agent/src/config/`.
|
||
- **Factor engine acceleration** — hot rolling operators use
|
||
`bottleneck`/NumPy fast paths, and alpha-bench parallelism stops resending
|
||
large panel payloads to workers.
|
||
- **Robinhood Agentic MCP refresh** — current MCP tool names across generic
|
||
reads, live-runner plumbing, default read-only seeds, and mandate-gate
|
||
tests; interactive OAuth holds the handshake open through multi-minute
|
||
broker sign-ins (`VIBE_LIVE_AUTHORIZE_TIMEOUT_SECONDS`).
|
||
- **Loader `fetch()` signatures** now match the loader protocol across
|
||
OKX / Tushare / yfinance (#437, thanks @shadowinlife).
|
||
- **Timezone-aware UTC timestamps** across session, goal, channel, and API
|
||
paths (#397, thanks @mustafakamal88).
|
||
- **Inbound IM media now lands under `~/.vibe-trading/uploads/<channel>/`**
|
||
(fixes #465, thanks @fei-moss for the report) — inside the default allowed
|
||
file roots, so the agent can read what users send over IM channels with
|
||
zero configuration. The Matrix E2E store moves to the runtime dir (legacy
|
||
path honored) so credentials never enter the readable root.
|
||
|
||
### Fixed
|
||
- **Docker/server startup crash** when FastAPI route iteration hit an
|
||
included-router entry without `path` (#450, thanks @Penn-Live).
|
||
- **GLM thinking models on the zhipu provider** no longer lose their
|
||
reasoning stream (#458).
|
||
- **Trading mandate UX guards** — a second-confirmation dialog before
|
||
committing a real mandate, unified error toasts, and clarified inputs
|
||
(#453, thanks @wison1717-maker).
|
||
- **`trading_place_order` treats zero quantity/notional as unset** instead of
|
||
passing a zero-size order to the broker (#417, thanks @irfanallana-oss).
|
||
- **Longbridge Decimal values serialize as floats** across quotes, bars,
|
||
balances, positions, orders, and executions (#459, thanks @fanfpy).
|
||
- **NapCat private messages now trigger pairing codes** (#463, thanks
|
||
@fei-moss).
|
||
- **Backtest validation artifacts**: `validation.json` no longer requires a
|
||
pre-existing artifacts dir (#429, thanks @isaveall), and nested
|
||
`NaN`/`Infinity` values are normalized before writing, so strict JSON
|
||
parsers don't choke.
|
||
- **CLI**: `resume` preserves the first user message (#448 closing #447,
|
||
thanks @morluto); `--swarm-run` rejects extra tokens with a clear error
|
||
(#428, thanks @isaveall); the interactive CLI prints the session-id on
|
||
exit with a copy-paste resume hint.
|
||
- **Shadow Account**: extracted rules carry RSI / prior-return entry bounds
|
||
computed from PIT-safe context fetched through the loader registry, so
|
||
generated engines enter on real conditions (#302/#314/#316, thanks
|
||
@Robin1987China); tushare ETF/index/HK symbol routing fixed along the way.
|
||
- **Content-filter resilience** — event-driven and swarm runs skip individual
|
||
LLM content-moderation hits and warn when filter rates are high; Gemini
|
||
safety finish-reasons recognized.
|
||
- **IM channel reply timeout is configurable** (#413, thanks @dpersek).
|
||
- **Provider preflight no longer follows redirects** (#404 closing #402,
|
||
thanks @dpersek).
|
||
- **Windows baseline green** — `vibe-trading setup`/`dev` handle Windows
|
||
TypeScript builds, correct cwd, the Vite 5899 port, and child-process
|
||
shutdown; mootdx batch pulls let `KeyboardInterrupt`/`SystemExit`
|
||
propagate.
|
||
- **Security hardening** — loopback API CSRF protection (cross-site POSTs
|
||
can no longer drive side effects on the local API), SSRF guards on
|
||
interactive fetch paths, tightened API/Docker/frontend dev defaults, and
|
||
cleared frontend dependency/CSP alerts.
|
||
- **Reverted the IRR-AGL reliability/governance stack** (#405/#416) after it
|
||
broke session chats on day 1 (#433 — thanks @yxhuang for the precise
|
||
diagnosis); the evidence-bound research-pipeline direction continues in
|
||
reviewable slices on #442.
|
||
|
||
## [0.1.10] — 2026-06-19
|
||
|
||
Roll-up release; see the
|
||
[v0.1.10 release notes](https://github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.10)
|
||
for the full narrative.
|
||
|
||
### Added
|
||
- **Global data layer** — market-data sources 10 → 18 (direct-API Eastmoney /
|
||
Sina / Stooq / Yahoo + key-gated Finnhub / Alpha Vantage / Tiingo / FMP)
|
||
with ban-risk-ordered fallback chains behind a shared throttled HTTP gate,
|
||
plus **18 read-only data tools** (fund flow, dragon-tiger, northbound,
|
||
margin, block trades, shareholder count, lockup, sector, research, news,
|
||
SEC filings, financial statements, options chains, institutional holdings,
|
||
screening, symbol search, FRED macro, iwencai) — all MCP-exposed.
|
||
- **10 broker SDK connectors** — Tiger / Longbridge / Alpaca / OKX / Binance /
|
||
Futu / Dhan / Shoonya join IBKR (local read-only) and Robinhood (Agentic
|
||
MCP); direct-SDK live orders pass the fail-closed bounded-autonomy gate;
|
||
brokers without a runtime paper/live discriminator are structurally capped
|
||
at paper + read-only.
|
||
- **Alpha Zoo `alpha compare`** across CLI, REST, Web UI, and agent tool.
|
||
- **Research Autopilot Phase 1** (`run_research_autopilot`,
|
||
`generate_backtest_config`) and the local Data Bridge loader.
|
||
- **Opt-in local data cache** (`VIBE_TRADING_DATA_CACHE`) for settled bars
|
||
under `~/.vibe-trading/cache/`.
|
||
- Per-run token usage (`llm_usage.json`) + progressive Run Detail charts;
|
||
CLI `resume <session-id>`.
|
||
|
||
### Changed
|
||
- **Provider reliability overhaul** — DeepSeek hang fixes, Kimi access,
|
||
streaming liveness watchdog, Gemini 3.x multi-turn tool-calling fix.
|
||
- Swarm workers pull market data through the loader layer; live swarm status
|
||
cards stream in the chat timeline.
|
||
- Baseline install slimmed (`pyharmonics`/`ta` behind
|
||
`vibe-trading-ai[harmonic]`).
|
||
|
||
### Fixed
|
||
- Community security-hardening wave (#241–#258): Settings write auth, shell
|
||
tools opt-in, LAN-access 403 clarity, Docker-to-host Ollama URL rewrite,
|
||
web_search multi-engine fallback, and more.
|
||
|
||
## [0.1.9] — 2026-06-01
|
||
|
||
### Added
|
||
- **Connector-first broker profiles (IBKR + Robinhood).** Trading access now
|
||
starts from a selectable connector profile rather than separate broker/live
|
||
entry points; `vibe-trading connector list/use/check/account/positions/orders/quote/history`
|
||
and the MCP `trading_*` tools share the selected profile, with paper/live as
|
||
a property under the connector. IBKR is usable immediately as a local
|
||
read-only TWS / IB Gateway profile; the official IBKR remote MCP path is
|
||
seeded as an OAuth `mcp.read` probe until stable read tool names ship.
|
||
Robinhood Agentic Trading is a bounded connector behind OAuth, a committed
|
||
mandate, an order guard, an audit ledger, and an instant halt switch.
|
||
- **Research Goal runtime.** Long-running, research-only goals with auditable
|
||
checklist criteria, budgets, and a `/goal` CLI command, plus REST + MCP
|
||
endpoints (`start_research_goal`, `get_research_goal`, `add_goal_evidence`,
|
||
`update_research_goal_status`) and a Web `GoalDrawer`.
|
||
- **Swarm `retry_run`.** Re-launch a failed/stale/cancelled run with the
|
||
original preset + variables; exposed as both `POST /swarm/runs/{id}/retry`
|
||
and an MCP `retry_run` tool (the `list_runs → retry` loop). 36 MCP tools now.
|
||
- **Operator-configured external MCP tools in swarm workers** (#142) and
|
||
**remote MCP transports** for the built-in agent.
|
||
- **`mootdx` A-share OHLCV loader** — native 通达信 TCP, no token, sits between
|
||
tushare and akshare in the fallback chain. CCXT loader now reads proxy env
|
||
for restricted networks (#126).
|
||
- **Hypothesis Registry CLI** — `list / show / invalidate`.
|
||
- **Strict alpha-bench mode** with a mandatory random control (#143).
|
||
|
||
### Changed
|
||
- **CLI split into the `agent/cli/` package** (from a 3216-LOC single file),
|
||
with a refreshed interactive terminal UI (figlet banner + activity rail) and
|
||
a single `cli/_version.py` version source.
|
||
- Swarm status reconciles from live task files on every read; `run_swarm`
|
||
sends MCP progress heartbeats, and the stale-run reaper uses per-run
|
||
thresholds (#132).
|
||
- Refreshed provider default model ids; bumped `langgraph` for CVE-2026-28277.
|
||
|
||
### Fixed
|
||
- **`--version` no longer drifts (#156).** The version derives from package
|
||
metadata, falling back to reading `pyproject.toml` directly — no hardcoded
|
||
constant left to forget on release.
|
||
- **Session running-status indicator** now survives reconnect / page reload /
|
||
sidebar navigation; **swarm DAG** blocks downstream tasks when an upstream
|
||
task fails (#145).
|
||
- **Robustness pass:** pre-flight validation for LLM-generated signal engines
|
||
with clean JSON errors (#149), graceful agent-loop exit at the iteration
|
||
budget instead of an output-less `failed` (#148), `flush + fsync` session
|
||
message writes that skip corrupted JSONL lines on read (#147), and IME Enter
|
||
handling in the Web composer (#146).
|
||
- **Full Report** link now always renders when a `runId` exists, even cross-browser
|
||
(#150); SSE idle timeout is configurable via `VIBE_TRADING_SSE_TIMEOUT` (#157);
|
||
cross-market correlation normalizes timestamps so crypto-vs-equity pairs align (#158).
|
||
|
||
## [0.1.8] — 2026-05-17
|
||
|
||
### Added — Alpha Zoo (450+ pre-built quant alphas)
|
||
- `agent/src/factors/` — base operators (`rank`, `scale`, `ts_*`, `delta`,
|
||
`decay_linear`, `signed_power`, `safe_div`, market-aware `vwap`) and a
|
||
registry that AST-extracts metadata from each alpha module without
|
||
importing it. Lookahead is enforced at the operator level
|
||
(`delta(d>=1)`), and registry sanity checks reject `+/-inf` and
|
||
outputs that are more than 95 % NaN.
|
||
- 4 zoos shipping 452 alphas total:
|
||
- **qlib158** (154 alphas) — port of Microsoft Qlib's `Alpha158`
|
||
feature handler under Apache-2.0, with pinned commit SHA per file.
|
||
- **alpha101** (101 alphas) — implementation of Kakushadze (2015)
|
||
*"101 Formulaic Alphas"* (arXiv:1601.00991), written from the paper
|
||
appendix; the relevant trademarked string is intentionally absent.
|
||
- **gtja191** (191 alphas) — implementation of Guotai Junan's 2014
|
||
*"191 Short-period Trading Alpha Factors"* research report.
|
||
- **academic** (6 factors) — Fama-French 5 + Carhart momentum, shipped
|
||
as honest price-based proxies (not the canonical FF series).
|
||
- `vibe-trading alpha {list,show,bench,compare,export-manifest}` CLI
|
||
subcommand. `show` and `export-manifest` enforce path-traversal guards.
|
||
- New agent tools: `AlphaZooTool` (browse) and `AlphaBenchTool`
|
||
(orchestrator with Jinja2 autoescape + strict CSP HTML report).
|
||
- `ZooSignalEngine.from_zoo(...)` — composite multi-factor signal engine
|
||
with cross-sectional standardisation, weighting, and optional top-N /
|
||
bottom-N long-short conversion.
|
||
- `wiki/scripts/build_alpha_library.py` — Alpha Library renderer.
|
||
Reads `manifest.json` produced by `vibe-trading alpha export-manifest`
|
||
and emits 452 per-alpha HTML pages plus 4 per-zoo overviews, each with
|
||
`script-src 'none'` CSP. The landing page hydrates per-zoo counts
|
||
from `content/index.json`.
|
||
- New blog post: *"Which of the 191 GTJA alphas still work in 2026?"*
|
||
with aggregate IC statistics, theme breakdown, and the top alphas
|
||
that survive eight years of out-of-sample data.
|
||
|
||
### Added — Web UI for Alpha Zoo
|
||
- New page at `/alpha-zoo` in the Vite + React frontend with three
|
||
views: browse (4 zoo cards + filter bar + paginated table), detail
|
||
(formula, metadata, collapsible source code), and bench-runner
|
||
(form → SSE-streamed progress + Alive/Reversed/Dead stat cards +
|
||
Top-5-by-IR table + by-theme breakdown chart). "Alpha Zoo" nav
|
||
entry added to the layout.
|
||
- Four new REST routes in the FastAPI server:
|
||
- `GET /alpha/list` — filterable alpha catalogue
|
||
- `GET /alpha/{alpha_id}` — meta + source code
|
||
- `POST /alpha/bench` — kicks off a background bench job and
|
||
returns a `job_id`
|
||
- `GET /alpha/bench/{job_id}/stream` — Server-Sent Events with
|
||
`progress`, `result`, `done`, and `error` event types. In-memory
|
||
job state with a 1-hour TTL; no Redis/Celery dependency.
|
||
- Bench math is refactored into `agent/src/factors/bench_runner.py`
|
||
so the CLI driver (`agent/scripts/w4a_run_benches.py`) and the new
|
||
API worker share a single implementation.
|
||
|
||
### Added — Safety floor
|
||
- `agent/tests/factors/test_alpha_purity.py` — AST allowlist scan over
|
||
every `zoo/**/*.py` module (whitelist: pandas, numpy, scipy.\*,
|
||
`src.factors.base`, `__future__`, `typing`, `math`, `dataclasses`;
|
||
banned: `os`, `sys`, `subprocess`, `socket`, `urllib`, `requests`,
|
||
`httpx`, `pathlib`, `Path`, `open`, `eval`, `exec`, `compile`,
|
||
`__import__`, and `getattr(_, "__*")`).
|
||
- `agent/tests/factors/test_lookahead.py` — sentinel future-row
|
||
injection on a 300-row synthetic panel; corrupting rows after the
|
||
probe must leave the probe value unchanged within 1e-9.
|
||
- `tools/ci_grep_gates.sh` — CI gate that rejects `yaml.load(` without
|
||
`safe_load`, any trademarked-name leak in shipped artifacts, and any
|
||
per-stock-code data leak in `wiki/**/*.{json,csv,html}`.
|
||
- `agent/tests/factors/conftest.py` — opt-in `pytest-socket` integration
|
||
that hard-fails any test attempting outbound network during the
|
||
factors test suite.
|
||
|
||
### Added — Community governance
|
||
- `CONTRIBUTING.md` — Developer Certificate of Origin sign-off
|
||
requirement and a contributor checklist for new alpha PRs (purity,
|
||
lookahead, `__alpha_meta__` shape, LaTeX-matches-code, per-zoo
|
||
LICENSE.md, DCO).
|
||
- `NOTICE` (repo root) — Apache-2.0 attribution for Qlib and a
|
||
declaration that the bundled formulas from Kakushadze, GTJA, and the
|
||
academic baselines are mathematical content (paper prose, tables, and
|
||
figures are not reproduced here).
|
||
- Per-zoo `LICENSE.md` for each of `qlib158/`, `alpha101/`, `gtja191/`,
|
||
and `academic/`, plus an upstream `NOTICE` for `qlib158/`.
|
||
|
||
### Changed
|
||
- `agent/src/tools/factor_analysis_tool.py` extracted its IC/IR and
|
||
layered-backtest helpers to `agent/src/factors/factor_analysis_core.py`
|
||
so the new `alpha_bench_tool` reuses the same maths. Public tool
|
||
signature is unchanged; `_compute_ic_series` and `_compute_group_equity`
|
||
remain importable as backward-compatible aliases.
|
||
- `agent/cli.py` grew by 7 lines to register the `alpha` subcommand;
|
||
all handler logic lives in `agent/src/factors/cli_handlers.py`.
|
||
- Packaging: `pyproject.toml` now ships `zoo/**/*.yaml`, `zoo/**/*.md`,
|
||
and `zoo/**/NOTICE` as package data; `MANIFEST.in` recursively
|
||
includes `agent/src/factors`.
|
||
|
||
### Known limitations
|
||
- The `btc-usdt` universe is single-asset; cross-sectional IC requires
|
||
≥2 instruments, so the bundled `alpha101_btc` bench run returns
|
||
alive/reversed/dead = 0/0/0 by construction. Use a multi-symbol crypto
|
||
basket (e.g. BTC + ETH + SOL + the top-N perpetuals) for meaningful
|
||
cross-sectional results; a curated `crypto-majors` universe is planned
|
||
for 0.2.
|
||
|
||
### Internal
|
||
- `wiki/alpha-library/manifest.json` and `wiki/alpha-library/content/`
|
||
are generated artifacts and gitignored. Run
|
||
`vibe-trading alpha export-manifest --out wiki/alpha-library/manifest.json
|
||
--force` followed by `python wiki/scripts/build_alpha_library.py` to
|
||
regenerate the static site.
|