Prepares the 0.1.13 release and fixes the defects found while verifying its
claims. The pieces interlock — the documented counts are the code's surface —
so they land together rather than as commits that fail their own tests.
Code:
- attribution and impact carried no __all__, and quantlib_call dispatches on
__all__ alone, so the tool listed ZERO functions for both while the package
docstring advertised them. Both now export; exposed callables 184 -> 195.
- Six read-only analytics tools reach MCP for the first time (64 -> 70):
quantlib_call, cashflow_performance, orderbook_depth, sentiment,
technical_indicators, get_fundamentals. They use the existing mirrored path,
which refuses any class whose is_readonly is not True, so order-placing tools
remain structurally unreachable over MCP.
Tests that measured a proxy instead of the truth:
- The manifest tool count was derived from @mcp.tool decorators, which ignores
every mirrored tool. Four institutional-research tools were live over MCP and
absent from the published SKILL.md, and the contract test asserted that
absence was correct. Both now measure mcp.list_tools().
- test_every_allowlisted_module_actually_imports only asserted ok is True, which
can never catch an empty listing. A new guard fails when any allowlisted
module exposes no callables; removing an __all__ names exactly the two
offenders.
- The README count tests only proved the five files agreed with each other and
with a pinned constant, so a uniform error was invisible — that is how the
broker count sat at 12 after eToro shipped as the 13th. The pin is now
anchored to the profile registry, every connector must appear in the table,
and the quantlib export count joins the verified badges.
Docs, corrected against measurement rather than the previous text:
- merged PRs since 0.1.12: 42 -> 162 (42 counted merge commits only; squash
merges leave none)
- auto-discovered agent tools in the project tree: 68 -> 94
- broker connectors: 12 -> 13, with eToro added to the table in all five READMEs
- quantlib: 249 across 17 modules -> 265 across 19, now exactly the allowlist
- SKILL.md: skills 88 -> 89, MCP tools 60 -> 70, python ">=3.11" -> ">=3.11,<3.14"
to match pyproject
- zh/ja/ko/ar gain the eToro connector section and the
CONTENT_FILTER_WARNING_THRESHOLD row that only English carried
- v0.1.13 news entry and contributor credits across all five READMEs
Full suite 10147 passed / 54 skipped / 0 failed (baseline on a clean tree was
10140 / 54 / 0; the +7 are the new guards). CI grep gates pass.
#1037 gave Eastmoney's Canada skip the status "unsupported: ...", while the
non-ASCII Yahoo skip already used "skipped: ...". The ledger recognizes a
non-failure by prefix only, so the second spelling read as a source outage:
a Canadian query returning no candidates became `invalidated` — a blocking
identity — instead of `not_found`, which is the state #1037 exists to avoid.
Measured before this change, with the real tool and the real validator:
eastmoney="unsupported: ...", yahoo=ok -> invalidated, honest answer REJECTED
eastmoney="skipped: ...", yahoo=ok -> not_found, honest answer accepted
eastmoney=ok, yahoo="HTTP 429" -> invalidated (a real outage, correct)
Both skips now share one `_SKIPPED` constant, and a cross-module test asserts
every skip status the resolver can emit is read as a non-failure by the ledger,
so the two sides cannot drift apart again silently.
Also add "C$" to the CAD aliases. A model writes a Toronto quote as "C$211.37",
not "211.37 CAD", and the currency check had the same gap that "¥" had for CNY.
Shadow-account profiles are written under the resolved runtime home, which
lands in the checkout whenever VIBE_TRADING_HOME points at the repo. A test
run produced 43 of them in one day; the sibling /shadow_runs/ entry right
above already covers the same class of artifact.
The gate could reach a state where no market tool could run and no draft could
pass, so the run spent its whole iteration budget and returned the fail-closed
"could not safely lock the instrument identity" message. Seven defects, each
reproduced against the live resolver, the live validator, and real quotes.
Identity resolution
- Fold Shanghai's `.SS` onto `.SH`. Eastmoney and Yahoo publish one listing
under both spellings, so `search_symbol("600519")` returned two candidates
for one instrument and no tie-break could choose; every Shanghai query
resolved `ambiguous` and blocked every consumer for the rest of the run.
Symbols now canonicalize once (`.SS`, `sh600519`, `700.HK`, `BTC/USDT`), and
the deleted `.SS`/`.SH` alias-conflict check is replaced by one that fires on
an actual cross-exchange swap, which it never covered.
- Skip Yahoo for a non-ASCII query instead of letting it fail. That endpoint
answers any CJK query with HTTP 400, and "not found" required two clean
sources, so a Chinese name that simply is not listed came back `invalidated`
— a blocking state — and even the honest "I did not find it" was refused.
"Not found" now needs every source that could answer to have answered.
- Let a lock outrank a stale `invalidated`/`unresolved` side query.
`identity_status` took the worst status across all records, with no recovery
path, so one flaky resolver call ended the session's ability to answer.
`conflicting` still outranks everything; per-symbol safety was never carried
by this aggregate.
Consumer arguments
- Replace the nine-tool bare-ticker whitelist with canonicalization plus a
uniqueness constraint. Eleven documented argument spellings across the
registry were rejected while being the first example in their own tool's
schema (`get_market_data(["700.HK"])`, `get_fundamentals(["AAPL"])`,
`get_margin_trading("sh600519")`, ...). A bare code still resolves only when
exactly one locked identity has it as its base.
Final-answer validation
- Accept a localized source and currency. The answer follows the user's
language, but the check demanded the ASCII loader id verbatim, so
"数据来源:腾讯财经" failed against `tencent`, and CNY accepted 人民币 but not
元 or ¥ (港元/美元 still do not satisfy CNY).
- Stop splitting a grouped number. The ASCII comma is both a clause separator
and a thousands separator and the split ran first, so "¥1,309.22" left a
clause ending in "¥1" whose 1 was compared against the observed
1300.01–1363.35 range — every price above 999 written the ordinary way.
- Check an unattributed price against the union of observed quotes instead of
refusing it. Every comparison report names its second subject in words, and
that clause was rejected although the value was the observed close.
- Require a locked identity only once the run has an identity record, and drop
`ambiguous` from the blocking set to match `_RESOLUTION_INCOMPLETE_STATUSES`.
"什么是市盈率估值法?" matched the trigger phrase and could then never produce
a passing answer, and #955 unblocked skill selection for a shortlist while
leaving the final answer blocked. Consumers stay blocked on `ambiguous`, and
an unevidenced figure is still caught by the price and unsourced-symbol
checks.
33 tests added, two-sided: correct Shanghai/Shenzhen/HK/US/crypto/A+H
resolution, conceptual questions, localized prose and grouped prices all pass,
while invented quotes, cross-exchange rewrites, another currency's 元, partial
resolver outages and shortlist-driven quote fetches stay blocked. A mutation
test asserts Shanghai regresses to `ambiguous` without canonicalization.
Remote MCP server configurations could share the same tool-spec
cache key because the key only considered stdio-oriented fields
and enabled tools. Changes to transport, URL, headers, or OAuth
configuration could therefore reuse specs discovered for a
different server configuration.
Include remote transport identity in the cache key and use
deterministic fingerprints for potentially sensitive configuration
values so credentials are not stored directly in cache keys.
Add regression coverage for URL, headers, OAuth configuration,
transport changes, deterministic ordering, and secret handling.
Signed-off-by: Lanre Shittu <136805224+Shizoqua@users.noreply.github.com>
The preset previously ended at the risk audit, leaving the user without a
single consolidated deliverable. Add a report_aggregator agent and a
task-report task that consumes the screening, factor research, backtest,
and risk audit summaries and produces one final research report covering
the final strategy and trading rules, selected factors, backtest summary,
and risk findings.
Closes#1029
Signed-off-by: Robin1987China <41602358+Robin1987China@users.noreply.github.com>
The Canadian fail-fast regex was anchored to an exact ticker
(^[A-Z0-9&.-]+\.(?:TO|V)\$) so only a bare "BTO.TO" matched. The model
searches "BTO.TO B2Gold" / "SGML.V Sigma Lithium Vancouver" (suffix + name),
which slipped past and hit Eastmoney - which has no Canada coverage and
returns a non-JSON body, logged as "Expecting value: line 1 column 1 (char 0)".
Broaden to ^[A-Z0-9&.-]+\.(?:TO|V)\b so a leading .TO/.V ticker (optionally
followed by name text) fails fast. Vienna .VI and "ASML N.V." do not match.
Also downgrade the benign "eastmoney suggest failed" log from warning to
debug: bare-name queries (no venue signal) legitimately fan out to Eastmoney,
which fails for names it has no coverage for - expected, not an error. The
status string still flows into the tool result sources map.
Adds regression tests: ticker+name .TO/.V fail-fast and bare-name fan-out.
Signed-off-by: Will Liao <liaotoca@yahoo.com>
The tool catalog was sent to the model twice on every LLM call: full
description + parameter prose in the system prompt (## Tools) AND the
structured tools array via bind_tools. The prose copy was redundant - the
model already receives the exact same name/description/parameter schema
natively - and roughly doubled per-call input tokens (~18.8K tok of the
~27.4K tok system prompt for 97 tools).
_format_tool_descriptions() now renders a compact one-line-per-tool list
(- name: <80-char hint>) instead of the full prose. Measured on real runs
(deepseek-v4-flash): system prompt ~27.4K -> ~11.3K tok, per-call input
~73-80K -> ~55-66K. No tool is removed (schemas still flow via
ToolRegistry.get_definitions() -> bind_tools) and the grounding gate (which
validates tool results, not the prompt) is untouched.
Also adds a Task Routing block so trading-plan / to-do-list / sell-orders file
tasks fetch observed prices for every cited symbol first (get_market_data or
run-dir OHLC CSVs), so the final-answer gate has this session's evidence.
Signed-off-by: Will Liao <liaotoca@yahoo.com>
pct_change() divides by the previous value. When an equity curve hits
zero (a blown-up account, a liquidation), pct_change() produces inf or
-inf. fillna(0.0) only handles NaN, not inf, so the inf propagates into
std(), mean(), and every downstream metric (Sharpe, Sortino, IR),
producing NaN results that are silently wrong.
Replace inf with 0.0 after pct_change() in calc_metrics,
bootstrap_sharpe_ci, and walk_forward_analysis. Guard the division in
_path_metrics against zero previous equity.
Devin review finding: _to_int used int(value) directly, so decimal
strings like '100.00' (valid Shoonya responses) raised ValueError and
were silently mapped to 0 instead of their real value. Use int(float(value))
to handle both integer and decimal string forms, matching the pattern
already used at sdk.py:444.
Devin review finding: the sweep dropped entries where fire_at < now,
but the throttle contract is that spacing lasts until fire_at +
min_interval. A bucket that fired 0.1s ago with a 1.5s interval was
swept, letting the next request fire immediately with no spacing —
exactly the burst behaviour that gets clients banned.
Store (fire_at, min_interval) per bucket and only sweep entries whose
full spacing window has elapsed. Add a regression test verifying that
a bucket within its spacing window survives the sweep.
Both goal/store.py and strategy_store/sqlite_store.py define _json_loads
to deserialize JSON columns (theme, benchmark, assumptions, etc.). The
original code called json.loads(value) without a try/except — a
corrupted JSON string in the database (manual edit, partial write,
encoding issue) crashes the caller with JSONDecodeError, taking down
the entire goal/strategy read path.
Wrap json.loads in try/except (JSONDecodeError, TypeError) and return
the default on failure.
clear() only removed the buffer but left subscriber queues in
self._subscribers. Subscribers would keep receiving heartbeats forever
on a cleared session, and future publish() calls would still enqueue to
those dead queues.
Send a session_cleared sentinel to each subscriber queue and remove the
subscriber list. The subscribe() generator yields the sentinel then
breaks out of its loop, so clients can detect the clear and reconnect
or stop cleanly.
Devin review findings:
- Remove unused 'defaultdict' import after switching to plain dict.
- Guard against max_requests <= 0: without the guard, a new key
bypasses the capacity check and gets 1 allowed request even when
the limit is zero or negative.
Devin review finding: closes is filtered to finite values but last_date
comes from rows[-1]. When the final bar has a NaN close (halted stock),
last_close is from an earlier bar but is labelled with the final bar's
date, producing a price/date mismatch in the prompt.
Track the last row with a finite close and use its trade_date for the
'Latest close' label.
The bash+yfinance workaround writes per-symbol OHLC CSVs into the run directory (data/raw/BYN_V.csv) instead of returning bars through get_market_data. Those prices are genuine observed tool output, but the grounding ledger only ingested JSON tool payloads, so the final-answer gate rejected every bash-derived price as numeric_claim_unavailable and withheld otherwise-correct answers.
Scan the run dir for OHLC CSVs and register their open/high/low/close/price rows as observed evidence, keyed to the symbol derived from the filename (BYN_V.csv -> BYN.V, PDI_TO.csv -> PDI.TO, GC_F.csv -> GC=F). Only files whose filename maps to a symbol already entitled in the run are accepted, rows are bounded by _MAX_GENERIC_EVIDENCE, and each file is ingested once. CSV cells are strings, so numeric cells are coerced with a dedicated helper.
Adds regression tests for CSV grounding, TSX filename mapping, fabricated-price rejection, and stray-symbol rejection.
Signed-off-by: Will Liao <liaotoca@yahoo.com>
Eastmoney has no Canada coverage and returns a non-JSON body for .TO/.V queries (Expecting value: line 1 column 1 (char 0)). Skip the endpoint for Canadian symbols instead of logging a scare error.
Yahoo returns the US OTC alias of the same company (BYN.V -> BYAGF.US, CERT.V -> CERT.US), which made the grounding ledger see one entity under two venues and reject downstream get_market_data calls with identity_conflict. Filter Canadian queries to .TO/.V candidates only.
Adds regression tests for both behaviors.
Signed-off-by: Will Liao <liaotoca@yahoo.com>
FTS5 search returned matching memories before the existing
importance-decay weighting logic could run, so decay had no
effect whenever FTS produced results. This made the combined
FTS and decay configuration rank memories purely by lexical
relevance.
Apply the existing importance multiplier to FTS relevance
scores before final ordering when decay is enabled, while
preserving the original FTS ordering when decay is disabled.
Add regression tests covering both combined and FTS-only
behaviour.
Signed-off-by: Lanre Shittu <136805224+Shizoqua@users.noreply.github.com>
agent/src/agent/loop.py's _is_tool_success was fixed for issue 886 to
treat {"ok": false, ...} and {"success": false, ...} as failures, not
just {"status": "error", ...}. get_stock_news's real failure shape
carries no status key at all.
agent/src/swarm/worker.py has its own, separate implementation of the
same check, _is_error_result, used for the multi-agent swarm execution
path. It was never updated with the same fix, so it only checked
status == "error" and the exact envelope issue 886 was filed over
still slipped through here.
A failed tool call using this shape was silently counted as a
completed data tool call and reported with status "ok" in the live
event stream. This adds the same ok/success check the single-agent
path already has, without changing behavior for anything it already
covered.
Signed-off-by: Lanre Shittu <136805224+Shizoqua@users.noreply.github.com>