* 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>
Concatenated pairs like BTCUSDT are alphabetic, so isalpha() labeled them
as US equities. Recognize USDT/USDC/BUSD suffixes before that branch.
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
Broker CSV cells like $1,234.56 or ¥1000 failed float() after only commas
were stripped, so price/fee silently became 0.0. Strip common currency
tokens and unicode minus too.
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>
Notepad/Excel Unicode text exports start with a UTF-16 BOM; latin-1 never
raises so those bytes were accepted as mojibake. Detect BOM and decode as
utf-16 first.
Signed-off-by: santhreal <64453045+santhreal@users.noreply.github.com>