v0.1.17
HKUDS/Vibe-Tradingv0.1.17Oct 21, 2026by warren618
Upgrading?
Full Release Notes
Rolls up 638 commits / 228 merged pull requests since 0.1.16, plus 41 pull
requests whose work landed through maintainer commits, from 33 contributors.
The headline is daily paper trading. A `paper_trade_step` tool for the agent
and MCP, and saved paper accounts through `vibe-trading paper`,
`/api/paper-accounts` and a Web page, match orders or a strategy against
completed daily bars without ever reaching a broker; saved accounts move
forward only. Most of
the rest of the cycle is correctness, and much of it has one shape: a value
that cannot be computed now says so instead of turning into a plausible
number. A profit factor with no losing trade, an undefined correlation, a
factor benchmark with too few IC observations, an upside at a zero price and
an unbounded ratio come back as `null` or a refusal rather than 0; a missing
price no longer bridges a return pair, a VaR transition or a lag pair; an
after-close SEC filing no longer reaches the IC a bar early; and a misspelled
backtest setting is reported in the run card instead of silently ignored.
Brazil (B3) joins as a market, the Alpha Zoo grows from 462 to 478 factors,
Slack and Discord join the guided channel setup, scheduled reports can
arrive by Email as HTML or a password-protectable PDF, token usage shows in
Run Detail, chat and swarm, a `claude-cli` provider runs the agent on a local
Claude Code login, and the Web UI adds Persian and Turkish for 11 languages.
### Added
- **Daily paper trading with saved accounts** (#1939, thanks @shadowinlife). A
new `paper_trade_step` agent and MCP tool matches explicit orders, or the
per-day orders of a strategy file, against completed daily bars for `cn_a`,
`hk` and `us`; nothing in it can reach a broker order path. The same
stateless step is served as `vibe-trading paper step` and
`POST /api/paper-trade/step`. Saved paper accounts are built on it and
reachable three ways: `vibe-trading paper create|list|show|history|advance`,
authenticated REST under `/api/paper-accounts` (list, create, read,
history, `POST .../{id}/advance`) and a **Paper trading** page in the Web
sidebar in all eleven UI languages.
An account starts from the cash and positions you enter, today or at the
next session, and moves forward only: an advance settles sessions whose
regular close is at least 30 minutes past and carries the revision it read,
so a stale or concurrent request is refused (HTTP 409 over REST) instead of
committing twice, and a failed step leaves the ledger unchanged. Ledgers
live in `<runtime_root>/paper-trading/accounts.db`; there is no background
scheduler. A strategy under `<runtime_root>/strategies/` runs in a child
process that sees only bars strictly before the execution day and the ledger
after earlier fills; orders fill at that day's close, and any strategy
failure discards the whole call. Matching is daily-close and all-or-none,
with simplified fees, price limits, board lots and A-share T+1. Execution
prices must be raw: adjusted or unknown price calibers and non-finite prices
are rejected, A-shares need a complete ClickHouse trading calendar, and a
split-like jump returns `PAPER_DATA_NOT_READY` rather than rescaling
holdings. Corporate-action reconciliation and cash dividends are not
modelled and NAV is not total return; intraday, margin, options and
cross-market phases remain open under #1938.
- **Live runner model calls have their own cadence and daily budget**
(closes #1854). Safety checks still run every minute, but a model
decision now defaults to every 15 minutes, each in a fresh conversation,
with at most 26 decisions and 1,000,000 reported tokens per broker per
UTC day; an optional once-daily time, timezone and operator-priced USD
limit are set in Runtime → Model schedule and spending limits, the chat
runner panel, `vibe-trading connector runner-settings` or
`GET`/`PUT /live/runner/settings`. Spending history survives restarts.
Limits are checked after each provider response, so one response can
overshoot; estimates ignore cache discounts and auxiliary calls and are
not a provider billing cap. Exhaustion or missing usage trips the
existing kill switch and its cancel/flatten behaviour, and a decision
left unfinished by a crash is blocked for inspection.
- **Strategy adaptation runs from preparation to evidence**. A registered
strategy with adequate, non-stale evidence can be adapted to a new name,
universe, date window and position sizing through `adapt_strategy` (agent
and MCP), `vibe-trading strategy-evidence adapt`, authenticated `POST
/api/strategy-adaptations` and a Web welcome prompt. The SDM child keeps the
parent's signal, entry and exit definitions and gets its own run directory,
SignalEngine and backtest; it never inherits parent metrics. Refreshing with
`scope=strategies` (`strategy-evidence refresh --scope strategies`, or `POST
/api/strategy-adaptations/evidence`) replaces only the named strategies'
evidence, and refuses an adapted run unless its config, code, metrics,
equity and trades match the hashes sealed in its run card. The hash binds
evidence to the persisted run; it does not certify that the generated code
implements its description.
- **extraETF holdings exports feed the portfolio, with allocation proposals**.
`vibe-trading portfolio import-extraetf`, `POST
/api/portfolio/imports/extraetf` and **Portfolio → Manage accounts → Import
extraETF Investments** store an Investments CSV as a local source. The whole
import fails on an unreadable row, a holdings count that differs from the
one you confirm, a missing price, a future file time or an export older than
the freshness limit you choose; transactions, dividend and X-Ray exports are
refused. `Portfolio ID` keeps accounts apart when they hold the same ISIN.
Totals are exported quantity × price and exclude cash and assets absent from
the export. EUR joins USD, CNY and HKD as a display currency, valued at
Frankfurter reference rates whose date is shown apart from the fetch time.
An allocation proposal (`portfolio rebalance --snapshot-id`, `POST
/api/portfolio/rebalance`, `portfolio_summary(snapshot_id=...,
target_weights=...)`, or the Web page) shows value changes for target
percentages that total 100%; it creates no orders, quantities, tax or fee
estimates, and an incomplete snapshot or stale cached rates cannot produce
one. Imported holdings stay out of historical risk tools until their trading
venue is resolved.
- **Portfolio research can stay on one snapshot** (#1769, thanks @zeus229).
`portfolio_summary` returns `snapshot_id` and `read_identity` and accepts a
`snapshot_id` back, so a multi-step analysis keeps reading the same stored
observation after a later refresh. `vibe-trading portfolio show
--snapshot-id ID` and `GET /api/portfolio/analysis-context?snapshot_id=`
read the same way, and the Web Portfolio page exports the sanitized research
snapshot it displays as JSON. An unknown snapshot, one valued under an older
valuation version, or one containing a source that is no longer enabled is
reported as unavailable and never replaced by newer data.
- **mStock (India) read-only connector** (#1675, thanks @Jackzigen). The
`mstock-live-rest-readonly` profile sends only GET requests for the Type B
holdings, net-position and fund-summary resources, with the API key and
current session token kept in the connection's OS credential vault; it never
logs in, refreshes a session or calls an order endpoint, and both order
entry points refuse before any network call. Holdings and net positions stay
separate books. Fund limits are shown raw because they are not cash or net
liquidation, so the profile withholds `account.read` and is not a portfolio
source. `connector account` and `connector positions` gain
`--connection-id`. Tests replay the documented reply shapes; no real mStock
account was available.
- **Five example options strategies and a candidate study** (#1774, thanks
@mdb1230). The options-strategy skill ships covered call, cash-secured put,
iron condor, volatility-gated short strangle and protective put
`OptionsSignalEngine`s, and `agent/scripts/options_candidates_backtest.py`
picks one parameter per strategy on 2016–2020 SPY/QQQ, then reports 2021
onward on SPY, QQQ, AAPL and MSFT with those parameters fixed. This is
research code for choosing what to paper-trade; no engine changed and no
order path exists.
- **Custom OpenAI-compatible model endpoints** (#1264, thanks @Emad211).
Settings gains a custom-provider manager and the CLI gains `vibe-trading
provider custom list|add|activate`. A profile is saved only after a test
chat completion with the same URL, model and key succeeded in the last five
minutes; the key goes to the OS credential vault (the `[keyring]` extra),
never to the profile JSON or argv, and activation needs an explicit
confirmation. An active profile cannot be edited: save the changed endpoint
under a new id. Base URLs carrying credentials, and hosts that are or
resolve to any non-public address (loopback, private, link-local, reserved,
or the shared 100.64.0.0/10 range that holds Alibaba Cloud's metadata
service), are refused, so a model served on localhost cannot be registered
this way. The host is checked when the profile is tested and saved; a name
that later re-resolves to a private address is not re-checked. The test is
a real request and may be billed. `LANGCHAIN_MAX_TOKENS`, when set, now caps
output tokens on the OpenAI-compatible adapter. The PR's per-channel
configuration API was not merged; channel settings stay where 0.1.16 put
them.
- **Persian and Turkish join the Web UI, which now has 11 languages** (#1264,
#1616, thanks @Emad211 and @bygordon130-hub). Persian (`fa`, right-to-left)
and Turkish (`tr`) are registered in `SUPPORTED_LANGUAGES` and the language
switcher, and all 11 locale files carry the same keys. The Turkish file
arrived with English placeholders and UTF-8 decoded as Windows-1252; both
were replaced before release, and a test now rejects copied-English Turkish
strings and that mojibake in `tr` and `fa`. The custom-provider strings,
first merged in English in every locale, are translated in all 11. Chat
messages, the composer and error lines take their direction from their own
text, so a Persian or Arabic message reads right-to-left inside an English
UI and the reverse. Some strings are still English in some locales.
- **Pixel Office, a shared manual research-task board** (#1616, thanks
@bygordon130-hub). The `/pixel-office` page, the `vibe-trading office` CLI,
authenticated `GET /api/pixel-office` and `POST /api/pixel-office/actions`,
and the `pixel_office` agent tool share one transactional SQLite board
(`pixel-office.sqlite3` under the runtime root, mode 0600), so separate
processes see the same tasks. Tasks are created, assigned to a role,
completed with a required result, and roles carry Operator / Senior / Lead
ranks. Assigning a task does not start an agent or place an order, ranks
grant no trading permission, and a recorded result is not an execution
audit. The PR proposed a JSON file plus browser state; the merge replaced it
with this shared store. The unsigned Windows review installer now builds the
bundled Python backend before packaging.
- **Scheduled jobs are edited in place, with channel-aware destinations**
(#1649, thanks @zeus229). `PATCH /scheduled-runs/{id}` and the Scheduled
page change a job's prompt, cadence, timezone, end date and delivery without
deleting it, so its id and history stay. A job that is running, or whose
delivery is being sent, refuses edits with 409. Each channel adapter
describes the destination it accepts (label, placeholder, input type), and
Telegram suggests numeric private-chat ids already in its `allow_from`; a
suggestion only fills the manual target field. Changing the destination
clears the old delivery receipt but keeps the session link, and a
timezone-only edit does not move an interval schedule's next run.
- **Scheduled Email reports as HTML or a PDF attachment** (#1680, thanks
@zeus229). Email delivery takes a format: plain text (the default),
sanitized HTML (Markdown rendered by mistune and cleaned by `nh3`), or a PDF
attachment with a short notice in the body. It is chosen on the Web
Scheduled page, in CLI playbooks (`--delivery-format`) and in agent
proposals, whose confirmations show it. A PDF that fails to render fails the
send instead of going out as plain text. Where WeasyPrint's native libraries
are missing, the PDF comes from the bundled ReportLab renderer with embedded
fonts, so desktop installs get PDFs too. A scheduled send now fails visibly
when the Email channel lacks consent or an SMTP host instead of being
skipped with a log line. The format selector is translated in every UI
locale.
- **Password-protected PDF reports** (#1709, thanks @zeus229). An operator
sets a private `pdf_password` on the Email channel; a PDF Email job can then
request protection, and the attachment is encrypted with AES-256 through
`pypdf`. Web, CLI playbooks (`--protect-pdf`) and agent proposals store only
the yes/no choice, a job config containing `pdf_password` is rejected,
confirmations show the choice, and creating, editing or confirming a
protected job without a configured password is refused. An encryption
failure never echoes the password.
- **The agent can save a report as a real PDF**. Before this, `write_file`
wrote Markdown text into a file named `.pdf`. A `.pdf` path now renders the
Markdown into a paginated PDF with ReportLab, without Pango or Cairo. Text
uses a bundled Noto Sans CJK subset (`VibeCJK-Regular.ttf`, OFL) covering
Latin, Greek, Cyrillic, CJK, kana and Hangul; Arabic lines are shaped with
`arabic-reshaper` and `python-bidi` (now base dependencies) in
matplotlib's DejaVu Sans and wrap across pages without clipping. A
character neither font covers fails the render instead of printing empty
boxes, and the write is atomic, so a failed render leaves an existing file
untouched. The result carries the path, the real byte count and an
authenticated `/api/reports/{id}` link, which the reply adds even when the
model leaves it out and the Web chat downloads; IM channels that accept
files receive the PDF as an attachment, taken only from this attempt's
staged reports, never from a path the model wrote. Staged copies live
under `generated_reports/` in the runtime root; if staging fails, the
written file is kept and the result reports `download_error`. The bundled
font adds about 19.9 MB to the package.
- **Shadow Account reports render a PDF without WeasyPrint** (#1683, thanks
@he-yufeng). When WeasyPrint is missing or fails, the report renders the
same eight sections and charts through ReportLab in a simpler layout, and
only then falls back to HTML-only; the payload's `engine` can now read
`reportlab`. A process no longer loses the fallback after its first render
on a host without a CJK font. The PR used Helvetica when no CJK font was
found; the bundled font is now tried first, and a host where no embeddable
font loads gets HTML-only, because an unembedded face can print blank pages.
- **Feishu joins the guided Web UI channel setup** (#1572, closes #1614,
thanks @shadowinlife). The Feishu panel gets field hints and a setup guide
in every UI locale. **Test connection** is a standalone probe that requests
a tenant access token from feishu.cn or larksuite.com, whichever the
config's domain 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.
- **Slack and Discord join the guided Web UI channel setup** (#1757, #1758,
#1764, #1777, closes #1763 and #1776, thanks @shadowinlife). Both get
localized forms and read-only credential probes. Slack checks the bot token
with `auth.test`, requiring a bot identity so a user token is refused, then
the app token and Socket Mode with `apps.connections.open`; only Socket Mode
is supported. Discord requires a bot identity from `users/@me`; privileged
intents cannot be checked over REST, so the setup guide covers them. The
shared token probe, also behind DingTalk, QQ and Feishu, now reports HTTP
408 and 429 as a temporary network failure rather than rejected credentials,
and a failed probe leaves saved settings unchanged. Discord's allowlists,
group policy and emoji settings, and Slack's allowlist and emoji, refresh
without reconnecting. #1757 and #1758 landed through #1764.
- **Token usage in Run Detail, chat and swarm** (#1890, closes #1889, thanks
@shadowinlife). Run Detail gains a Token usage tab built from the run's
existing `llm_usage`, chat shows a live counter from the `llm_usage` stream
event (replay cannot double-count), and swarm cards and the dashboard show
run and per-agent totals. `GET /swarm/runs/{id}` now returns token totals,
provider and model, and swarm workers record prompt-cache tokens and whether
a count is provider-reported or estimated; an estimate is labelled as one
and a missing label reads "unlabeled". A reported zero stays zero beside a
nonzero count, and a missing cache field is never shown as 0. These are
token counts, not cost or cross-run totals.
- **`get_southbound_flow` serves the Stock-Connect net flow that is still
published** (#1486, closes #1483, thanks @shadowinlife). The new read-only
agent and MCP tool returns market-wide southbound daily net buy into Hong
Kong, split into the Shanghai and Shenzhen channels, in 100M HKD. Eastmoney's
datacenter report is the primary source; Eastmoney's `NET_DEAL_AMT` arrives
in million HKD and is scaled to the envelope unit, and a rejected report is
an error rather than an empty history. The keyless fallback is HKEX's
official daily statistics (Buy minus Sell turnover), which serves only the
latest trading day. There is deliberately no Tushare path: its southbound
fields now carry the cumulative stock, not a daily flow.
- **Gildata research tools and off-exchange fund NAV** (#1485, thanks
@sambazhu). With `GILDATA_TOKEN` set, three tools wrap Gildata's
natural-language endpoint: `get_cn_macro_series` (China macro, regional and
industry series), `get_cn_announcements` (A-share, HK and fund
announcements) and `search_broker_reports`. Each echoes the vendor APIs its
query was routed to and returns at most 200 rows (default 50);
`GILDATA_SRV_TOOL_URL` overrides the endpoint. `get_fund_nav` returns unit,
accumulated and adjusted NAV for funds such as `110022.OF`, and only those
three NAV fields count as price evidence for grounding. The Gildata loader
now also serves Hong Kong equities, stamped `raw`, and CN indices, each
resolved only when the vendor's `ref_code` matches the symbol exactly. It
stays at the tail of each chain so paid quota is spent last. Without a token
none of this registers.
- **Run cards flag backtests that may sit inside the model's training data**
(#1618, closes #1613, thanks @lorenzozanee). Strategy writes record the
provider and model that produced `config.json` and `code/signal_engine.py`,
and the run card shows them. Set `model_training_cutoff` (an ISO date) in
`config.json`: a window ending before it is marked as exposed, and an unset
or invalid cutoff is reported as unknown and treated as exposed. No cutoff
is inferred from the model name. `vibe-trading alpha bench --strict` takes
`--training-cutoff` to start the out-of-sample segment after that date, and
now returns an error instead of `ok` when no alpha rows qualify.
- **Loader-health reports say why a source came back empty** (#1643, #1698,
thanks @cgycorey). The weekly public loader-health lane attaches up to three
of the loader's own warnings to an unhealthy row, so an anti-bot refusal and
a broken loader no longer look identical. The report is a public artifact:
each message is capped at 200 characters, URLs and credential-shaped values
are redacted (including a labelled `Authorization: Bearer token: VALUE`),
and a warning that contains a filesystem path is omitted in full. The
health tests now register real loaders before stubbing one, so running a
single test no longer reaches the network.
- **A free BaoStock fallback for A-share `val:*` panels** (#1799, thanks
@Jackzigen). Without a Tushare token, or when Tushare returns nothing for a
field, `val:pe_ttm`, `val:pb` and `val:ps_ttm` come from BaoStock's daily
`peTTM` / `pbMRQ` / `psTTM`. `val:pe` and `val:dv_ttm` have no BaoStock
counterpart and stay NaN rather than being filled from another field. A
BaoStock outage or login failure degrades to NaN panels, not an exception.
Token holders see no change.
- **Hong Kong orders are sized on each symbol's board lot** (#1813, thanks
@alexwan02). A backtest config may carry `hk_lot_sizes: {symbol: lot}`;
`00700.HK`, `0700.HK` and `700.hk` share one entry and five-digit counters
such as `80700.HK` stay distinct. Any HK symbol without a configured lot is
sized on 100 shares and listed under `lot_assumptions` in the run metrics,
also when no table is given. Mixed-market runs carry each sub-engine's
assumptions under `market_diagnostics`.
- **Brazil (B3) market data and an opt-in ZimaOS template** (#1783, thanks
@incorpore). A `.SA` symbol is market `br_equity`, quoted in BRL and served
by `yahoo` → `yfinance` → `b3_cotahist` → `local`, with `BOVA11.SA` as the
default benchmark. The Brazil engine applies configurable B3 fees, R$0.01
ticks and a default lot of one share (`br_lot_size=100` for round lots); it
does not model income tax, stock-borrow cost or settlement cash locks.
`python -m src.br_data sync` stores official B3 COTAHIST, CVM, Banco
Central and Tesouro Direto data outside the checkout, and the
`get_br_market_data` agent tool reads it. COTAHIST bars are raw, and CVM
statements are latest revisions, not point-in-time inputs. `zimaos/` holds
a Compose file and a fork publishing example that each need explicit setup;
upstream gains no scheduled publishing job.
- **A complete pip + Telegram quick start in the README** (#1954). It covers
installing the Telegram extra, sender authorization with `allow_from`,
matching the API port between `serve` and `channels start`, and recovering
when `vibe-trading channels` is missing because the package was installed
into a different environment.
- **Thirteen fundamental and valuation factors** (#1787, #1788, thanks
@shadowinlife). Ten read point-in-time SEC XBRL filings: accruals,
book-to-market, cash-to-assets, CFO yield, earnings growth, FCF yield,
leverage, operating margin, revenue growth and ROA. Growth is year over year
through a 365-day as-of lag, and earnings growth divides by the absolute
prior value, so a loss turning into a profit reads as growth. Three read a
new `val:*` namespace of daily A-share valuation data (`fund_pb`,
`fund_ps_ttm`, `fund_dividend_yield`). Those are keyed by trade date and
attached only on the CSI 300 universe; a nonpositive P/B or P/S is excluded
rather than ranked as cheap. XOM keeps its current SEC filer, and filings by
its pre-reorganisation filer that predate the change extend its history.
Basic shares never stand in for missing diluted shares, and
price-times-diluted-shares ratios are valuation proxies, not reported market
capitalisation.
- **Industry-neutral A-share value factors** (#1802, closes #1789, thanks
@shadowinlife). `fund_pb_ind_neutral`, `fund_ps_ttm_ind_neutral` and
`fund_dividend_yield_ind_neutral` z-score each name against its Shenwan
(SW2021) level-1 peers on each date instead of the whole cross-section. A
CSI 300 benchmark attaches a point-in-time sector tag built from Tushare
`index_member_all` membership dates, and only when at least 90% of names
carry a label. A name with no label, or a group too small for a statistic,
stays NaN; there is no fallback to a whole-market z-score. Membership is
point-in-time, but the SW2021 taxonomy itself is applied retroactively, and
each factor's notes say so. With #1787 and #1788 the registry holds 478
factors, 20 of them fundamental (462 and 4 in 0.1.16).
- **Factor benchmarks at a longer horizon** (#1801, #1802, thanks
@shadowinlife).
`alpha bench --horizon N` (1 to 252, also on the tool and MCP wrapper)
measures IC against N-bar forward returns, sampled without overlap and
anchored at the end of the panel; a missing close never bridges the
horizon. `horizon=1` is the previous next-bar formula. Result envelopes now
state their horizon, sampling and `universe_excluded` list. SEC-fundamental
factors requested on a universe other than the S&P 500 are listed as
excluded instead of failing the whole run, and `alpha compare` shows them
as skipped with the reason. The alive/dead thresholds were calibrated on
daily IC and are not re-derived for N > 1.
- **Factor benchmarks feed `fund:*` factors their SEC inputs**. Benchmarks
now load each selected factor's declared fundamental fields as
point-in-time SEC panels; before, those inputs never reached the benchmark.
This needs the S&P 500 universe. The S&P 500 price panel goes through the
shared fallback chain and reports requested, fetched and missing
instruments and the price sources used.
- **Factor neutralisation and PCA in the quant library** (#1817, thanks
@BHhhh-123). `neutralization.neutralize_factor` regresses a factor on named
numeric and categorical controls (OLS or WLS) separately at every time on
the panel, at any bar frequency. Controls are either all required or
selected greedily on coverage, rank and sample size, and every time records
what was dropped and the exposure before and after. `pca_factors` fits
cross-sectional PCA per time. Neither function fills, pools or resamples
rows, and a rank-deficient time is skipped instead of being demeaned. The
caller has to supply exposures known at that time. The finishing commit
rescales inputs before squaring so very large or very small units neither
overflow nor change the answer, keeps covariance PCA in common units, and
breaks equal-loading sign ties by input order. Reachable through
`quantlib_call` and MCP.
- **Technical indicators can be read as of a date** (#1768, thanks @zeus229).
`technical_indicators` accepts an optional `end_date` (strict `YYYY-MM-DD`;
anything else is an error) as the acquisition boundary. Every response
carries `read_identity`: the boundary mode, requested end date, bar count,
serving provenance and a SHA-256 fingerprint of the exact interval, dates,
closes and volumes used, so two reads can be shown to have used the same
bars.
- **A `claude-cli` provider that runs on the local Claude Code login**
(#1804, closes #1800, thanks @alexwan02). Each turn runs one
`claude -p --restricted --strict-mcp-config --tools ""` subprocess that only
picks the next step (call tools or answer); Vibe-Trading executes the tools
itself, so the evidence ledger, the grounding gate and swarm delivery checks
run as they do for any other provider. No API key is needed.
`CLAUDE_CLI_DENY_TOOL_PREFIXES` (default `trading_,etoro_`) hides tools from
the model and `CLAUDE_CLI_BASH_POLICY` (default `python-only`, or `off`)
limits bash to `python` scripts and read-only listing commands; refused calls
are fed back at most three times. Both are convenience filters, not a
security boundary: a Python script can still do anything, and live orders
stay behind the mandate gate. Settings lists the models the login can run
and offers only the reasoning efforts the chosen model accepts. A decision
can take minutes, so the provider emits a waiting signal every second;
the idle timeout no longer discards an answer still on its way, the stop
button kills the child process, and progress consumers see `llm_waiting`.
- **The backtest tool returns a structured summary** (#1646, closes #1644,
thanks @shadowinlife). A successful result carries `summary`: the run
card's scalar metrics, `structured_metrics` and `validation`, run metadata,
an equity preview of at most 50 points with the first and last bar always
kept, and artifact paths including each symbol's `ohlcv_<CODE>.csv`. The
metrics no longer reach the model only as JSON inside a `stdout` cut to its
last 2,000 characters. A missing or corrupt run card omits the summary and
leaves the run successful, as does one whose artifacts resolve outside the
run directory. When the envelope would exceed the 10,000-character
tool-result cap, logs are dropped first, then the preview is thinned, and
anything still omitted is named in the result.
- **`read_run_artifact` reads run artifacts as structured pages** (#1647,
closes #1645, thanks @shadowinlife). An agent and MCP tool with three
modes: `rows` (offset pages of whole records with `next_offset`),
`downsample` (equal-stride sample, endpoints kept) and `meta` (columns, row
count, size and the run card's sha256 for the file). Aliases (`equity`,
`trades`, `metrics`, `positions`, `target_positions`, `ohlcv:<CODE>`,
`run_card`) resolve by a fixed map; any other path is served only if the
run card's artifact manifest lists it, and paths or symlinks that leave the
run directory are refused. Pages fit the same 10,000-character cap the agent
and swarm apply, CSVs are scanned without loading the whole table, and
zero-padded codes such as `000001` stay strings. Rows shown this way ground a
report only while the file is an unchanged engine-written table; a file the
model wrote never does.
- **Grounding checks are declared in one registry, and each validation names
the checks that fired** (#1628, #1661, #1691, thanks @he-yufeng). Each
check has a stable name, its issue codes, a description, a severity and a
phase. Fifteen are declared: the identity, answer and declaration checks
from these PRs, plus the per-figure role checks (observed, derived,
proposed, cited, count) and the price-provenance checks, which a
maintainer follow-up registered. Every attempt recorded in
`grounding_evidence.json` carries `fired_checks` and `check_findings`,
taken from the predicates that actually returned a finding, so role checks
that share `numeric_claim_conflict` do not implicate each other.
Predicates and their order moved unchanged, so verdicts are the same.
`CONTRIBUTING.md` describes how to add a check with its passing and failing
answer pair.
- **The session iteration cap is configurable** (#1833, thanks @sambazhu).
`VIBE_TRADING_AGENT_MAX_ITERATIONS` sets how many ReAct iterations a session
run (Web UI, API) may take before it stops with "reached max iterations".
The default stays 50 and values below 1 are rejected. The no-progress
budget and spending limits still apply.
### Changed
- **The live runtime scheduler runs on the shared scheduler kernel** (#1899,
thanks @shadowinlife). It keeps its public surface, its interval and
one-shot job syntax and its sleep-until-the-earliest-deadline wakeups; the
mandate gate, kill switch and market-hours layers are unchanged. A second
concurrent `stop()` now waits for the first one's teardown instead of
returning at once.
- **Broker connector documentation and tests** (#1770, #1793, #1809,
closes #1724, thanks @lorenzozanee, @alexwan02 and @he-yufeng). The broker bring-up
checklist in `CONTRIBUTING.md` asks for options evidence: write-side
argument names and types recorded apart from read shapes, unsupported shapes
kept gated, and supported ones tested against mandate caps on premium ×
multiplier. The `readonly` field on nine SDK connector configs is documented
as what it is: the order methods do not read it, and order access is decided
by the connector profile. IBKR's pool reset test no longer depends on test
order.
- **Tencent, Eastmoney and AKShare A-share prices move to the multiplicative
forward-adjusted basis when they can** (#1551, closes #1541, thanks
@he-yufeng). These sources serve dividend-additive qfq series, so returns on
them are not total returns. The loaders now also fetch the raw series and,
when `qfq - raw` forms valid per-action plateaus, rebuild the series with
`(prev_close - dividend) / prev_close` factors and stamp it
`split_dividend`. Any inconsistency (misaligned calendars, drifting
offsets, implausible dividends, a failed raw fetch) keeps the additive
series with its stamp and warning. A window with no corporate action keeps
its static stamp, and the series is anchored at the window's end, so
absolute levels depend on the requested range while returns do not.
- **mootdx is retired from the default A-share chain and the public canary**
(#1730, closes #1729, thanks @he-yufeng). The TDX servers stopped answering
mootdx's protocol in 2026-07, and every automatic A-share fetch spent about
ten seconds there before falling through. The loader stays registered for
explicit `source="mootdx"`. A saved source-order override that still lists
it is no longer a permutation of the default chain, so it is ignored with a
warning and the default order applies.
- **Every per-loader test suite checks the health lane's OHLCV contract**
(#1725, closes #1723, thanks @Jackzigen). `agent/tests/loader_contract.py`
holds the column, index, dtype, finiteness and OHLC-ordering checks the
loader-health lane applies, and each loader's canonical-frame test calls
it, so a drifting source fails its own suite instead of waiting for the
weekly run.
- **An MCP tool result reaches the agent once, not up to 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.
- **The system prompt is byte-stable across runs** (#1708, closes #1707,
thanks @Jackzigen). The workspace state (the run directory and tool
counters) changed the system prompt on every turn and tool call, which
invalidated a provider's prefix cache from that point on. The state now
rides in the first user message inside an `<agent-state>` envelope, next to
`<recalled-memories>`; the token count is the same.
- **`loop.py` is split into modules, first slices** (#1636, #1631, #1662,
thanks @Jackzigen, @he-yufeng and @Mikesad). Tool-result helpers moved to
`src/agent/tool_results.py`, context compaction and message repair to
`src/agent/context_compact.py`, and LLM usage accounting to
`src/agent/llm_usage.py`, byte-identical and re-exported from `loop`. Log
records from the moved code now come from the `src.agent.tool_results` and
`src.agent.llm_usage` loggers. A test that monkeypatches `loop.X` no longer
reaches a function that now lives elsewhere; patch the module that holds it.
- **`loop.py` is split into modules, remaining slices** (#1641, #1678, #1791,
thanks @Jackzigen). The final-status ladder moved to `run_outcome.py`, the
tuning getters and `TOKEN_THRESHOLD` to `tuning.py`, and written-file
tracking with its pending-write directive to `write_targets.py`.
Maintainer follow-ups moved the tool-call lifecycle (`tool_lifecycle.py`,
still logging as `src.agent.loop`), per-turn streaming callbacks
(`loop_streaming.py`) and final-answer grounding orchestration
(`loop_grounding.py`). `loop.py` went from 3,761 lines at 0.1.16 to 1,628.
One stated difference: the `degraded` flag on the end event and result now
comes only from success branches, so a run cancelled or stalled between a
fallback release and the status decision is no longer marked degraded.
- **Scheduled research, the Brazil data sync and memory consolidation run on
one scheduler kernel** (#1893, thanks @shadowinlife). `agent/src/scheduling/`
holds the trigger grammar, the next-due arithmetic (pinned by 133 golden
vectors recorded before the move) and a tick loop with bounded retry backoff.
The scheduled-research executor uses it, and its REST contract is unchanged.
The FastAPI lifespan now owns the scheduler, and the unused `cron_service`
parameter is gone. The live-trading scheduler was not part of this change.
The kernel also adds protected `system:` jobs. They are seeded at startup,
a user cannot create an id with that prefix, and REST and CLI refuse to edit
or delete one. Both system jobs are opt-in and need
`VIBE_TRADING_ENABLE_SCHEDULER=1`:
- `system:memory-consolidation` runs daily at 04:00 UTC when
`VT_MEMORY_CONSOLIDATION` is on. `VT_MEMORY=on/full` does not turn it on,
and it does nothing unless `VT_MEMORY_GC` is also on.
- `system:br-data-sync` needs `VIBE_BR_SYNC_ENABLED=1` and replaces
`python -m src.br_data daemon`, which now prints a migration notice and
exits 0. `VIBE_BR_SYNC_TIME` sets the slot only when the job is first
seeded, and the first sync waits for that slot. The ZimaOS sidecar
container is gone. Its Compose file leaves both flags off by default
(`068ec3bd`; the PR had turned them on).
Since `068ec3bd`, a source that fails inside the sync fails the attempt, so
the scheduler's retry budget applies. Before, the sync was recorded as a
success. Before downgrading to a version without system jobs, delete the
`system:` rows from `scheduled_research_jobs.json`. Older readers reject
those rows and quarantine the whole file.
- **A chat message can be up to 100,000 characters** (#1701, thanks @mminkus).
The limit used to be 5,000 characters, while scheduled-run prompts had no
limit. A longer message was refused before reaching the model, the Web UI
showed only "Failed to send message, please retry", and the 422 response
repeated the whole prompt. Since `f6e72428`, the 422 returns a short
`message_too_long` body naming the limit, without the prompt. The Web
composer checks the length before sending, counting code points as the
server does. Goal prompts stay limited to 5,000 characters.
- **Agent chat loads small run previews instead of full runs** (#1853, closes
#1852, thanks @alexwan02). `GET /runs/{run_id}/preview` returns only the
metrics, the first 1,000 equity points and whether the run deserves a card.
Agent chat requests previews for a whole session at once. Before, it fetched
each full run one at a time; the PR measured about 70 MB for a session with
one backtest card.
`GET /runs/{run_id}` now builds and encodes its response off the event loop,
so other requests no longer stall; its bytes are unchanged. One run type
loses its chat card: a run whose only output was price data that the full
response rebuilt online, with no metrics, equity, trades, positions,
validation, strategy, price or OHLCV files and no run card.
- **Dependency updates** (#1665, #1666, #1667, #1668, #1692). Python:
46 packages in `requirements-lock.txt` moved, including `langchain` 1.4.3,
`langchain-core` 1.6.6, `langchain-openai` 1.6.7, `fastapi` 0.142.2,
`starlette` 1.7.0, `uvicorn` 0.54.0, `pyjwt` 2.15.1 and `akshare` 1.19.1.
The loader-health environment uses `baostock` 0.9.4. Frontend: React 19.3,
Vite 8.3, react-router 8.4 and Vitest 5.0.2, with `@vitest/coverage-v8`
moved to 5.0.2 to match.
CI: the Docker build actions, `cloudflare/wrangler-action` 4.1.3 and
`actions/upload-artifact` 7.0.1. `uuid-utils` stays at 0.17.0 because 1.0.0
(#1670, not merged) conflicts with the `uuid-utils<1.0` requirement of
`langchain-core` and `langsmith`.
- **Docker image pins move past known advisories**. `requirements-lock.txt`
and `requirements-channels-lock.txt`, which the Docker image installs with
`--require-hashes`, now pin `aiohttp` 3.14.5, `certifi` 2026.10.19,
`urllib3` 2.8.0, `multidict` 6.9.1 and `pillow` 13.0.0 (`ccxt` 4.5.89
comes along so the newer `certifi` resolves). A plain `pip install`
already resolved newer releases. `mcp` stays below 1.30 for the IBKR OAuth
constraint, so CVE-2026-59951 (Streamable HTTP sessions are never
reclaimed) remains open for `vibe-trading-mcp --transport http`.
- **For contributors running the suite: symlink tests skip where symlinks are
unavailable** (#1803, thanks @hunters1011-ctrl). Three positions/sectors API
tests used to fail on Windows hosts that refuse to create symlinks
(WinError 1314). They now skip there and still run everywhere else.
- **Channel edits to live-read settings apply without reconnecting** (#1737,
closes #1734 and #1736, thanks @shadowinlife). Each adapter declares the
settings it reads per message (allowlists, emoji and similar); an edit that
changes only those refreshes the running channel in place. The comparison
uses validated values, so a full Web form echoing unchanged connection
fields does not count as an edit. A refresh copies only the declared fields
and keeps connection state resolved at login, such as WeChat's authenticated
server address, and Signal refreshes its writable `dm` / `group` policies
rather than its computed allowlist. The Web result now says whether the
channel was refreshed, reconnected, restarted with the others, or will pick
the change up when channels start, in every UI locale. When a hot swap
degrades to a full reset, the reason is returned with URL userinfo and old
and new secret values removed, and a best-effort notice goes to the changed
channel's last chat.
- **Channel status records runtime failures** (#1738, closes #1735, thanks
@shadowinlife). A failure to start a channel, to send after the last retry,
or to stop the old adapter during a reload is recorded on that channel's
status entry as a one-line `error` of at most 200 characters with URL
userinfo removed, plus `error_context` (`start`, `send` or `reload_stop`)
and `error_at`. The traceback stays in the log. A successful rebuild clears
it.
- **New channels are checked against the authoring contract** (#1630, thanks
@he-yufeng). A test walks every discovered channel and fails when one leaves
a `BaseChannel` abstract unimplemented, has a scalar config key without a
field hint, or has a credential-shaped key that would reach the browser
unmasked. `CONTRIBUTING.md` gains an "Adding a Channel" recipe.
- **Web UI navigation is grouped and explains each page**. The sidebar groups
pages into everyday research, specialized analysis and automation & setup,
with plainer names ("Research assistant", "Backtest reports", "Trading
status"). Every page except the chat has a collapsible "How to use" panel,
and the welcome screen offers three editable starter tasks (research a
stock, test a strategy, review holdings). Math and code highlighting load
only for completed answers. API requests time out after 120 seconds
(`VITE_API_TIMEOUT_MS` at build time; 15 seconds for the session list) and
say whether a timed-out write may still be processing; writes are never
retried automatically. Session-list, rename and delete failures are shown
instead of ignored.
- **Analysis pages keep their work across navigation**. Alpha Zoo, Correlation
and Options Lab keep their inputs for the browser session. An Alpha Zoo
evaluation runs as a server job the page can reconnect to after leaving or
reloading; a resubmitted request with the same id returns the existing job
rather than starting another, and a job lost to a server restart says so.
Before running, the page states each universe's data requirements: CSI 300
needs a Tushare token and permissions, S&P 500 uses public data, and
BTC-USDT is refused as a single asset.
- **The runtime page lists connectors by status** (#1845, thanks @alexwan02).
Connected connectors come first, then those needing attention, then the
rest; peers keep the backend's alphabetical order, so the 15-second poll
never reshuffles them. Only the order changes. The page header no longer
calls the page read-only, since model cadence and spending limits can be
edited there while a runner is stopped.
- **Report rendering dependencies** (#1680, #1683, #1709, thanks @zeus229 and
@he-yufeng). The base install now requires `mistune`, `nh3` (previously only
in extras), `pypdf`, `reportlab`, `arabic-reshaper` and `python-bidi`. The
Docker image adds `fonts-noto-core` and `fonts-noto-cjk` so WeasyPrint
renders CJK and Arabic reports.
- **Update the Windows desktop PyJWT pin to 2.15.1**. This matches the main
lock and includes the fix for
[GHSA-ffc3-869f-jxw9](https://github.com/jpadilla/pyjwt/security/advisories/GHSA-ffc3-869f-jxw9).
Artifact hashes are verified against PyPI; other Windows dependency pins
stay unchanged.
### Fixed
#### The grounding gate
- **Indexed and container field refs resolve, and a miss lists the refs that
would work** (#1633, thanks @zeus229). `a.0.b` and `a[0].b` now name the
same evidence element; an unindexed field still selects none. When a
`call::field` ref matches nothing, the correction lists exact
`call_id::path[i]` refs, value matches first. A ref naming a container lists
its numeric descendants that hold the figure's value, and a tool name shared
by several calls ranks those calls by value match, most recent first.
These are hints only: no hint authorizes a value, and a maintainer
follow-up keeps them to the figure's own instrument and to the element an
indexed ref names.
- **A ref naming no call of the session gets real refs to copy** (#1638,
thanks @zeus229). A `scope::field` ref whose scope names no call, tool or
run stays rejected, now with the `unknown_call_id` reason and up to five
exact `call_id::field` candidates. The system prompt tells the model to copy
`tool_call_id` verbatim instead of inventing a short alias. A maintainer
follow-up applies the same instrument and figure-kind limits a real ref
has, so a candidate is never one that would be refused.
- **A correction keeps the figures that already passed** (#1702, thanks
@he-yufeng). The correction prompt listed only what failed, and a model
facing 65 issues rewrote whole numeric sections as prose, deleting valid
Risk X-Ray values and a benchmark table that had no issue. It now also
lists up to 24 measured figures that checked clean and tells the model to
keep them as written. A value that fails anywhere in the draft is not
listed, and bare integers never are. When more than 24 issues exist, the
prompt says how many are not shown and that unlisted figures are not
assumed to pass.
- **Only the engine's own output grounds a backtest report** (#1685, thanks
@cgycorey). The active run's `metrics.csv` could credit an earlier run's
Sharpe to the current call; that archived copy now counts only when its
manifest names the full directory the call declared and the exact call id,
and a model-written manifest is ignored. A `metrics.csv` or `metrics.json`
the model wrote is no longer engine evidence, a date cell with an explicit
year must match that year, and a resolver step that failed, was refused or
came back empty no longer erases a locked or conflicting identity.
- **A derived figure's ref must name evidence that exists** (#1835, #1837,
thanks @zeus229). A comma- or semicolon-separated ref is resolved member by
member, and an empty or unknown member fails with `no_evidence` instead of
falling back to every value in the session. A single ref that names no call,
tool, field, artifact or observed symbol is refused the same way rather
than borrowing a coincidentally equal value. Unattributed multi-symbol
arithmetic keeps its `no_symbol` diagnostic.
- **Derived notes after a label parse, and process metadata is not evidence**
(#1728, #1767, thanks @zeus229). A note such as `scenario result: 125 /
10000 * 100` was not evaluable because only the full-width `:` was a
separator; the ASCII `:` now is too. Top-level `exit_code`, `returncode`,
`elapsed_seconds` and `duration_seconds` of a code-running tool's result are
no longer recorded as observed values, so a backtest's runtime cannot
ground a figure.
- **Percent-named fields are read as percentage points** (#1823, closes #1820,
thanks @he-yufeng). A-share rows carry `changepercent` values that are
already points, so `0.52 - 1.41` refused the correct "-0.89%" and passed
"-89%". When every data operand anchors only to fields named `percent`,
`pct` or ending in `%`, the result is read in points; an operand whose
value also appears under a plain field keeps the fraction reading. A
maintainer follow-up converts the operands before evaluating, so ratios and
compound factors of two point values come out right.
- **Every data operand of a derived figure must be observed.** Only added or
subtracted terms were checked, so a numerator or denominator the session
never fetched could pass. A chosen multiplier or divisor (a discount factor,
a weight) now needs a `count` declaration and its value and label on the
same visible line of the answer. Literal 1 and square or cube exponents are
structural, and an explicit trailing `*100` on a percent claim is read as a
unit conversion without exempting any other operand.
- **Instrument identity survives matrices and ticker-keyed results** (#1850,
#1851, thanks @zeus229). In a square Markdown matrix with an empty top-left
header (a correlation table), each row's symbol is taken from its first
cell, bold removed, when the row and column symbol sets are complete,
unique and equal; anything else stays unscoped. A result keyed by
instrument (`risk_contribution_pct.ABC.US`) keeps each value's instrument
and exact ref, so the same number cannot ground a claim about another
instrument. List-indexed scalars such as `positions[0].contribution_pct`
keep their indexed-ref meaning.
- **A figure that names its currency must match evidence in that currency**
(#1905, thanks @ThreeTima). An ISO code or an unambiguous mark (`€`, `£`,
`₩`, `₹`, 人民币, 美元, 円) fixes the figure's currency for observed,
derived and proposed checks, and SEC XBRL amounts carry their declared unit
(CHF, NZD, BRL, ARS and VND included). A scaled amount such as `1.2B USD`
keeps its code. Bare `$` and `¥` stay ambiguous and match any currency.
#### Agent loop, providers and swarm
- **A restored read-only result stays visible until the model has seen it**
(#1635, thanks @zeus229). Compaction could clear a result replayed from
the run cache before the next model request carried it, so the restore was
wasted. Replayed results are now held back from microcompaction, text
collapse and the summary layer until one successful model request has
included them.
- **Writing a strategy file no longer fails with `AttributeError`** (#1673,
closes #1672, thanks @tomaszkubiak-courses). After the model-provenance
writer landed, every run that wrote a strategy file crashed reading
`_active_model_id`. The loop now sets it from the runtime snapshot and,
before each batch of tool calls, from the provider-reported model, falling
back to the configured one. This regression never reached a release.
- **Codex tools may omit optional arguments** (#1785, thanks @zeus229). The
Codex Responses endpoint treats a tool without an explicit `strict` as
strict, so the model filled every optional property, sending `""` for an
optional string. Tool schemas now go out with `strict: false`.
- **Skills load and save under the names they advertise** (#1797, closes
#1795; #1858, closes #1855; thanks @Shizoqua). Two section headings that
differ only by case could not be loaded, and the path the error suggested
repeated the error; an exact-case match is now tried first. Saving a skill
whose own frontmatter carried another name left it unreachable under its
slug and let each lookup append a duplicate; the `name:` line is now
rewritten to the slug on every save, and a fallback read never duplicates
a loaded skill.
- **Research-goal evidence keeps its provenance inside the run** (#1688;
#1895, closes #1894; thanks @zeus229 and @Shizoqua). The goal tool and
skill tell the model to copy a result's `tool_call_id` verbatim and never
to use a ledger `evidence_id` (`ev_...`) as a grounding ref;
`tool_call_id` stays optional for manual evidence. An absolute artifact
path outside the current run directory is now rejected, as a relative
escape already was, instead of being verified against another run's file.
- **Each swarm task gets its own artifact directory** (#1455, thanks
@Shizoqua). Artifacts were keyed by agent alone, so when one agent ran
several tasks, a later task could read an earlier task's `report.md` back
as its own output. They now live under `artifacts/<agent>/<task>/`, and
earlier tasks' reports, summaries and message logs stay in place;
sequential and parallel runs are covered through the real runtime.
- **Swarm presets take their variables from the prompt** (#1674, thanks
@Mikesad). Seven presets ran on fixed defaults whatever was asked: a copper
request ran every commodity worker on gold, a bearish options request used
a neutral view, and crypto, factor, event, fund and macro prompts were
replaced the same way. These variables are now extracted from the prompt,
an explicit duration ("3 months", "两周") is used as stated, and
`run_swarm` accepts a `variables` object whose values override extraction.
Prompts that match nothing keep the old defaults.
- **A half-written last line no longer breaks a swarm event stream** (#1908,
thanks @rudycelekli). `read_events` raised on an `events.jsonl` whose final
line was still being appended or was cut short by a crash, which killed the
SSE stream and run recovery. An unterminated last line that is not yet
valid JSON, including a cut UTF-8 sequence, is skipped until the next poll;
a complete invalid line still raises.
#### Market data and loaders
- **`get_northbound_flow` stops returning an empty success off a dead feed**
(#1484, closes #1481, thanks @shadowinlife). Eastmoney's northbound
endpoint still answers but has carried no net flow since HKEX's 2024
disclosure change. When both its realtime and history sides are empty or
all zero, the tool now uses the Tushare fallback or returns `ok: false`
naming both failures. The fallback no longer multiplies every row by 100:
values stay in million CNY, rows before 2024-08-19 are net buy, and later
rows, which are turnover, come back under `turnover_*` fields with their
net-flow fields `null`.
- **Daily fund-flow fallback covers empty responses and labels its source**
(#1954). An A-share daily request now tries Tushare when Eastmoney returns
no usable rows, not only when the request or symbol resolution fails. Each
symbol names the provider that served it, and a batch served by both
reports `mixed`. Tushare's `main` is now large plus super-large net flow;
`net_mf_amount`, which is total net flow, is returned separately as
`net_total`. A missing or non-finite buy or sell amount stays `null`
instead of becoming zero, and the result states its methodology because
provider bucket definitions differ. Minute requests never fall back to
daily data, and a missing token or permission remains a per-symbol error.
- **A blocked Stooq costs one request** (#1637, #1640, closes #1648, thanks
@cgycorey). Once Stooq refuses the client, the loader stops requesting
further symbols instead of only silencing its warning, so the fallback
chain moves on at once. The refusal is detected from the response before
parsing: the proof-of-work page, a 403 or 429, or the plain-text daily-hits
message. Concurrent callers serialize on the check, and the latch lasts 300
seconds instead of the life of the process. A 5xx is still a per-symbol
failure and `N/D` is still "no data".
- **FMP and Tiingo series never mix adjusted and raw prices** (#1684, #1689,
thanks @cgycorey and @he-yufeng). The price basis is decided once per
response from bars that can actually be emitted. A bar whose adjusted
prices are missing or invalid is dropped, with a warning, rather than
filled with raw prices beside adjusted neighbours. Bars with unparseable
dates or non-positive prices are dropped too. Each frame carries its actual
`adjustment` stamp, the loader cache keeps it (the cache format version
was raised, so older entries are refetched), and tests pin the stamp on both paths and through a
warm cache hit.
- **Local data files load every bar they should** (#1651, #1825, #1819,
thanks @rudycelekli and @he-yufeng). The end-date filter is half-open at the
next midnight, so bars stamped in the last fractional second of the end day
are no longer dropped. A Parquet file whose dates live on a
`DatetimeIndex` loads with or without a configured `date_format`; before,
setting a format silently dropped every bar. An explicitly mapped date
column takes precedence over the index, which also removes a crash when
both share a name.
- **The opt-in loader cache follows changes to local sources** (#1650,
#1747, #1879, thanks @rudycelekli). Local cache entries are keyed by the
file, query, date format and column mapping as well as the range, so
repointing a symbol no longer serves the previous source's bars. Correcting
or deleting a CSV, Parquet or DuckDB file (including a DuckDB WAL change)
refreshes the entry on the next fetch, reusing the same cache paths. Cache
metadata that is missing, malformed or unusable is a cache miss that the
loader repairs, not an exception. An edit that preserves every observed
filesystem identity field is still not detected.
- **The Longbridge loader works on SDK 4.0 and later** (#1805, #1847, thanks
@alexwan02). SDK 4 refuses `Config(app_key, app_secret, access_token)`, so
every Longbridge fetch failed and auto mode fell through even with valid
credentials. The loader and the trading connector now share one helper that
uses `Config.from_apikey` when the SDK has it, and the initialization error
names the exception type. One loader test covers both SDK shapes through
windowing and caching.
- **BaoStock 0.9.4 connections keep their timeout.** The bounded connection
wrapper accepts the new API-key argument and routes `bs-` keys to the VIP
host while staying compatible with 0.9.3.
- **Data tools honour their row limits** (#1676, #1927, thanks @cgycorey and
@rudycelekli). `get_macro_series` reports `observations_available`,
`truncated`, the effective `limit` and a hint when it caps a series, and
trims whole observations to fit the tool-result budget so `count` is what
was delivered. A FRED response covering only part of the requested window
is an error. `get_fund_nav` clamps an explicit `limit=0` to one row instead
of returning the 2,000-row default.
- **Symbol suffixes resolve to the right exchange** (#1907, #1914, thanks
@rudycelekli). `get_etf_holdings` treats Yahoo's `510050.SS` like
`510050.SH` instead of sending it down the US/SEC path. Symbol search labels
Beijing codes (`920xxx` and legacy `4xxxxx` / `8xxxxx`) `.BJ` instead of
`.SZ`, and the Tushare fallback no longer maps `920xxx` to `.SH`.
- **RSSHub event enrichment keeps calendar dates and share classes** (#1829,
#1830, thanks @rudycelekli). A timezone-aware `as_of` no longer raises, and
aware daily bar labels keep their displayed date instead of shifting to the
UTC day. Configured `bare` and `exchange_prefix` routes split only the final
venue suffix, so `BRK.B.US` requests `BRK.B` rather than `BRK`. Intraday
publication instants are not converted between markets.
- **SEC quarterly statements derive fiscal Q4 only for currency flows**
(#1917, thanks @rudycelekli). `period="quarter"` built Q4 as FY minus three
quarters for every concept, which produced negative weighted-average share
counts and EPS figures no filing reports. Q4 is now derived only for
three-letter currency units that match across all four periods; share
counts, per-share values and dimensionless ratios are left out of the
derived row.
- **Correlation matches the data it compares** (#1710, #1711, #1746, #1933,
closes #1932, thanks @rudycelekli, @cgycorey and @Jackzigen). Daily
histories from sources in different time zones are joined on each source's
local calendar date in both correlation and regime analysis. Equity tickers
that end like a quote asset (`ABNB`) are no longer routed to crypto
sources. `700`, `0700`, `700.HK` and `00700.HK` are one instrument for the
duplicate-asset guard, while five-digit counters such as `80700.HK` stay
distinct. A pair whose correlation is undefined (a zero-variance leg) is
`null` from `/correlation` and `/correlation/analysis`, not 0.0, and the
heatmap shows a muted dash with an explanation.
- **Correlation analysis reports per-symbol coverage.**
`/correlation/analysis` computes the matrix and the optional regime timeline
from one price snapshot, rejects two spellings of the same asset before any
fetch, and returns which symbols are missing with every source tried and why
it failed. The fetch honours saved source-order overrides and tries Yahoo
first for `^`, `=F` and `=X` identifiers.
#### Backtest runner, engines and metrics
- **Profit factor is undefined when no trade lost** (#1602, thanks
@davidalmeida90). `profit_factor` and `profit_loss_ratio` were 0.0 for a
run whose trades all won, which ranked it below every run with a loss.
Both are now `None` (an empty `metrics.csv` cell, `null` in JSON) in the
daily metrics and the options engine. A run with no closed trade, or only
losing trades, keeps 0.0.
- **Sortino uses zero-target downside deviation over every period** (#1693,
#1727, thanks @MetaAviator). The denominator was the sample standard
deviation of the losing returns, so equal losses looked riskless and a
single loss left the ratio huge or undefined. It is now the root mean square
of negative returns divided by the full return count, in the shared metrics
and in the options engine.
- **The first bar's result counts against initial capital everywhere**
(#1750, #1818, #1844, closes #1749 and #1843, thanks @he-yufeng and
@DrDLyuan). The options engine's drawdown peak and the `drawdown` column in
`artifacts/equity.csv` start from initial cash, so a first-bar loss shows up
and the column agrees with `max_drawdown`. Per-bar returns measure the first
bar from initial cash too, so Sharpe and Sortino see it, the `ret` column
compounds to `total_return`, and bootstrap validation uses the same series.
- **Calmar stays finite and zero-bar trades count in holding periods**
(#1901, #1876, thanks @Bortlesboat and @MetaAviator). Calmar falls back to
0.0 when an overflowed annual return makes it non-finite, like the other
ratios; `annual_return` itself is unchanged. Completed same-bar trades now
count in the average holding period. `avg_holding_days` remains the
engine's bar-based measure, not calendar days.
- **Options legs are priced on the run's bar cadence** (#1753, thanks
@remote-controlled-man). Historical volatility was always annualised at 252
bars a year, so a crypto or hourly options run priced its legs on one
cadence and reported metrics on another. Pricing now receives the run's
factor. A cross-market run estimates each bar's cadence from the intervals
observed up to that bar, so extending a run cannot change earlier option
prices.
- **`trades.csv` reports the opening fill and keeps break-even exits**
(#1779, #1780, thanks @MetaAviator and @alexwan02). Each entry and exit row
carries an `event` column. The entry row shows the price and size of the
fill that opened the position, not the weighted cost after later scale-ins;
those stay in `fills.jsonl`. Validation and run-artifact readers pick exits
by the event column, so zero-PnL exits are no longer dropped. Unmarked
older files keep the previous convention.
- **Benchmarks follow the symbol's exchange and the run's source** (#1712,
#1781, thanks @rudycelekli and @alexwan02). `.SH`, `.SS`, `.SZ` and `.BJ`
symbols take the CSI 300 benchmark whatever the source; local, BaoStock and
yfinance runs could fall through to SPY. A source that forbids network
fallback, `local` included, fetches its benchmark through that same loader.
When the declared benchmark cannot be resolved, the run card says that
benchmark-relative metrics were graded against the equal-weight basket.
- **Crypto spellings get crypto rules** (#1740, #1748, #1861, #1862, #1864,
closes #1751 and #1863, thanks @he-yufeng, @rudycelekli and @Jackzigen).
`BTC/USD`, `ETH/USD` and four other known slash-USD pairs are classified
before the forex rule, in backtests, correlation and Agent/MCP data routing,
while `XAU/USD` stays forex. Digit-leading bases (`1INCH-USDT`,
`1000PEPE-USDT`), separated USDC and BUSD quotes (`BTC-USDC`, `ETH/USDC`)
and `BASE-USDT-PERP` perpetuals no longer fall through to A-share rules,
where board-lot rounding rejected every order. Signal alignment uses the
same classification. The same CI repair reads the alpha-readiness Tushare
token through the config layer and treats a whitespace-only token as unset.
- **A-share price limits cover every board's codes** (#1913, thanks
@rudycelekli). ChiNext `301xxx` and STAR `689xxx` codes get the ±20% band,
and Beijing `920xxx` and legacy `4xxxxx` / `8xxxxx` codes get ±30%; they had
fallen back to ±10% and blocked legal fills. ST status and listing-day
exemptions still cannot be inferred from a code.
- **Contract specs for Yahoo forex pairs and ICE softs** (#1930, #1944,
thanks @wavebyrd). `EURUSD=X`-style symbols, and `.FX` in any case, are
normalised before the spread and swap tables are read, so they stop paying
the default 2-pip spread and swap. Coffee, sugar and cotton multipliers are
dollars per cent (375, 1120, 500), like the grains; they were full contract
pounds, a hundred times too large.
- **Backtests warn about config keys that nothing reads** (#1935, #1947,
thanks @he-yufeng and @rudycelekli). A key no reader on the backtest path
recognises (`initial_capital` instead of `initial_cash`) adds a run-card
warning with a did-you-mean hint. A key that only another engine reads,
such as `commission_rate` on a crypto run that uses `maker_rate` and
`taker_rate`, is named as unused by the selected engine, including the
sub-engines of a mixed-market run. Runs still proceed. This resolves #1934.
#### Validation and run artifacts
- **Monte Carlo path metrics start from initial capital** (#1664, thanks
@MetaAviator). Actual and shuffled paths both begin at the starting
capital, so the first trade's return and loss enter Sharpe and the
drawdown high-water mark. Monte Carlo statistics and p-values change
accordingly; the chart payload is unchanged.
- **Validation stops inventing returns across missing equity** (#1754,
thanks @MetaAviator). Bootstrap and walk-forward returns use only observed
adjacent equity values, so a gap no longer becomes a flat return plus one
jump. A walk-forward window without observed endpoints or any adjacent
return reports an error instead of a zero score.
- **Standalone revalidation reproduces the original run's settings**
(#1815, #1915, #1842, thanks @MetaAviator and @rudycelekli).
`python -m backtest.validation <run_dir>` uses the saved Monte Carlo,
bootstrap and walk-forward parameters and seeds instead of defaults, still
running all three tools. The runner records its annualisation factor in
`run_card.json` and revalidation reuses it, including `null` for calendar
annualisation; older cards reconstruct it from the config and equity
timestamps. Reloaded trades recover their entry dates from marked
entry/exit pairs, so walk-forward windows group them as the original run
did.
#### Quant Library and analysis tools
- **Brinson attribution reads the book, not its liquidation** (#1782, thanks
@alexwan02). A run closes every position on its last bar, so the final
`positions.csv` row is all zeros. When it is, attribution uses the row
before it and the report notes that row's timestamp. It steps back only
one row: a book already flat before the close is still reported as flat.
- **Attribution annualises with the run's recorded rate** (#1948, thanks
@rudycelekli). When the run card records `annualisation.bars_per_year`,
attribution uses it instead of re-deriving the rate from the requested
interval, which disagreed for crypto's 365-day year and any run whose
served bar spacing differed from the request.
- **Attribution inputs and Carino linking** (#1659, #1943, thanks
@rudycelekli). `brinson_fachler` and `carino_factor` reject non-finite
weights, returns and tolerances by name instead of returning NaN effects.
Carino's near-coincidence branch now compares the two returns relative to
the remaining wealth. Two returns close to -100% can differ by a tiny
absolute amount while their wealth relatives differ severalfold, and
those used to take the limit branch.
- **VaR backtests no longer join observations across a gap** (#1658, thanks
@rudycelekli). A pair dropped for a non-finite return or forecast still
leaves the coverage counts, and now also breaks the Christoffersen
independence transitions. The breaches either side of it are no longer
counted as consecutive days.
- **Implied volatility meets its price tolerance** (#1657, thanks
@rudycelekli). The bracketing fallback passed `tol`, a price tolerance, to
`brentq` as a volatility tolerance. It now solves tightly and returns NaN
unless the repriced option is within `tol` of the market price, the same
check the Newton path applies.
- **Turnover-aware optimiser and min-weight floor** (#1652, #1653, #1885,
thanks @rudycelekli). Turnover is measured on signed holdings: reversing a
50% long into a 50% short counts 0.5, both the close and the reopen.
Allocations kept without optimisation (all cash, short history, a failed
fit) now update the prior holdings the next penalty is measured against.
A single-asset book is checked against its per-name and group caps and
raises when the allocation exceeds them or is not finite. When a
`min_weight` floor cannot be funded by the names above it, the date falls
back to equal weights over the active names, keeping their signs and gross
exposure, instead of pushing a funding name through zero.
- **Risk X-ray returns and downside deviation** (#1717, #1875, thanks
@rudycelekli). Returns are computed per symbol before incomplete dates are
dropped, so a missing close no longer becomes a multi-bar move booked as
one period. Downside deviation is the zero-target root mean square over all
bars, as the backtest's Sortino computes it. The standard deviation of the
losing bars alone read zero for a book that lost 1% on every losing bar.
- **Missing observations stay missing in time-series and validation helpers**
(#1713, #1714, #1715, thanks @rudycelekli). `fit_ornstein_uhlenbeck` builds
lag pairs before dropping missing values, so a gap is no longer fitted as a
one-step transition. `purged_walk_forward_splits` skips a fold whose
training set purging has emptied, as its contract states. Regime edge
density stays unknown on a bar where any pair's correlation is undefined
(a constant or missing series) instead of counting that pair as no edge.
- **Grouped purged K-fold purges groups whose labels reach the test block**
(#1700, thanks @Shizoqua). `group_purged_kfold_splits` takes
`label_end_times`. An earlier group whose labels resolve inside the test
block is purged, as is a later group inside the test labels' horizon, and
the embargo starts after the furthest test label ends. The finishing commit
made the argument keyword-only, so existing positional calls keep their
meaning. It also requires contiguous, chronological groups when the
argument is given. Without it, only the embargo applies, as before.
- **Event-study fitting stays before the event** (#1920, thanks
@rudycelekli). For an event window that opens after the event, for example
`(1, 5)`, the estimation window used to end at the window's start and so
took in the event day. With `estimation_gap=0` it also took in the days in
between. It now ends before both the event and the window, for every
normal-return model.
- **Credit, stochastic-volatility and factor-model inputs** (#1716, #1718,
#1719, #1928, thanks @rudycelekli). `cds_price` keeps premium dates on the
requested frequency and prices a final stub for a fractional maturity.
Before, it spread the periods evenly over the tenor.
`portfolio_style_exposure` drops an asset with any missing factor exposure
and reports its weight as `unmatched_weight`, instead of summing NaN into a
zero exposure. `heston_price` rejects non-finite parameters and a
nonpositive integration limit before integrating.
`factor_risk_decomposition` rejects a specific variance below -1e-8 instead
of clipping it to zero idiosyncratic risk.
- **Non-finite bond and Sharpe-summary inputs are refused by name** (#1824,
#1821, #1822, thanks @he-yufeng and @rudycelekli). An infinite coupon
frequency used to price any bullet bond near par, and an on-coupon-date
shortcut in `accrued_interest` turned a NaN face into 0.0. `ytm_solve`
checks its price and bracket, `key_rate_duration` its tenors and `dv01` its
par amount. `probabilistic_sharpe_ratio` checks all five of its inputs; a
NaN benchmark used to come back as NaN "confidence". `expected_maximum_sharpe`
refuses NaN or infinite trial counts. #1824 supersedes the narrower reports
in #1821 and #1822.
- **Fund math and cash-flow ingestion refuse ambiguous inputs** (#1732,
#1739, #1919, thanks @he-yufeng, @Shizoqua and @rudycelekli).
`american_waterfall` refuses a bare `as_of` date, which it used to ignore
silently; per-deal dates are a mapping. A NaN preferred rate used to hand
every profit dollar to the LP as preferred return, and the result still
reconciled. Hurdle and waterfall amounts must now be finite. Cash-flow and
entity-panel ingestion refuse a single dot followed by exactly three digits
(`12.000` is 12 in a US export and 12000 in a European one) unless
`decimal_separator` is given. Scientific notation such as `1.2e5` is still
accepted.
- **Drawdown duration and three-statement tolerance** (#1784, #1786, thanks
@rudycelekli and @remote-controlled-man). `max_drawdown_analysis` dates the
peak at the latest visit to the high-water mark before the trough, so
revisiting a peak starts a new underwater period. `project_three_statement`
applies the caller's `balance_tolerance` inside each projected period too;
a widened tolerance used to fail there at the module default.
- **Portfolio-weight and copula numerics** (#1694, #1695, thanks
@rudycelekli). Inverse-variance weights no longer overflow on very small
positive variances. Inverse-variance weights and HRP reject a non-finite
diagonal, and HRP now checks a single-asset matrix as well. A Gaussian
copula fitted from a Kendall's tau that rounds to correlation ±1 is nudged
back inside the open interval, and its CDF accepts the
near-singular covariance.
- **`quantlib_call`, VPIN and options-payoff results** (#1826, #1836, #1897,
thanks @rudycelekli). The 5,000-value result budget is enforced while
encoding. Before, a long series past the budget still emitted one
`<truncated>` string per remaining element. VPIN completes a bucket at a
relative tolerance, so scaling volume units no longer changes which buckets
count as full. `options_payoff` returns an error envelope when extreme
inputs overflow to infinity instead of raising.
- **Report audit keeps the sign of a value** (#1663, #1654, thanks
@he-yufeng and @rudycelekli). Accounting parentheses, ASCII or full-width,
are negative: `(25.3)亿` is read as -25.3 亿 with its unit, where it used
to read as +25.3 with no unit. `-25M`, `−25M`, `-$25M` and `$-25M` keep
their minus sign, and `(-25)` is still a negative. In prose a bracketed number
without a unit or currency, such as `(2024)`, is skipped as a year.
Mismatched brackets, a unit both inside and outside, and non-finite or
larger-than-1e15 values are rejected. Reported in #1660.
- **Financial-rigor checks stop reading undefined as agreement** (#1883,
#1906, #1921, thanks @rudycelekli). A zero reported market cap matches
only a zero product, and against a zero cross-source median only an exact
zero agrees. Either way the percent deviation is `null`. A negative median
is compared on its magnitude. Before, these read as a 0% deviation. An
explicit `tolerance_pct=0` or `years=0` is honoured instead of falling
back to 2% and 3 years. Three-scenario upside is `null` at a nonpositive
price, where it used to report 0% ("fairly valued"), and negative `years`
is an error.
- **Swing-based skill signals no longer fire before the swing is known**
(#1873, thanks @rudycelekli). The bundled Elliott-wave and harmonic example
engines stamped a signal on the swing bar itself, which is only confirmed
`swing_window` bars later. They now replay the swings in order and stamp
each signal on its confirmation bar. The optional `pyharmonics` path is
unchanged.
- **Pattern tool extrema and arguments** (#1882, #1923, thanks
@rudycelekli). A flat top or bottom spanning adjacent bars is one extremum,
reported at its first bar. A missing `run_dir`, a list of patterns, a null
or non-integer `window` now get a JSON error or their default instead of
raising out of the tool.
- **Shadow-account RSI uses Wilder's seeded smoothing** (#1457, thanks
@Shizoqua). The first average is the simple mean of the first 14 gains or
losses, and the recursion runs from there. The extractor and the scanner
share one implementation, so the RSI bounds a profile extracts are the ones
the scanner tests.
- **Small-price indicators keep their digits** (#1925, thanks @rudycelekli).
MACD and Bollinger values were rounded to 4 and 2 decimals, so a 1.2e-5
crypto price came back as zeros. Each nonzero value now keeps at least four
significant digits, including a small component next to a larger one;
ordinary prices round as before.
- **Hypothesis search and backtest links** (#1720, #1902, thanks @Shizoqua).
A search with `limit=0` returns nothing instead of one record, and the
`link_backtest` tool forwards its optional `validation` results
(walk-forward, Monte Carlo, bootstrap) to the registry instead of dropping
them.
#### Alpha Zoo, factors and alpha bench
- **Fundamental factors no longer see after-close filings a bar early**
(#1794, thanks @Jackzigen). SEC filings carry only a date and can arrive
after the close, so the benchmark IC path now uses fundamental panels one
bar later and records `fundamental_pit_lag_bars: 1`. IC measured on
same-day filings needs to be rerun. The backtest engine's separate
next-bar execution is unchanged.
- **Carhart momentum no longer reads today's price** (#1578, thanks
@Shizoqua). `academic_carhart_mom` was the 12-month return minus the
1-month return, which moves with today's close, the opposite of skipping
the last month. It is now the return from 252 to 21 trading days ago. A
test moves every close inside the skipped month and requires the value to
stay put. The finishing commit corrected the factor notes, the zoo licence
text and a strategy example that taught the old formula under a factor id
that does not exist.
- **`decay_linear` weights the newest bar heaviest** (#1912, thanks
@rudycelekli). Inside each window the weights ran from n on the oldest bar
down to 1 on today, the reverse of the Alpha101 definition. Every factor
that calls it changes value (22 Alpha101 and 19 GTJA 191 modules); windows
stay causal and the missing-data masks are unchanged.
- **Rolling argmax/argmin ties no longer depend on the backend** (#1898,
#1696, thanks @rudycelekli and @Mikesad). Without `bottleneck`, a tie
resolved to the oldest bar; it now resolves to the most recent, as
`bottleneck` does. `bottleneck` is a core dependency, so most installs
already got the most recent bar. The registry test suite also runs on
Windows hosts without symlink privileges.
- **Alpha benchmark arguments and environment** (#1806, #1910, #1918, thanks
@xaritoku and @rudycelekli). `alpha bench` and `alpha compare` load the
`.env` file before reading credentials, as other CLI commands do. `top=0`
is rejected instead of quietly becoming 20, and boolean, fractional or
non-numeric `top` values return an error envelope. A factor with fewer than
two IC observations is skipped with the reason instead of being ranked
with a NaN IC standard deviation and an IR of 0.
- **38 GTJA 191 factors show their formula**. Their `formula_latex` read
"see body", which the Alpha Library displayed as the formula. It now holds
the formula already quoted in each module's docstring. Metadata only; no
computation changed.
#### Live trading, brokers and portfolio
- **Live option orders are refused before reaching a broker** (#1585, thanks
@lorenzozanee). Robinhood's `place_option_order` is pinned as a write, so a
remote read-only annotation cannot pass it through, and the profile says
live order placement is equities-only. The mandate gate also refuses every
option intent, on both the MCP and SDK paths and even when the mandate lists
options, because share-notional limits cannot bound a contract multiplier or
maximum loss. Live options execution is not implemented.
- **Direct-SDK positions are priced for the mandate exposure check** (#1581,
thanks @Shizoqua). Toss, Dhan, KIS, Zerodha and Shoonya report a position's
price as `current_price` or `ltp`, and Futu its value as `market_val`; none
was read, and one unpriced position fails the exposure check closed, so
every later order on those brokers was refused. A zero or negative price, a
boolean, or a non-finite value or total now fails closed as well.
- **The kill-switch sweep survives its own failures, and stopping a runner
stops it** (#1703, #1704, thanks @Romzhou). An audit-ledger write that
raised used to abort the preemptive cancel/flatten sweep and latch it as
fired, so it never ran again; it is now logged and the sweep continues. A
resting order without an order id, or a position with a missing, malformed,
non-finite, overflowing or boolean quantity or no symbol, is reported as an
error instead of being cancelled blindly or closed for NaN shares. `POST
/live/runner/stop` used to answer `was_running: false` while the scheduler
kept firing ticks, because the driver task returned as soon as the scheduler
started; it now lives as long as the scheduler, and stopping it cancels an
in-flight agent decision and waits for it to end. Each broker keeps its own
runtime job store, migrated from the shared one.
- **US equity live sessions close at 13:00 ET on half-days** (#1706, closes
#1705, thanks @he-yufeng): 27 November and 24 December 2026 and 26 November
2027. The calendar is still a static two-year table.
- **KIS reads see SOR and NXT fills and recover from a replaced token**
(#1726, thanks @passionbull). Order and fill reads asked only the KRX venue,
while most retail orders route through SOR, so an account that had traded
read as empty. They now query KRX, SOR and NXT and merge the rows, and a
venue that fails or a page without a row list fails the read instead of
returning a partial one. KIS issues one token per app key, so another
process can silently invalidate the cached one; an explicit token-expiry
rejection (HTTP error, 401/403 or an HTTP 200 business error) now drops the
cache and retries once. Order placement still goes to KRX only.
- **Binance balances survive clock skew, and unpriced assets mark the snapshot
incomplete** (#1505, thanks @goatyyc). A balance or Simple Earn read
rejected as "ahead of the server" (`-1021`) refreshes the clock offset and
retries once. Quotes, bars and symbol search use an unauthenticated client,
so a public ticker no longer needs a signed request. Binance's account total
is built from priced positions, so a Binance asset without a price now marks
the snapshot incomplete and names the asset, and "last complete" reads and
history skip such snapshots.
- **Futu reads a saved non-HK market and reports both cost bases** (#1812,
#1814, closes #1807, thanks @Frank-zhu2000 and @alexwan02). All four Futu
profiles pinned `filter_trdmarket` to HK, hiding a US account saved in
`futu.json`; HK is now only the default when nothing is saved. A
multi-currency account's balances, which Futu labels `N/A`, carry the HKD
they were requested in. Positions add `average_cost`, `pl_ratio_avg_cost`,
`unrealized_pl` and `realized_pl` (`None` where Futu reports `N/A`), and the
portfolio derives Futu unrealized P&L from the average cost, because Futu's
`cost_price` is diluted by realized gains and can turn negative.
- **Futu `HK.00700`-style codes reach the risk x-ray and holdings merge**
(#1922, thanks @rudycelekli). Market-prefixed codes are rewritten to the
suffix form the loaders use (`00700.HK`, `BRK.B.US`) before risk inputs are
built and holdings are merged across brokers. Snapshots valued before this
change are hidden from latest and history views until the next refresh
rebuilds them.
- **The Portfolio page's holdings and history follow the display currency**
(#1810, thanks @rwxyx518-prog). The holdings table's value and P&L columns
and the history chart used USD whatever currency was selected. Converted
values use the snapshot's own rate and show a dash when that rate is
missing, rather than an assumed one.
- **Weekly and monthly risk x-rays annualise per period** (#1656, thanks
@rudycelekli). `portfolio_risk_xray` annualised `1W` and `1M` volatility
with 252 bars a year; it uses 52 and 12, and `1M` is not read as a minute. A
monthly request without a start date fetches 31 months, enough for the
minimum sample.
#### Trade journal
- **Mixed-currency journals are reported per currency** (#1811, closes #1808,
thanks @Jackzigen). `total_pnl`, `max_drawdown` and dividends summed HKD and
USD round-trips without conversion. With more than one settlement currency,
`total_pnl`, `total_dividends`, `max_drawdown` and `profit_loss_ratio`
become currency-keyed objects with a warning, the overtrading diagnosis is
given per currency, and `top_symbols` ranks by trade count with each
amount's currency; no FX conversion is applied. Single-currency journals
keep scalar values. Drawdown now starts from zero realized P&L, so a losing
first close counts.
- **Amounts and symbols parse the way the broker wrote them** (#1766, #1871,
#1872, #1881, #1916, thanks @remote-controlled-man and @rudycelekli). A
generic `Date,Symbol,Side` journal is claimed as a Futu export only when
Futu's `Quantity` or `Amount` column exists, so its quantities and prices
are no longer read as zero. Accounting negatives such as `(1,234.50)` and
`$(1,234.50)` keep their sign instead of becoming 0. `1.234,56` reads as
1234.56 (a comma-only `1,234` is still thousands). Fiat pairs such as
`EUR/USD` are no longer classified as crypto, `.US` symbols, share classes
like `BF-B.US` included, are US equities, and `.SS` is Shanghai.
- **Dates are combined, ordered and validated before analysis** (#1900, #1941,
#1942, thanks @MetaAviator). Separate generic `date` and `time` columns are
combined, including Excel serial dates and day fractions, and a time alone
never stands in for a missing date. Rows with equal timestamps keep their
file order through FIFO matching. A journal with a missing or unreadable
date returns an error instead of being analysed or filtered without those
rows.
- **FIFO P&L counts reversal fees once, and unbounded ratios are valid JSON**
(#1891, #1884, thanks @MetaAviator and @rudycelekli). A fill that closed one
side and opened the other charged its whole fee to both; the fee is now
split by quantity. A journal with no losing round-trip produced an
`Infinity` profit/loss ratio that strict JSON parsers reject; it is `null`.
When winners and losers both have zero holding time, the disposition check
reports an undefined ratio instead of a bias.
- **Unknown analysis types and filters are rejected** (#1911, #1924, thanks
@rudycelekli). An unrecognised `analysis_type` returned an empty `ok`
result, and an unrecognised filter returned the whole journal as if it had
been applied; both now return an error naming the accepted values (`full`,
`profile`, `behavior`, `strategy`; a date range, `symbol=` or `market=`).
#### Strategy store
- **Strategy records keep their trading rules, and declared backtests feed
decay monitoring** (#1848, #1849, thanks @alexwan02). `sdm_register` accepts
`entry_rules`, `exit_rules` and `position_sizing` (a list is stored one rule
per line) instead of dropping them. Nothing used to write `bench_history`,
so `sdm_status(decay_check)` and `sdm_decay_scan` always reported
insufficient data: a backtest whose `config.json` names an `artifact_id` now
records its Sharpe, annual return, max drawdown and Calmar and reports a
`bench_record` (`recorded`, `skipped` or `failed`, with a reason). Decay
monitoring reads only `initial` and `periodic` rows, so an undeclared rerun
(`manual`) cannot move the baseline. Failed runs, runs without trades or
metrics, and a measurement already stored, even from a copied directory, are
skipped, atomically across connections; a different evaluation window is a
new measurement. Bookkeeping errors never fail a successful backtest.
- **SDM decay compares disjoint IC windows** (#1699, thanks @Shizoqua). The
baseline and recent windows are the oldest and newest five IC observations;
with fewer than ten they overlap (and with five or fewer they are the same
window), which pinned `ic_ratio` near 1.0 however far the IC moved.
`ic_ratio` is now `null` until ten observations exist.
#### Tools and documents
- **A refused file write names the setting that allows the path** (#1679,
thanks @he-yufeng). When `write_file` or `edit_file` refused a path outside
the allowed roots, the error said only that the path escaped. Now it lists
the allowed roots and names `VIBE_TRADING_ALLOWED_WRITE_ROOTS`; read-side
refusals name `VIBE_TRADING_ALLOWED_FILE_ROOTS`. The variable is also
documented in `.env.example` and the README. The boundary itself is
unchanged. Since `aac03fa6`, on a case-insensitive filesystem a path spelled
differently from an allowed root (`dropbox` for `Dropbox`) is accepted when
the filesystem confirms it is the same directory. Path names are never
case-folded.
- **Skill file removal follows the same rules as skill file writes** (#1639,
thanks @Shizoqua). `remove` could delete any file in a skill directory. It
is now limited to `references/`, `templates/`, `examples/` and `assets/`,
like `write`. Since `8ec3b80f`, both actions also refuse `..` segments and
symlinks that resolve outside those directories, and `remove` deletes only
regular files, never a directory.
- **`read_file` reports an unreadable directory as a structured error**
(#1946, thanks @rudycelekli). Listing a directory inside the allowed roots
ran outside the tool's error handler. A permission error therefore escaped
instead of returning the usual JSON `status: error`.
- **Document reader keeps more of PDF, Word, PowerPoint and Excel files**
(#1838, #1839, #1840, #1841, thanks @rudycelekli).
- PDF: with no OCR engine, a page whose native text is shorter than
`min_text_per_page` (50) used to be skipped. Its text is now kept, and the
result is marked `no_ocr_engine`. If every selected page has only such
short text, the read is still refused with the OCR install hint.
- Word: paragraphs and tables come out in document order. Before, every
table was appended after all paragraphs.
- PowerPoint: text inside grouped shapes and tables is read; merged table
cells appear once.
- Excel: cells holding `NA`, `N/A`, `null` or `NaN` keep that literal text.
Only empty cells count as missing.
- **README tables match the code** (#1671, #1733, thanks @he-yufeng and
@Jackzigen). The Indonesian README lists `get_southbound_flow` and the
current tool counts. The Quant Library contract table in `README.md` had
duplicate rows for `credit`, `timeseries`, `factormodel` and
`multipletesting`; each now has one row with the newer description.
#### Sessions, memory and search
- **Runtime files stay out of a checkout used as the runtime root**. When
`VIBE_TRADING_HOME` points at the repository, `memory_index.db` (moved to
the runtime root in this release, and holding memory contents), the paper
trading `strategies/` folder and `shadow_reports/` were not git-ignored.
They are now, and the guard test also finds names joined onto the runtime
root inline or through `*_DIRNAME` constants, not only `*_FILENAME` ones.
- **Session search finds Chinese, Japanese and Korean text** (#1686, thanks
@Shizoqua). SQLite's default tokenizer treated a run of East Asian
characters as one token, so a query rarely matched anything shorter than the
entire run. The ported fix indexes those characters split apart.
`aac03fa6` reworked it: ideographs, kana and Hangul are indexed one
character per token in a separate `search_content` column; queries become
phrase matches; and the stored message keeps its original text for
snippets. An existing `sessions.db` is upgraded and reindexed once, on first
open.
- **Session search snippets, limits and concurrency** (#1834, #1827, #1828,
thanks @rudycelekli). A snippet now centres on the match SQLite actually
found and highlights that span in the original text. Before, it highlighted
the query wherever the text contained it, so `alpha` lit up inside
`alphabet` while `café` (matched by `cafe`) was not highlighted. The result
limit now counts distinct conversations. Before, the search read only the
first five matching messages per requested conversation, and one chatty
conversation could fill them all and hide the others.
Index writes and searches on the shared SQLite connection are serialized,
so concurrent callers no longer interleave on one cursor.
- **Renamed conversations show their new name in search** (#1832, thanks
@rudycelekli). The REST rename and auto-title routes now update the search
index's title as well. Auto-titling reloads the session after the model
answers, so updates saved meanwhile are kept, and a manual rename made
meanwhile is kept with status `kept`.
- **Reconnecting to a session's event stream no longer repeats an event**
(#1798, closes #1796, thanks @Shizoqua). An event published between the
replay snapshot and the new subscription reached the client twice, once in
the replay and once live. Both steps now happen under one lock.
- **Memory search snippets and titles read back as written** (#1682, thanks
@Shizoqua). A CJK snippet could show its span twice ("使用用移移动…")
because the index stores both single characters and pairs. A title mixing
Chinese with a ticker, number or punctuation came back with extra spaces
(`比特币A策略` became `比特币 A 策略`). Since `8ec3b80f`, highlights and
`...` truncation markers also survive in snippets built only from character
pairs.
- **Memory removal, links and recall** (#1697, #1771, #1772, thanks
@Shizoqua). `remove()`, which the `remember` tool's `forget` action uses,
accepts a filename stem as `find()` already did. Before, it reported "not
found" for an entry that `find()` and `vibe-trading memory forget` could
see. A blank name is refused (`f6e72428`). Semantic links match
two-character identifiers such as `BP` or `GE`; the minimum used to be
three. FTS recall skips index rows whose memory file was deleted outside the
process. The PR fetched four times the requested count; `54e225de` replaced
that with a window that doubles until enough current memories are found or
the index runs out.
- **Verifying an archived audit ledger reports a corrupt line instead of
crashing** (#1721, thanks @Shizoqua). `verify_chain_with_archives` parsed
every segment with a bare `json.loads`, so a damaged line raised
`JSONDecodeError`. It now uses the same parser as single-file verification
and returns a `malformed_json` break at that line's position across all
segments.
#### Credential redaction
- **Environment-style credential names and URL credentials are redacted**
(#1765, thanks @cgycorey). `TUSHARE_TOKEN=…`, `AWS_SECRET_ACCESS_KEY=…`
and `aws_secret_access_key` lines in `~/.aws/credentials` are now caught.
Lowercase identifiers such as `pad_token` and `max_token` are still left
alone. `scheme://user:password@host` becomes `scheme://[redacted]@host`, and
a password containing `@` is handled. A bare username before `@` is redacted
only when it is a 20-plus-character token, so `git@github.com` stays
readable.
- **`Authorization: Basic` and other non-bearer schemes are redacted**
(#1846, thanks @cgycorey). The key pattern used to redact the scheme word
and leave the base64 `user:password` visible. `Basic`, `Digest`, `Token`,
`Negotiate` and `NTLM` values are now redacted, but only right after an
`authorization` label, so prose like "basic information" is untouched.
- **More secret formats are redacted** (#1874, #1866, #1867, #1868, #1869,
#1870, thanks @rudycelekli). #1874 combined the five smaller PRs. These are
now redacted:
- unlabelled GitHub fine-grained (`github_pat_`), GitLab (`glpat-`), npm,
Hugging Face, Stripe (`sk_`/`rk_` live and test), Google API (`AIza`) and
AWS `ASIA` keys;
- PEM and PGP private-key blocks, including a block cut off before its END
line, which is redacted to the end of the text;
- `Cookie:` and `Set-Cookie:` header values;
- fields named like `cookie` or `private_key` in structured payloads.
Public keys and certificates are left as they are, and running redaction
twice changes nothing. Since `47de62de`, a quoted header such as
`curl -H "Cookie: a=\"x\"; b=y"` is redacted up to its closing quote.
- **Telegram bot tokens, Slack and Discord webhook URLs, Groq and NVIDIA keys
are redacted** (#1880, thanks @rudycelekli). The Telegram token is caught
inside `https://api.telegram.org/bot<token>/…`, which HTTP error messages
repeat in full. For a webhook URL, only the secret last path segment is
replaced. The PR accepted only 8–10 digit Telegram bot ids and lowercase
`hooks.slack.com`. `2c00cd20` widened that to any length of numeric id, to
GovSlack (`hooks.slack-gov.com`) and to webhook hostnames in any letter case.
#### CLI, API and scheduling
- **`vibe-trading update` leaves a Git install alone** (#1655, thanks
@rudycelekli). An install made from a Git URL was treated as a PyPI wheel,
so the update replaced the user's chosen revision with a released wheel. The
command now detects `vcs_info` in the install metadata, reports the newer
PyPI version and tells the user to update from the original source.
- **The terminal dashboard matches parallel tool results to the right call**
(#1859, closes #1856, thanks @Shizoqua). When several calls to the same tool
ran at once, progress and results were matched by tool name and recency, so
results could be swapped between calls. They are now matched by call id.
Since `47de62de`, a result whose id matches no step gets its own step and no
longer completes a different call that is still running.
- **CLI limits and number formatting** (#1886, #1926, thanks @rudycelekli).
`vibe-trading list --limit` and `memory search --limit` reject zero and
negative values instead of passing them on. Token counts, abbreviated
numbers and durations switch unit when rounding reaches the next one:
999,950 tokens shows as `1.0M tokens` instead of `1000.0k tokens`, 999.6 ms
as `1.0s`, and 59.96 s as `1m 00s` instead of `60.0s`.
- **A cancelled upload leaves no partial file** (#1945, thanks @rudycelekli).
If the upload request was cancelled while a chunk was being read, the
half-written file stayed in the uploads directory with no reference to it.
The file is now deleted and the cancellation is re-raised.
- **More runtime files follow `VIBE_TRADING_HOME`** (#1896, #1904, #1887,
thanks @rudycelekli). These paths were fixed to `~/.vibe-trading` and now
use the runtime root:
- persistent memory and its search index (`memory/`, `memory_index.db`);
- autopilot run directories;
- the alpha-bench panel cache and reports;
- the Gildata code cache.
The Shadow Account report route also reads from the runtime root, which is
where the report writer saves. Report links therefore work again when
`VIBE_TRADING_HOME` is set. Existing files under `~/.vibe-trading` are not
moved, and the memory paths are read when the process starts.
#### Channels and scheduled delivery
- **IM channels show agent progress again, without repeated lines** (#1878,
thanks @alexwan02). Channel replies now forward the attempt's status lines
(working, waiting for the model, using a tool, checking the figures), tool
hints and opt-in reasoning while the agent runs, so Feishu's streaming card
and other streaming channels update again (reported in #1792). Only those
labels go out: answer drafts and tool results stay private, and the final
answer still passes grounding first. #1878 applies the channel's own
visibility settings before deduplicating, so hidden tool hints no longer
break the dedupe and a chat no longer stacks one "Waiting for the model" per
tool round.
- **Streamed replies are no longer lost or split** (#1860, #1903, #1929,
closes #1857, thanks @Shizoqua). Telegram (#1860) and Matrix (#1903) dropped
the whole answer when the live preview message could never be created; they
now send the accumulated reply as a new message. CI installs the `matrix`
extra so the Matrix test runs instead of skipping. WeCom (#1929) generated a
new stream id for every chunk, so progress and the final answer arrived as
unrelated messages; one id now covers each reply.
- **Email sender authentication cannot be satisfied by forged headers**
(#1508, thanks @Shizoqua). SPF and DKIM checks read only the single
`Authentication-Results` header bearing the configured
`trusted_authserv_id`; with none configured, or with two matching headers,
verification fails closed. A pass counts only when its domain equals the
From domain or an aligned `dmarc=pass` is in the same header, and comments
or quoted text cannot pose as results. The receiving provider must strip
forged copies bearing its id; the id is not a signature. The guided Email
setup exposes the setting, and a blank one rejects inbound mail while SPF or
DKIM checks are on. The PR described trusting the topmost header; what
shipped selects by configured id instead.
- **URL credentials in channel settings are neither leaked nor erased**
(#1742, #1744, thanks @cgycorey). Status and config redaction strips
`user:password@` even when the port is malformed or out of range or the URL
is followed by punctuation, where it used to return the URL untouched
(#1741). Saving or testing a form that echoes the redacted URL keeps the
stored credential instead of overwriting it (#1743); a different URL or a
clear flag still applies. Connection-test and reset messages, including
raised SDK exceptions, are scrubbed too.
- **Unchanged empty fields no longer block channel saves** (#1681, thanks
@zeus229). A nullable setting such as Email's `post_action` was submitted by
the form as an empty string and rejected with a 422 on unrelated edits. An
empty value now maps back to null where the adapter's default is null, for
both Save and Test.
#### Web UI and locales
- **Run trades list every trade, newest first** (#1865, #1877, #1831, thanks
@alexwan02 and @he-yufeng). The Trades tab and its CSV download used the
first 500 rows of `trade_log`, which dropped the newest trades of a long
run; they now use the full `trades.csv` rows (#1865). Rows sort newest first
with a header toggle, keeping same-time fills in engine order, and the
symbol filter lists codes in order (#1877). #1831 removes a race in the
Settings effort-level test that intermittently failed frontend CI; the
component is unchanged.
Related Repositories
public-apis/public-apis★ 491,217
A collective list of free APIs
NousResearch/hermes-agent★ 256,087
The agent that grows with you
yt-dlp/yt-dlp★ 200,261
A feature-rich command-line audio/video downloader
microsoft/markitdown★ 191,560
Python tool for converting files and office documents to Markdown.
Significant-Gravitas/AutoGPT★ 187,699
AutoGPT is the vision of accessible AI for everyone, to use and to build on. Our mission is to provide the tools, so that you can focus on what matters.