13 Commits
Author SHA1 Message Date
JackzigenandJackzigen 20f2869ccc feat(loaders): free baostock fallback for val:* when tushare is unavailable (#1799)
Without a TUSHARE_TOKEN, val:* columns degraded to all-NaN panels (and
before #1788's NaN guard, the run crashed outright). #1789 follow-up #3
asks for the free path: serve pe_ttm/pb/ps_ttm from baostock
query_history_k_data_plus daily bars, which need no token at all.

- _load_baostock_valuation maps val fields to baostock peTTM/pbMRQ/psTTM
  and reuses the bounded socket guard from the OHLCV baostock loader
- load_valuation_panel now resolves tushare to None when no token is
  configured (instead of raising) and retries unserved fields against
  the baostock fallback; pe/dv_ttm stay NaN rather than being proxied
- tushare remains the primary source; baostock only serves fields
  tushare could not (missing token or empty daily_basic)
- new tests pin the ladder: fallback serving, tushare-outage fallback,
  no-token-no-baostock NaN, unserved fields not proxied, baostock
  outage NaN; the existing missing-token test pins baostock off to
  stay offline

Closes #1789 follow-up #3.

Co-authored-by: Jackzigen <zigenhan@gmail.com>
2026-10-12 19:15:39 +08:00
Jackzigen a8f94c41e9 docs: dedupe the quantlib contract table
Six fix PRs (#1713-#1718) each appended a new row to the module contract
table instead of extending the existing row, leaving four modules
duplicated: credit, timeseries, factormodel·eventstudy and
multipletesting·crossvalidation. Merged each pair into one row keeping
the extended contract wording; no module lost coverage and no wording
was changed beyond the merge.
2026-10-08 14:15:32 +08:00
Jackzigen 25118412a3 test(loaders): dedupe imports, pin contract vocabulary, cover remaining loaders
Adversarial review follow-ups on #1725:

- The bulk wiring duplicated the loader import block in 17 files
  (identical adjacent 'from backtest.loaders...' lines). Remove the
  copies; ruff F811 never flagged them because both were used.
- _CONTRACT_REASONS was defined but never consulted, so a typo'd
  reason string would pass silently. bad() now asserts membership.
- Wire the shared contract into the 5 remaining per-loader suites
  (okx, wallex, zerodha, ccxt-perpetual, longbridge) so the claim
  'every per-loader suite' holds. okx and wallex are canary symbols
  in loader_health — exactly the drift-prone loaders to pin.

Full unit suite: 1813 passed, 71 skipped (4 OAuth/MCP env failures
and 6 akshare forex failures reproduce on clean upstream/main).

Signed-off-by: Jackzigen <Jackzigen@users.noreply.github.com>
2026-10-07 14:37:58 +08:00
Jackzigen b2ca55c598 test(loaders): wire shared OHLCV contract assertion into per-loader suites
Each per-loader test file pinned its canonical fixture with bespoke
assertions; the only shared schema check lived in the weekly health
lane. A data-source drift that renames/reorders columns or lets
non-finite values through now only breaks whichever bespoke assertions
happen to cover it, and can slip past review unnoticed.

Extract the canonical-frame checks from backtest/loader_health.py into
tests/loader_contract.py (one call, same failure vocabulary) and wire
it into every per-loader suite's canonical-frame test (24 files).

This is the first step of the #1723 roadmap: with every loader suite
asserting the same shared contract, a loader that emits a
non-conforming frame fails its own suite immediately instead of
waiting for the weekly health lane.

Closes #1723 (step 1/3)

Signed-off-by: Jackzigen <Jackzigen@users.noreply.github.com>
2026-10-07 13:56:05 +08:00
JackzigenandClaude 1aaef8dd33 test(context): address review — drop dead memory_summary kwarg, pin envelope ordering
- Remove stale memory_summary= kwarg from test_system_prompt_format_succeeds
  (str.format tolerates excess kwargs, so it silently passed while masking
  a potential reintroduction of the field).
- Pin that the <agent-state> envelope stays a sibling prefix outside
  <user-message> tags when loop.py goal-context wrapping is active.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-06 19:15:39 +08:00
JackzigenandClaude df776a1b75 fix(context): keep system prompt byte-stable across runs for prefix cache
WorkspaceMemory.to_summary() renders run_dir (fresh timestamp_uuid per
run) and tool counters (bumped every tool call). Rendering it into the
system prompt (## State section) meant the prompt bytes changed on every
turn and every tool call, invalidating the provider prefix cache for
everything after that point — including the cache_control block the
Anthropic adapter marks on the system message.

Move the volatile summary into the first user message wrapped in an
<agent-state> envelope (same convention as <recalled-memories>), drop
the ## State section from the template, and pin the new property with
tests: system prompt is byte-identical when only run_dir/counters
differ, and no counter or run-dir value reaches the system prompt.

Fixes #1707

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-06 18:49:55 +08:00
Jackzigen 3788d8877e refactor(agent): extract loop tuning/config helpers into tuning module
Signed-off-by: Jackzigen <9687529+Jackzigen@users.noreply.github.com>
2026-10-02 18:56:18 +08:00
Jackzigen 8db56a2b43 docs: name mStock in broker tables, badges, and compatibility tier
The doc guards require every shipped connector to be named in all seven
README broker tables and paragraphs, the pinned broker count to match the
registry, and every portfolio-eligible connector to carry an explicit
compatibility tier.

- bump EXPECTED_BROKERS 18 -> 19 and every broker badge
- add an mStock row to each broker table (REST Type B, read-only live,
  no paper account) and name it in the no-paper paragraph
- register mstock as experimental in the portfolio compatibility map
  (Type B reply shapes stay unverified until the HTTP read mapping lands)

The remaining test_readme_counts failures are pre-existing on main (the
southbound tool batch updated counts without touching the READMEs) and
are unchanged by this patch.

Signed-off-by: Jackzigen <jackzigen@users.noreply.github.com>
2026-10-02 15:14:19 +08:00
Jackzigen ec87d3f414 feat(agent): add mStock read-only profile
Registry-only first cut for #1367: one built-in profile
mstock-live-rest-readonly (account.read + positions.read, readonly,
no order path), registry registration, _LIVE_CAPABLE tier entry on
the no-paper-environment reasoning (Robinhood/Scalable/KIS line),
README matrix row, and focused tests (14 passing).

HTTP client, auth flow, and holdings mapping are deliberately a
follow-up after profile shape confirmation.

Signed-off-by: Jackzigen <9687529+Jackzigen@users.noreply.github.com>
2026-10-02 14:47:53 +08:00
Jackzigen bc3f626235 docs(run_outcome): note the degraded-flag semantics delta from review
Review of the extraction (he-yufeng, non-blocking observation) flagged
that pre-extraction the degraded flag on the end event and result dict
keyed off self._released_fallback read after the ladder, so a
cancel/stall in the gap between fallback release and the ladder would
stamp degraded=True on a non-success terminal. Post-extraction the flag
only comes from success branches. Both release sites set final_content
first so has_output wins the ladder in every normal flow, and nothing
outside loop.py consumes the flag on failed runs — document the delta in
the module docstring rather than re-plumb, per review.

Signed-off-by: Jackzigen <9687529+Jackzigen@users.noreply.github.com>
2026-09-30 19:28:33 +08:00
Jackzigen da48546bdd refactor(agent): extract final-status resolution from _run_bound into run_outcome
Roadmap item 3 (#1624), next slice after #1636. The 107-line tail of
AgentLoop._run_bound — the terminal-status ladder (stall → cancelled →
no-progress → content-filter breaker → success → empty response →
max-iterations), the end trace event, and the result-dict assembly —
moves to a new src/agent/run_outcome.py as pure functions.

Behavior is unchanged. The side-effectful state-store transitions stay
in loop.py in their original order; every branch condition and message
string moved verbatim (the empty_model_response reason keeps its
provider/model/iteration context). loop.py re-exports the moved names.

The ladder is now directly unit-testable without stubbing a whole LLM:
test_run_outcome_module.py pins all seven branches, the lazy
pending-write-directive evaluation, and the re-export contract.

agent/tests: 16023 passed, 149 skipped (full suite, CI lanes included)

Signed-off-by: Jackzigen <9687529+Jackzigen@users.noreply.github.com>
2026-09-30 18:04:15 +08:00
Jackzigen 0871485135 refactor(agent): extract tool-result helpers from loop.py into tool_results module
Split loop.py along an existing seam (issue #1624, roadmap item 3 of 5).
The module-level helpers that judge a finished tool call move verbatim to
a new module, src/agent/tool_results.py:

- success/failure classification of a JSON result envelope
  (_is_tool_success, _failure_code)
- detection of provider tool-call DSL left in final text
  (_looks_like_tool_call_syntax + the two DSL regexes)
- the user-message target-file scan (_named_target_paths,
  _TARGET_PATH_RE, _TARGET_ACTION_RE)
- run_dir normalization (_normalize_tool_run_dir)
- the replace-not-merge archive of a successful detached backtest into
  the active run (_archive_backtest_result, _previously_archived,
  _ARCHIVE_MANIFEST re-import)

loop.py re-exports every moved name, so existing
from src.agent.loop import ... imports keep resolving to the same
objects. The moved block is byte-for-byte identical to the old inline
code (verified against upstream/main before commit), and loop.py drops
the imports the block owned.

New test module pins the re-export contract: every moved name in loop
must be the very object in tool_results.

loop.py: 3,761 -> 3,541 lines.

Signed-off-by: Jackzigen <9687529+Jackzigen@users.noreply.github.com>
2026-09-29 17:59:24 +08:00
Jackzigen ae2130831b fix(live): enforce narrowing semantics for non-numeric and list mandate adjustments
The commit-time adjustment path in _resolve_profile only guarded the
both-numeric branch, so three bypasses slipped through:

- type confusion: a rendered numeric limit (leverage 2) adjusted with a
  string ("10") skipped the widening check and committed as-is
- bool masquerade: True/False are int subclasses and passed isinstance
- list fields: instruments/allowed_instruments had no comparison branch,
  so whitelists could be widened at commit time

The ceiling re-check _profile_fits_ceilings had the same both-numeric
blind spot.

Now numeric limits reject non-numeric adjustment types outright, bool is
excluded from numeric handling, list limits must be subsets of the
rendered whitelist, other scalar types must be equivalent, and the
ceiling re-check fails closed on non-numeric pairs.

Add H19 regression tests covering string/bool/list widening and
narrowing, plus defense-in-depth ceiling checks.

Fixes part of the roadmap audit in #1207 (finding 19).
2026-08-31 11:07:58 +08:00