Adds a read-only agent tool over a published Taiwan-market SQLite
snapshot (stdlib + sqlite3 only, mode=ro&immutable=1, no network).
Maintainer repairs on top of the contribution:
- Bulk responses are now budgeted to 9,500 serialized characters so they
survive the agent loop's 10,000-char tool-result cut as valid JSON. At
the documented maximum (200 rows x 50 stocks) the response was 5.4 MB
and the prefix cut produced a JSON parse error. Rows are retained
newest-first, round-robin across stocks, so the newest bar of every
stock survives trimming; responses carry total_rows, returned_rows,
truncated and a narrowing hint. The budget covers history, universe,
latest and lookup — the size overflow was not limited to history.
- Indicators are no longer mis-dated. The tool picked the newest price
row and the newest feature row independently but reported a single
date, so 07-23 indicators were served under date=07-24. latest and
universe now return price_date, feature_date and a stale_features
flag.
- The Compose snapshot mount no longer defaults inside the checkout
(./data/tw-stock was also un-gitignored). The default host path is
now ${HOME}/.vibe-trading/tw-stock, overridable via
VIBE_TW_STOCK_DATA_DIR; no market data can land in the working tree.
- The snapshot schema (4 tables, 37 columns) is validated on open, so an
unrelated or stale SQLite file reports an actionable error instead of
registering successfully and failing mid-query.
- Fixed a connection leak: sqlite3's connection context manager commits
but never closes, leaking a handle per call.
The contributor's unrelated requirements-lock.txt rewrite (21 stale pin
changes, including FastAPI and OpenAI downgrades) was dropped; the tool
needs no dependency change.
TraceWriter.write() only flushed, so a host crash or container kill lost
the last records. Each record is now fsynced, and — because a fsynced
record must never point at a blob that is not on disk — sidecar writes go
through temp-write -> fsync -> os.replace -> parent-directory fsync
before the referencing record is written. New trace.jsonl and sidecar
directory entries are also fsynced.
fsync failures (unsupported filesystem) log one warning per writer and
degrade to flush-only rather than breaking the run.
Trade records carrying pnl=None or a non-numeric value raised TypeError
while computing trade statistics. Skip those records instead, and — since
trade_count still reports every record — append a warning naming how many
were excluded so win_rate and profit_loss_ratio are never silently
derived from a subset.
compute_edge_density divided by n_pairs, which is zero for a zero- or
one-column frame. Correlation edge density is undefined without at least
one distinct asset pair, so return an index-aligned all-NaN series. The
timeline entrypoint already requires two fetched assets, so this does not
mask a user-facing error.
Supersedes the duplicate guard bundled in #912.
The input validator requires the original basket total to be positive but
permits individual zero weights, so the history filter can leave only
zero-weight survivors and the renormalization divided by zero. Raise a
clear ValueError instead: allocating capital to zero-weight survivors
would invent holdings the user does not own, and an all-zero x-ray would
contradict the fully-invested-basket contract.
- POST /sessions/{id}/title/auto — summarize the first exchange into a short
title in the user's language; never clobbers a manual rename
- Frontend fires it after the first completed exchange and refreshes the sidebar
- reasoning_delta now carries a bounded 600-char rolling tail on the existing
throttled emit; ActivityLine whispers the live thought (serif italic, 3-line
clamp) while the attempt is active
One durable activity object per turn rendered by a single ActivityLine
(verb + newest step + 1s tabular timer; userOpen ?? auto disclosure with
900ms dwell; cancel/timeout/failure leave terminal rows with Continue and
re-attach; scattered spinners and the pulse bar removed). Escape-to-stop
and full + menu keyboard contract; follow-up placeholder after runs;
hidden-tab completion title marker. Agent.tsx decomposed 2353→1686 via
memoized Composer (byte-faithful IME guard + render-isolation test),
GoalPanel, LiveRuntimePanel. ja/ko/ar translated to full 929-key parity.
Assistant narrative paragraphs read in a CJK-safe serif at 15.5px; tables,
metrics, and code stay sans/mono. Remaining page-level hardcoded strings
localized.
Backend (surgical, additive): call_id on tool_call/result/progress/heartbeat
(loop.py, swarm worker); tool_trail serialized onto assistant messages at
attempt completion and exposed through session history. Frontend: exact
call_id matching with FIFO replay fallback, trail rebuild on reload via the
same helper as the live path, 84 localized tool labels, MetricsCard plain-
language glosses, focus-trap dialogs, overlay connection banner with
terminal reload state, cross-tab prefs sync, idle echarts prefetch, mandate
zh glossing (交易授权), swarm vocabulary unification, '/' product-first
routing with /about.
* feat(backtest): emit risk x-ray artifacts in portfolio runs (Portfolio Studio slice)
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
* fix(backtest): refuse the average-basket x-ray for long-short books
compute_risk_xray is long-only by contract, but average_invested_weights
filtered negative mean weights out before the check could fire — a
long-short run would emit an x-ray describing only its long half while
presenting it as the whole strategy. Net short average exposure now
raises, taking the same no-artifact path as flat and short-history runs.
---------
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
* fix(backtest): keep bar_returns bit-identical for gapped positive series
#872 replaced close.pct_change().fillna(0.0) with bar_returns and stated that
results for strictly positive series are bit-identical, so ordinary equity and
crypto runs are unchanged. That was verified only on gapless series.
On the pinned pandas (>=2.0,<3.0) pct_change defaults to fill_method='pad' and
forward-fills the divisor. bar_returns divided by the raw shifted series, so any
positive series with an interior NaN diverged. _align deliberately limits its
ffill to 5 bars (base.py:166-179) to keep long suspensions visible, so a halt
longer than that reaches bar_returns as a NaN prior and the real move across it
is reported as 0.0 -- silently, because the undefined-bar counter excludes NaN
priors by construction. On a real _align run an 8-bar halt turned a +17.1429%
resumed-trading bar into +0.0000%.
Injecting a zero where a large move occurred shrinks dispersion. Return deltas
cancel by sign; that damage does not. On a 20-symbol universe with two halted
symbols and news-shaped gaps, annualised vol fell 4.9619% -> 4.4673% and Sharpe
rose 3.0502 -> 3.3817.
Carry the prior price forward explicitly. This restores the pre-#872 answer and,
unlike the deprecated default it replaces, gives the same result on every
supported pandas -- the old behaviour flips silently under pandas 3.0, where
fill_method is None. The #872 contract is untouched: a non-positive prior is
still undefined, still 0.0, still logged.
Second, smaller item: inf > 0 is True, so where(prev > 0) kept an infinite prior
and the bar after it read as a clean -1.0 with no warning. buy_and_hold_return
had the same asymmetry -- it checked isfinite(last) but only first > 0, so an
infinite entry price returned -100% instead of None. Both now assert finiteness.
Signed-off-by: Ali Emadi <ali@gatekeeperai.com>
* test(backtest): lock the _align boundary and align the undefined-bar warning
- add an engine-boundary regression: a halt longer than _align's ffill
limit must still credit the across-gap move to the resumed bar
- the undefined-bar warning now also fires for non-finite prior prices,
so say so in the message (and in the assertions that pin it)
---------
Signed-off-by: Ali Emadi <ali@gatekeeperai.com>
Co-authored-by: Ali Emadi <ali@gatekeeperai.com>
Co-authored-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
`main()` routes the `connector` subcommand group straight to
`_dispatch_connector`, which — unlike the interactive TUI — never runs
preflight, so nothing populated `os.environ` from `~/.vibe-trading/.env`.
`EnvConfig` reads only `os.environ`, so every connector resolving
credentials through `get_env_config()` reported them as missing despite a
valid file on disk: Longbridge's app key / secret / access token (the only
broker credential set sourced from env vars rather than a JSON config
file), and Futu's `FUTU_TRADE_PWD_MD5` live-unlock hash.
Call the existing `_ensure_cli_env()` helper at the dispatcher entry.
`_ensure_dotenv()` is idempotent and loads with `override=False`, so a real
environment variable still outranks the file, and connectors that read JSON
config are unaffected.
Distinct from #477 / #541, which were the stale-`EnvConfig`-singleton
variant: there `_ensure_dotenv()` ran but no `reset_env_config()` followed.
Here it never ran at all, so that fix could not reach this path.
Closes#901
Signed-off-by: Xiaotian Jin <jin.xiaotian.kim@gmail.com>
* fix(metrics): add missing annualisation entries for 15 data sources
13 of 24 VALID_SOURCES entries had no trading_days or bars_per_day
lookup, causing calc_bars_per_year to silently return 252 for every
interval — intraday bars for these sources were never annualised
correctly (e.g. Longbridge 1m returned 252 instead of 252×390).
Add all 15 missing sources to _TRADING_DAYS and _BARS_PER_DAY:
- crypto: binance (365 days)
- A-share: baostock, tencent, eastmoney, sina (252 days, 240-min)
- US/international: yahoo, finnhub, alphavantage, tiingo, fmp,
stooq, longbridge (252 days, 390-min)
- Indian: india_broker (252 days, 375-min)
- daily-only: qveris, local (252 days, 1 bar/day for all intervals)
24 new test assertions across 16 tests, including a cross-VALID_SOURCES
sweep that verifies every source returns >0 for both daily and intraday.
Closes#884
Signed-off-by: Robin1987China <41602358+Robin1987China@users.noreply.github.com>
* fix(metrics): restore _SOURCE_ALIASES, _normalize_interval, and related tests
These were accidentally dropped during stash conflict resolution;
restore the alias/interval-normalisation layer that loaders depend on
and re-add the 3 corresponding tests (lowercase hour/day matching,
yahoo→yfinance alias).
Signed-off-by: Robin1987China <41602358+Robin1987China@users.noreply.github.com>
* fix(metrics): address review — reclassify local/qveris, harden drift guard
Three changes from review (#891):
1. Reclassify local and qveris from daily-only to US equity session
(252d/390-min) — both loaders support all intervals via resampling.
Updated all 7 interval layers of _BARS_PER_DAY.
2. Already fixed in d6770bd: source_key lower normalisation via
str(source).strip().lower() + _SOURCE_ALIASES guard — no change needed.
3. Strengthen test_no_intraday_source_falls_to_default with a
set-coverage assertion that fails the moment a loader is registered
without a _TRADING_DAYS + _BARS_PER_DAY entry.
Signed-off-by: Robin1987China <41602358+Robin1987China@users.noreply.github.com>
---------
Signed-off-by: Robin1987China <41602358+Robin1987China@users.noreply.github.com>
* fix(channels): preserve leading indent when splitting messages
After a newline cut, lstrip() removed indentation on the next chunk, so
code blocks and nested markdown lost spaces across Discord/Slack/Telegram
splits. Drop only the separator character.
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
* fix(channels): keep Signal style offsets aligned after indent-preserving splits
split_message no longer blanket-lstrips; mirror that in _partition_styles so
indented chunks keep UTF-16 style ranges.
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
* fix(channels): advance style cursor on space separator preceding indented chunk
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
---------
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
* fix(skills): parse frontmatter when closing fence is at EOF
SKILL.md / memory files that end at the closing --- without a trailing
newline (common from editors and LLM skill saves) were treated as plain
text, so name/description were dropped. Empty ---/--- blocks had the
same miss. Accept EOF or empty meta between fences.
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
* fix(skills): require newline before closing frontmatter fence
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
---------
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
React Router 8 requires Node >=22.22.0, so `npm install` now fails for anyone
still on Node 20 with an error that does not name the cause. Declare it in
package.json engines, and say it on the Quick Start line that tells people to
run it, in all five READMEs.
React Router 7.12.0 through 8.2.x carry a high-severity RSC-mode CSRF bypass
(GHSA-qwww-vcr4-c8h2). We were on 7.18.1, inside that range. The advisory is not
reachable here — the app is a client-side SPA on createBrowserRouter with no
@react-router/server and no RSC entry point, and the frontend ships as static
Vite output — but 7.18.1 is the final 7.x release, no backport exists, and 8.3.0
is the only patched version, so staying put means staying on an EOL line with a
permanent open alert.
react-router-dom has no 8.x at all; the DOM exports moved into react-router
itself. So the dependency is swapped and every import rewritten. All ten APIs in
use here are unchanged in 8.3.0, and none of the data-layer surface that changed
most between majors is used.
react-router 8 requires Node >=22.22.0, which is the same wall that had jsdom 30
and @testing-library/jest-dom 7 sitting in the Dependabot ignore list. Node 20
reached EOL on 2026-04-30, so the toolchain moves to Node 22 across CI, the wiki
workflow, the Dockerfile and docker-compose. That unblocks all three at once:
jsdom and jest-dom are taken here too, and their ignores are removed rather than
left carrying a rationale that no longer holds.
Verified on the frontend suite: tsc -b and vite build clean, 310 tests across 35
files pass before and after the jsdom/jest-dom bumps.
Problem
-------
Next-gen Claude models (e.g. claude-opus-5, claude-opus-4-8, claude-sonnet-5)
reject the "temperature" request field with HTTP 400 "temperature is deprecated
for this model." for ANY value, while older models (claude-opus-4-5,
claude-sonnet-4-5, ...) still accept it. _build_anthropic unconditionally
forwarded the global default temperature=0.0, so every run on a next-gen model
failed before producing output. Model names are not predictable (opus-4-5
accepts it, opus-4-8 does not) and the rejection is value-independent, so
neither a fixed model allowlist nor an "omit when 0.0" shortcut is a correct
fix.
Solution
--------
Add a temperature-safe ChatAnthropic subclass (_make_temperature_safe_anthropic)
that wraps all four generation entry points (_generate, _agenerate, _stream,
_astream). When the API reports temperature as unsupported it drops the field
and retries once, then records the model in a process-wide set so subsequent
requests omit it up front (no repeated failed request). Models that accept
temperature keep their configured deterministic value unchanged. The subclass
is built from the lazily-imported class and cached per base class, preserving
the optional langchain-anthropic dependency boundary and existing test doubles.
Correct for any temperature value, current and future model names, and the
streaming path the Web UI relies on.
Tests
-----
Add regression tests in agent/tests/test_llm.py: self-heal retry drops
temperature and remembers the model, temperature is preserved for supporting
models, and the error matcher only triggers on temperature-unsupported errors.
Full test_llm.py suite: 58 passed.
ccxt does not use ranges — it exact-pins its entire transitive closure. All 23
of those packages that appear in our lock sit at precisely ccxt's pin, with no
mismatches, so none can move until ccxt itself releases, and ccxt 4.5.68 is
already the latest. Any Dependabot PR touching one is unmergeable by
construction: the pip group PR proposed five of them and died with
ResolutionImpossible on certifi against the ccxt line in the lock.
Re-running the resolver confirms this is the ceiling rather than a stale lock —
pip-compile --upgrade under python:3.11-slim, matching CI, reproduces every one
of those versions, so there is no lock change to make. pydantic-core is in the
same position via pydantic's own == pin, and was the sixth package in that PR.
Ignore the set. ccxt itself is deliberately not ignored: it is the package whose
bump legitimately carries all of them. This suppresses only the automated PRs —
Dependabot alerts come from the dependency graph and are unaffected by this
file, so a CVE in cryptography, urllib3, requests or certifi still surfaces in
the Security tab, where the remedy is a ccxt bump.
close.pct_change() silently assumes price[t-1] > 0. That holds for equities but
not for instruments that can print zero or negative prices, such as European
day-ahead power. As the divisor approaches zero an ordinary absolute move reads
as an enormous percentage move, so the compounded benchmark explodes — a
reported +39,560% where the honest figure was -42.7%. At exactly zero the next
bar is inf, which fillna(0.0) does not neutralise because it fills NaN, and
(1 + r).prod() then collapses to nan.
Adopt an explicit contract. A return is defined only when the previous price is
finite and strictly positive; otherwise it is 0.0, never inf or nan. Buy-and-
hold total return becomes the price relative P_end / P_start - 1 rather than the
compounded product, and is omitted entirely when the entry price is not
positive, because no honest percentage exists. Undefined bars are logged rather
than silently swallowed: a wrong return is a wrong reported number, not a crash.
bar_returns keeps the exact `close / prev - 1` expression pct_change uses, so
results for strictly positive series are bit-identical and ordinary equity and
crypto runs are unchanged.
The loader still rejects an exact-zero close, but the reason has narrowed to the
one that survives: sizing is target_notional / abs(price), undefined at zero.
Closes#872
The research-goal tools are registered unconditionally and resolve their
session from the host runtime, so an entry point that calls _run_agent without
an id makes every goal call fail validation with "session_id is required" while
the run still reports SUCCESS. Reported for `vibe-trading run`; six of the seven
_run_agent call sites had the same omission, including cmd_session_chat, which
already received the session id as a parameter and simply never forwarded it.
Add _ensure_session_id, which builds a Session (whose id is populated at
construction) and persists it best effort, so a store or index failure still
yields a usable id rather than the empty string that caused the bug.
cmd_continue reuses one session across continuations so goals accumulate: a
session-backed run_id already is a session id, since sessions and their traces
share SESSIONS_DIR, so it is used directly; a plain run gets a derived id.
Re-registering an existing id is idempotent. The interactive REPL had the same
latent hole whenever _new_session returned None, and now falls back to an
unpersisted id.
Also align the MCP contract. All four MCP goal tools declared session_id
required while the in-process registry injects it and leaves it optional — the
opposite contract, which asked the model to invent an internal identifier it
has no way to know. session_id is now optional there too, defaulting to one
stable id per server process.
Closes#885
CMD has no `cp`, so `cp agent/.env.example agent/.env` fails there; add the
`copy` form and note that PowerShell's `cp` alias already works. Also add the
CMD activation line and the execution-policy hint that blocks Activate.ps1.
Reported by @brady-cec in discussion #29.
jsdom 30.x requires Node ^22.22.2 || ^24.15.0 || >=26 while CI and Docker are
on Node 20. On Node 20 the incompatibility surfaces indirectly as vitest
failing to start its pool workers rather than as a clean engine error, so #877
reads like a flaky frontend failure. Same wall as @testing-library/jest-dom 7.
Dependabot could not land either half of a mutually-blocking pair: the
minor/patch group wanted ccxt 4.5.68 (which pins setuptools==83.0.0) while
keeping setuptools at 82.0.1, and the separate setuptools 83.0.0 PR conflicted
with the ccxt 4.5.67 already in the lock. Neither can go green alone because
setuptools is a major bump and is excluded from the minor/patch group by
design, so it cannot ride along.
Regenerate the lock so both move together. Verified with pip-compile under
Python 3.11 on Linux (matching CI) and checked with CI's own gate,
pip install --dry-run --require-hashes. Note the lock is platform-sensitive:
regenerating on macOS drops jeepney/secretstorage and swaps py-mini-racer for
mini-racer, so it must be produced on Linux.
Also ignore websockets, capped at <16 by langgraph-sdk 0.4.2, and move all
three ecosystems to monthly with 2 concurrent PRs. Security updates are a
separate channel and are unaffected.
caio: aiofile 3.11.1 (latest) requires caio~=0.9.0, so bumping caio to 0.10.x
makes pip unresolvable and the grouped PR fails CI. Ignore only caio, not the
whole minor/patch group.
@vitejs/plugin-react 6.x peers vite@^8 (we are on 6.x) and
@testing-library/jest-dom 7.x requires Node >=22 (CI/Docker use 20). Both are
migrations, not dependency bumps.
loader_cache_enabled() returned the raw config value, so a MagicMock config
read truthy and switched the opt-in cache on. loader_cache_root() then passed
that MagicMock to Path(), which resolves via __fspath__ to a *relative* path
and put real market-data parquet under agent/MagicMock/ inside the repo.
Validate both at the boundary: require a genuine True to enable the cache, and
honor the root override only when it is a real non-blank str.
Resolves the import-list conflict in agent/tests/test_trade_journal.py
introduced when #862 landed on main: keep both _to_float (this branch)
and load_dataframe (#862).
Under the project's pandas>=2,<3 pin, pct_change defaults to
fill_method='pad'. A halted session (NaN close) is forward-filled into a
fabricated 0% return, which survives the inner join and gets paired against
the peer's real move for that day.
regime.py:113 performs the identical computation with fill_method=None, and
its docstring states it "Mirrors the alignment in
_rolling_correlation_matrix" — the sibling was written from this function and
fixed there while the original was left behind. risk_xray.py:144 also passes
fill_method=None. The intended behaviour is already established in-tree.
Passing it explicitly also silences the FutureWarning; the default flips
again under pandas 3.
Add task-scoped cancellation, reject broad Python process-name kills, expose timeout context, and cover the lifecycle on Windows 11 and Windows x64 with Python 3.14.
Signed-off-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
vn.py 4.0 split every app (CTA strategy, CTA backtester, ...) into its own
pip-installable package. The vnpy-export skill's generated strategy template
and SKILL.md docs still used the pre-4.0 import path
(`vnpy.app.cta_strategy`), which no longer exists — a strategy file exported
by this skill would fail to import in any current vn.py install.
Verified against a real vn.py 4.0 + vnpy_ctastrategy install: the old import
raises ModuleNotFoundError; the corrected import resolves and the exported
MaCrossStrategy class loads successfully.
Signed-off-by: y85998607 <y85998607@gmail.com>