Base URL: http://localhost:8000/api
All responses are JSON (snake_case). Streaming endpoints use text/event-stream (SSE). All timestamps are ISO 8601.
Start a new analysis. Returns SSE stream with progress events, final result as last event.
Request:
{
"ticker": "AAPL",
"strategy_id": "strategy-uuid-or-default",
"date": "2024-01-15T00:00:00Z",
"current_position_pct": 0.0,
"mode": "standard",
"active_agents": ["market", "news", "fundamentals", "bull_researcher", "bear_researcher", "pm"],
"beliefs": ["专注于短期事件驱动机会"],
"debate_rounds": 2,
"enable_debate": true,
"enable_cross_review": false
}The Quick Ask UI always sends the selected strategy identity. It exposes an
explicit default option alongside persisted strategy IDs, and restores a
validated strategy_id from the URL or the active analysis snapshot.
SSE Events:
event: progress
data: {"agent": "company_overview", "status": "completed", "duration_ms": 1200}
event: progress
data: {"agent": "market_analyst", "status": "started"}
event: progress
data: {"agent": "market_analyst", "status": "completed", "duration_ms": 3200}
event: progress
data: {"agent": "news_analyst", "status": "tool_call", "tool": "get_news"}
event: debate
data: {"type": "investment", "round": 1, "bull_claim": "...", "bear_claim": "..."}
event: debate
data: {"type": "risk", "round": 1, "aggressive": "...", "safe": "...", "neutral": "..."}
event: result
data: { full AgentState + TradingDecision + debate_records }
Multi-ticker analysis.
Request:
{
"tickers": ["AAPL", "MSFT", "GOOG"],
"mode": "standard"
}Response: { "batch_id": "uuid", "results": [...] }
List all strategies.
Query params: ?type=agent&status=active&tag=tech&page=1&limit=20
Response:
{
"items": [ { StrategyConfig summary } ],
"total": 12,
"page": 1
}Create new strategy.
Request: StrategyConfig (see data-models.md §3)
Response: 201 + StrategyConfig
For type: "quant", create validates the canonical typed executable definition
before persistence. Supported rules are momentum/v1 and
sma_crossover/v1; required parameters, ranges, SMA window ordering, and
target_position_pct <= max_position_pct are enforced. Unknown rules return
422 strategy_not_backtestable; malformed or out-of-range parameters return
422 strategy_config_invalid. Agent and HITL creation retain their existing
non-quant semantics.
Response: StrategyConfig (full)
Update strategy config (partial update supported).
For an effective quant Strategy, the server merges the partial update with the
persisted record and validates the complete typed definition before writing.
A rejected update leaves the persisted Strategy unchanged and returns the same
stable 422 code/message contract as create.
Delete strategy and its database. Requires confirmation: ?confirm=true
Request: { "name": "My Cloned Strategy" }. name is trimmed, non-empty,
and at most 200 characters; unknown request fields return 422.
Response: 201 + a new StrategyConfig with a fresh id, name, timestamps,
status: "draft", and parent_strategy_id set to the source strategy ID. Only
configuration is copied, including custom config fields. Identity/timestamps,
inline memory/history/decisions, runtime state, database references, credentials,
and secret/API-key fields are excluded; the source strategy database is not copied.
Persist a lifecycle status change: draft|paused|stopped -> active. Starting an
already-active strategy is idempotent and does not rewrite it. Archived or other
unsupported states return 409. This endpoint does not start a scheduler.
Persist active -> paused. Pausing an already-paused strategy is idempotent and
does not rewrite it; all other states return 409. Positions are unchanged.
Request: { "liquidate": false } (false is the default). The field must be
a JSON boolean; unknown request fields return 422.
Persist draft|active|paused -> stopped. Stopping an already-stopped strategy is
idempotent and does not rewrite it; unsupported states return 409.
{ "liquidate": true } always returns 409 with
{ "detail": "Liquidation is not supported" }; this endpoint never pretends to
liquidate positions.
For clone, start, pause, and non-liquidating stop requests, an unknown strategy
ID returns 404 with { "detail": "Strategy {strategy_id} not found" }.
Because liquidate=true is always unsupported, that stop request returns its
stable 409 before strategy lookup. Transition conflicts use 409 with
{ "detail": "Cannot {action} strategy from status {status}" }.
Get agent configuration for this strategy (which agents are active, debate rounds, etc.)
Update agent configuration.
Request:
{
"active_agents": ["market", "news", "fundamentals", "bull_researcher", "bear_researcher", "pm"],
"debate_rounds": 2,
"risk_debate_rounds": 2,
"enable_cross_review": false
}Get trading beliefs for this strategy.
Add a trading belief.
Request:
{
"text": "专注于短期事件驱动机会:优先关注公司公告、并购重组等催化事件",
"weight": 1.0
}Update belief text or weight.
Remove a belief.
Response: Account
Response: { "positions": [Position], "total_value": 100500.0 }
Query params: ?period=1m&benchmark=SPY
Response: PerformanceMetrics
Query params: ?ticker=AAPL&from=2024-01-01&to=2024-06-01&page=1&limit=50
Response:
{
"trades": [Trade],
"total": 120,
"page": 1
}Query params: ?ticker=AAPL&from=2024-01-01&to=2024-06-01
Response:
{
"decisions": [
{
"session_id": "uuid",
"ticker": "AAPL",
"timestamp": "2024-01-15T10:30:00Z",
"direction": "Bullish",
"confidence": 0.75,
"action": "BUY",
"target_position_pct": 60.0,
"winning_belief": "短期事件驱动",
"cross_review_consensus": true,
"pm_report": "...",
"risk_report": "...",
"debate_records": [...]
}
]
}Get debate history for a session or date range.
Query params: ?session_id=uuid&from=2024-01-01&to=2024-06-01
Response:
{
"debates": [
{
"session_id": "uuid",
"type": "investment",
"rounds": [
{
"round_num": 1,
"bull": { "claim": "...", "evidence": [...] },
"bear": { "claim": "...", "evidence": [...], "rebuttal_to": "bull_claim_1" }
}
]
}
]
}Audit trail. Query params: ?session_id=uuid
Response:
{
"events": [SystemEvent],
"session_id": "uuid"
}Query params: ?ticker=AAPL&limit=20&min_score=0.5
Records are read from MEMORY_DB_PATH (default data/memory.db) and are scoped
to the strategy ID in the path. A storage open/read failure returns 500 with
{ "detail": "Failed to read memory" }; it is not represented as an empty list.
Response:
{
"memories": [MemoryRecord],
"total": 145
}Response: MemoryRecord (full, all 5 layers)
This endpoint uses the same configured store as the list endpoint. An unknown
memory ID, or a memory owned by a different strategy, returns 404 with
{ "detail": "Memory {memory_id} not found" }. Storage failures use the same
safe 500 response as the list endpoint.
Semantic search over memories.
Request:
{
"query": "AAPL earnings surprise",
"limit": 10,
"min_similarity": 0.6
}Weekly reflection history.
Query params: ?page=1&limit=10
Response:
{
"reflections": [Reflection],
"total": 26
}Response: Reflection (full)
Trigger a reflection generation manually.
Get the latest pre-trade safety status.
Response:
{
"drawdown_ok": true,
"streak_ok": true,
"concentration_ok": false,
"warnings": ["AAPL position exceeds 25% concentration limit"],
"can_trade": true
}Query params: ?type=rule&page=1&limit=20
Response:
{
"entries": [KnowledgeEntry],
"total": 35
}Create knowledge entry.
Request:
{
"type": "rule",
"title": "AAPL rallies after positive earnings",
"content": "...",
"confidence": 0.9,
"source_session_id": "uuid"
}Update entry.
Delete entry.
Query params: ?q=earnings+rally&type=finding
Query params: ?status=active
Response:
{
"hypotheses": [Hypothesis],
"total": 8
}Create hypothesis.
Request:
{
"claim": "RSI(14) < 30 combined with positive news sentiment produces >60% win rate",
"acceptance_criteria": "Win rate > 60% over at least 20 trades",
"budget_rounds": 20
}Response: Hypothesis (full with evidence rows)
Update hypothesis status or add evidence.
Add evidence to a hypothesis.
Request:
{
"session_id": "uuid",
"result": "supporting",
"note": "RSI(14)=28, news_sentiment=0.7, PnL=+3.2%"
}List all skills.
Query params: ?category=analysis&active=true
Response:
{
"skills": [Skill],
"total": 25
}Response: Skill (full with prompt template)
Create user skill.
Request:
{
"name": "my-custom-strategy",
"category": "strategy",
"description": "My custom trading strategy",
"tools": ["get_prices", "get_indicators"],
"prompt_template": "..."
}Update user skill (builtin skills are read-only).
Delete user skill.
Enable/disable a skill. { "is_active": false }
MCP server status.
Response:
{
"running": true,
"transport": "stdio",
"tools_exposed": 10,
"connected_clients": 2
}List all MCP tools exposed by this server.
List configured external MCP servers.
Response:
{
"servers": [
{
"name": "financial-datasets",
"command": "uvx",
"args": ["financial-datasets-mcp"],
"enabled": true,
"tools_count": 10
}
]
}Add external MCP server.
Remove external MCP server.
Test connection to external MCP server.
Request:
{
"strategy_id": "strategy-uuid",
"ticker": "AAPL",
"date_from": "2024-01-02",
"date_to": "2024-01-03",
"frequency": "daily",
"benchmark": "SPY",
"mode": "deterministic"
}Response (202):
{
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"status": "pending",
"contract_version": 1
}mode defaults to deterministic. Each request synchronously resolves the
mutable Strategy into an immutable BacktestRunSpec, persists the request and
spec atomically, and only then enqueues the job. The worker receives that spec
and never reloads Strategy state. Changing or deleting a Strategy after 202
therefore cannot alter the accepted run.
Deterministic mode accepts only active quant Strategies with a non-empty
uppercase ticker universe and a typed momentum/v1 or sma_crossover/v1
definition. frequency is a run override; the Strategy execution frequency is
preserved separately. Strategy position limits use 0..100 percent and are
converted to broker decimal fractions. Initial capital is USD. Commission
0.001 is 10 bps and slippage 0.0005 is 5 bps. Canonical v1 is long-only and
uses signal-close/next-open timing.
Agent Strategies require explicit agent_experiment mode. Experimental results
are stochastic/provider-dependent and cannot be presented as canonical
performance. Only this mode requires a non-empty model, provider credentials,
and forced structured-output capability. Missing provider capability returns
503; deterministic creation is independent of LLM credentials. HITL and
hollow quant Strategies are rejected.
Preflight errors are synchronous: an unknown Strategy is 404
strategy_not_found; inactive/ineligible/type/ticker/config/request errors are
422 (strategy_inactive, strategy_not_backtestable,
strategy_type_unsupported, ticker_not_allowed,
strategy_config_invalid, or agent_model_missing); unavailable experimental
provider credentials or forced-structured-output capability are 503
provider_capability_unsupported. Invalid dates, ticker, frequency, or unknown
request fields are Pydantic 422 responses. After this 202, data, decision,
execution, storage, and interruption failures are observed only by polling the
job; they never rewrite the accepted HTTP response.
Before the point-in-time loop begins, the worker preloads the exact target and
benchmark OHLCV request window through DataService; every decision tool then
reads only the persisted slice at or before its as_of boundary. The ACTIVE
route never invokes a legacy analysis pipeline.
The ID is exactly 32 lowercase hexadecimal characters. An invalid ID is 422;
an absent valid ID is 404 backtest_not_found. Every successful poll has these
and only these top-level fields:
id, status, request, config, progress, decisions, result,
error, created_at, and updated_at. progress always has
bars_total, bars_processed, decisions_total, decisions_eligible,
decisions_not_ready, decisions_completed, and current_decision_date.
result is non-null only for terminal success and contains exactly outcome,
warnings, no_trade_reasons, metrics, equity, orders, fills, closed_trades,
end_position, snapshot, and provenance. error is non-null only for failed jobs and
has safe code, stage, decision_date, attempt, and message fields.
Pending/running jobs and failed jobs have result: null; completed jobs have
error: null.
Each decision exposes exactly sequence, signal_date, execution_date,
status, attempts, target_position_pct, confidence, action, rationale,
feature_hash, policy_hash, error_code, and error_stage. action is the
derived transition implied by the unique target_position_pct position truth.
rationale is persisted audit evidence but is excluded from the economic hash.
Each actual broker order exposes order_id, status, signal_date,
execution_date, reason, ticker, side, quantity, order_type, and
limit_price; operational order_id is excluded from the economic hash.
For a v1 completed read, the server verifies the stored canonical result hash,
the bound frozen input snapshot, and the separately persisted decision
projection before it exposes any completed payload. Snapshot verification
includes bounded decompression, byte/hash/row metadata, normalized provenance,
and executable target/benchmark frame semantics. If any of those checks, the
canonical configuration binding, or the decision projection comparison fails,
the read is fail-closed: it projects status: "failed",
error.code: "storage_corrupt", result: null, and decisions: [] rather
than serving partly trusted performance evidence.
Terminal-success response (exact JSON shape):
{
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"status": "completed",
"request": {
"strategy_id": "strategy-uuid",
"ticker": "AAPL",
"date_from": "2024-01-02",
"date_to": "2024-01-03",
"frequency": "daily",
"benchmark": "SPY",
"mode": "deterministic"
},
"config": {
"ticker": "AAPL",
"start_date": "2024-01-02",
"end_date": "2024-01-03",
"frequency": "daily",
"benchmark_symbol": "SPY",
"strategy_id": "strategy-uuid",
"account_id": "default",
"mode": "deterministic",
"agent_model": null,
"strategy_snapshot_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"policy_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"data_snapshot_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"engine_version": "backtest-engine/v1",
"strategy_execution_frequency": "daily",
"run_frequency": "daily",
"initial_capital": 100000.0,
"commission_rate": 0.001,
"commission_bps": 10.0,
"slippage_rate": 0.0005,
"slippage_bps": 5.0,
"execution_timing": "next_open",
"max_position_pct": 1.0,
"allow_short": false,
"provider_adjustment_mode": "auto_adjusted_prices_v1",
"corporate_actions_mode": "provider_adjusted_prices",
"data_provider": "yfinance",
"data_provider_version": "",
"data_interval": "1d",
"data_auto_adjust": true,
"data_actions": false,
"data_end_exclusive": "",
"data_lookback_days": 0,
"data_provider_buffer_days": 100,
"data_provider_end_semantics": "exclusive",
"data_provider_timezone": "unknown",
"data_timezone_normalization": "exchange_session_date_to_UTC_midnight",
"warmup_bars": 0,
"risk_free_rate": 0.0,
"periods_per_year": 252,
"max_drawdown_limit_pct": 0.0,
"max_drawdown_limit_enforced": false,
"evaluation_bar_count": 0,
"sample_first_date": "",
"sample_last_date": ""
},
"progress": {
"bars_total": 2,
"bars_processed": 2,
"decisions_total": 1,
"decisions_eligible": 1,
"decisions_not_ready": 0,
"decisions_completed": 1,
"current_decision_date": "2024-01-02T00:00:00Z"
},
"decisions": [{
"sequence": 1,
"signal_date": "2024-01-02T00:00:00Z",
"execution_date": "2024-01-03T00:00:00Z",
"status": "completed",
"attempts": 1,
"target_position_pct": 80.0,
"confidence": 0.9,
"action": "BUY",
"rationale": "Momentum threshold crossed",
"feature_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"policy_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"error_code": null,
"error_stage": null
}],
"result": {
"outcome": "completed_no_trades",
"warnings": ["completed_with_no_trades_not_trusted_performance"],
"no_trade_reasons": [{"code": "all_hold", "count": 1}],
"metrics": {
"cumulative_return_pct": 0.0,
"total_return_pct": 0.0,
"annualized_return_pct": null,
"annualized_volatility_pct": null,
"benchmark_return_pct": null,
"excess_return_pct": null,
"max_drawdown_pct": 0.0,
"max_drawdown_duration": 0,
"sharpe_ratio": null,
"win_rate_pct": 0.0,
"profit_factor": null,
"avg_win": 0.0,
"avg_loss": 0.0,
"payoff_ratio": null,
"number_of_trades": 0,
"number_of_fills": 0,
"number_of_orders": 0,
"number_of_rejections": 0,
"number_of_closed_trades": 0,
"avg_holding_period_days": 0.0,
"realized_pnl_usd": 0.0,
"unrealized_pnl_usd": 0.0,
"net_pnl_usd": 0.0,
"total_fees_usd": 0.0,
"total_slippage_usd": 0.0,
"turnover_pct": 0.0,
"average_daily_gross_exposure_pct": 0.0
},
"equity": [],
"orders": [{
"order_id": "operational-order-id",
"status": "executed",
"signal_date": "2024-01-02T00:00:00Z",
"execution_date": "2024-01-03T00:00:00Z",
"reason": "",
"ticker": "AAPL",
"side": "BUY",
"quantity": 10.0,
"order_type": "MARKET",
"limit_price": null
}],
"fills": [],
"closed_trades": [],
"end_position": {
"ticker": "AAPL",
"shares": 0.0,
"market_value": 0.0,
"average_cost_basis": 0.0,
"unrealized_pnl": 0.0,
"liquidated_at_end": false
},
"snapshot": {
"compressed_bytes": 512,
"uncompressed_bytes": 1024
},
"provenance": {
"strategy_snapshot_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"policy_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"data_snapshot_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"canonical_result_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
},
"error": null,
"created_at": "2024-01-02T00:00:00Z",
"updated_at": "2024-01-03T00:00:00Z"
}Replay reads only the source job's frozen run_spec_json and bound normalized
input snapshot, then returns a new 202 acceptance object with a new id.
It does not resolve the current Strategy or fetch current provider data. A
legacy job, a missing snapshot, or corrupt frozen snapshot envelope returns
409 replay_unavailable. A normal POST /api/agent/backtest is distinct: it
always resolves the current Strategy and refetches data.
Replay validates the complete frozen envelope before it allocates the new job. It checks the bounded payload, metadata, provenance, and executable frames; there is no accepted replay that later discovers its source snapshot was unusable.
A verified v1 completed result exposes result.snapshot.compressed_bytes and
result.snapshot.uncompressed_bytes from the persisted, verified input
snapshot record. A legacy unverified completed result exposes snapshot: null.
These byte counts are storage evidence only and are excluded from the canonical
economic result hash.
For a deterministic frozen replay, result.provenance.canonical_result_hash
is the SHA-256 of a versioned canonical economic comparison payload. It binds
the frozen run-contract and engine versions; policy, strategy-snapshot, and
data-snapshot hashes; the final economic configuration; ordered decision,
order, fill, and closed-trade economics; daily equity; metrics; outcome; and
warnings. Historical signal, execution, fill, entry, and exit session dates
remain in that comparison because they are economic evidence.
The payload deliberately excludes job, account, session, order, fill, and
decision identities; strategy names and identifiers; agent-model and
strategy-execution-frequency metadata; the duplicate frequency,
commission_bps, and slippage_bps display aliases;
created/updated/provider-fetch/runtime timestamps; progress and retry metadata;
derived display counts; and the internal legacy trades alias. The economic
configuration allowlist contains the ticker and evaluation dates, run
frequency and benchmark, mode and broker economics, provider/adjustment and
corporate-action contract, risk constants, drawdown contract, and verified
sample count and boundary dates. Those operational details are expected to
differ between jobs and cannot be used to make a replay appear stable. Equal
hashes demonstrate frozen-snapshot replay fidelity only. They do not establish
prediction accuracy, out-of-sample robustness, or a trustworthy performance conclusion.
The UI therefore always labels out-of-sample robustness as not evaluated,
and completed_no_trades remains explicitly untrusted even when its replay
hash is stable.
Exports are available only for terminal completed jobs. Unknown IDs return
404; pending, running, failed, and malformed/unavailable exports return
409 export_unavailable. Every filename is derived from the validated ID and
uses Content-Disposition: attachment; filename="backtest-{id}-<kind>.<ext>".
The same completed-read verification applies to every export: a stored result
that fails integrity verification is projected as storage_corrupt and cannot
be downloaded.
GET /api/agent/backtest/{backtest_id}/trades.csvusestext/csv; charset=utf-8withdate,ticker,side,quantity,price,realized_pl,equity_after; these are fill-level executions.GET /api/agent/backtest/{backtest_id}/closed-trades.csvusestext/csv; charset=utf-8withentry_date,exit_date,ticker,quantity,entry_vwap,exit_vwap,net_realized_pl,fees,slippage,holding_period_trading_days.GET /api/agent/backtest/{backtest_id}/decisions.csvusestext/csv; charset=utf-8withsequence,signal_date,execution_date,status,target_position_pct,confidence,action,rationale,attempts,error_code.GET /api/agent/backtest/{backtest_id}/decisions.jsonusesapplication/jsonand returns a JSON array with the same decision fields in sequence order.
executions is the fill-level ledger: one row for every completed fill. Its
price is the actual execution price, fee and slippage are USD, and
avg_cost_after is the remaining long position's all-in average cost basis in
USD per share. The public job envelope names this array fills; trades is an
internal legacy compatibility alias and does not mean closed round trips.
closed_trades contains only complete long-only average-cost episodes. A
scale-in remains part of the open episode, a partial reduction records realized
P/L on its execution but does not create a closed-trade row, and only the final
positive-to-flat exit creates one row. Closed-trade entry_vwap, exit_vwap,
and average_cost_basis are USD per share; net_realized_pnl, fees, and
slippage are USD; holding_period_trading_days is a count of target trading
sessions. The compatibility field summary.number_of_trades is the same
closed-trade count.
Summary field units are fixed: number_of_fills, number_of_orders,
number_of_rejections, and number_of_closed_trades are counts;
realized_pnl_usd, unrealized_pnl_usd, net_pnl_usd, total_fees_usd, and
total_slippage_usd are USD; turnover_pct and
average_daily_gross_exposure_pct are percentages. total_return_pct uses
configured initial capital; annualized_return_pct is CAGR using the actual
evaluation calendar duration. annualized_volatility_pct and sharpe_ratio
use daily equity returns, config.periods_per_year=252, and
config.risk_free_rate (currently 0.0). max_drawdown_duration is an
integer count of consecutive underwater target trading_days, while
avg_holding_period_days is also a target trading-session count, not a
calendar-day duration. number_of_orders and number_of_rejections are
derived only from actual broker orders. Synthetic
unaffordable target intents remain visible in orders as truthful unfilled
evidence and do not increment those actual-order counts.
Slippage is an execution-price diagnostic: the fill price already embeds the
slipped execution price. The ledger includes that price once in all-in cost and
P/L, while slippage/total_slippage_usd report the diagnostic amount without
deducting it a second time. end_position.average_cost_basis is USD per share,
and end_position.unrealized_pnl is USD; an open ending position is marked to
market and is not implicitly liquidated.
Pending/running jobs have result: null. Failed jobs have result: null and
a safe { "code", "stage", "decision_date", "attempt", "message" } error.
Results live in system.db; startup marks abandoned pending/running jobs as
failed with code: "interrupted".
completed_no_trades is a terminal outcome with an explicit warning, not a
trusted performance result. no_trade_reasons records only observed causes:
no_signals, not_ready, all_hold, and all_rejected; an entry-only open
position has a real execution but no closed round trip, so it keeps the
untrusted outcome with an empty reason list rather than being mislabelled a
hold. all_rejected excludes unfilled and cancelled evidence. The independent
sample warnings insufficient_evaluation_bars_lt_63,
insufficient_evaluation_bars_lt_252, and
insufficient_closed_trades_lt_30 use strict < thresholds.
Benchmark comparison uses the target trading calendar without changing target
decisions: benchmark close values are left-joined to target dates and only
past values may forward-fill for at most five target sessions. If target and
benchmark have zero overlapping evaluation sessions, preparation fails with
typed market_data_unavailable at stage data. Partial overlap never shrinks
the target schedule. If the first
target date has no benchmark bar, benchmark_return_pct and
excess_return_pct are null with benchmark_start_unavailable; a later
stale gap produces benchmark_stale_unavailable and the same nullable summary
contract. Otherwise the benchmark starts with the same initial cash, buys
integer shares at its first target-session open after slippage and commission,
retains residual cash, and marks those shares at target-session closes. Public
payloads use JSON null for unavailable or mathematically undefined values
(including undefined Sharpe, profit factor, and payoff ratio); they never emit
Infinity or NaN.
Closed-trade statistics exclude open positions; gross and net metrics remain
distinct. The documented contract codes are strategy_not_found,
strategy_inactive, strategy_not_backtestable, strategy_type_unsupported,
ticker_not_allowed, strategy_config_invalid, agent_model_missing,
provider_capability_unsupported, insufficient_history,
decision_transient_exhausted, decision_schema_invalid, execution_failed,
storage_corrupt, interrupted, and replay_unavailable. Existing safe
worker subcategories such as market_data_unavailable, agent_failed,
decision_context_invalid, decision_policy_invalid, provider_failed, and
backtest_failed remain terminal categories where their narrower cause is
known. Messages never expose provider responses, prompts, credentials, paths,
or stacks. The frozen input snapshot includes exactly the policy-required
trading-bar warm-up plus the evaluation window for target and benchmark.
Warm-up bars can make a feature ready but never enter performance series or
metrics.
YFinance requests are fixed to interval=1d, auto_adjust=true, and
actions=false; adjusted prices therefore do not dispatch separate dividend or
split cash/quantity events. A missing interior benchmark session is aligned
past-only and cannot remove a target decision date.
List all available alpha factors.
Query params: ?zoo=gtja191&theme=momentum&limit=20
Response:
{
"zoos": ["qlib158", "alpha101", "gtja191", "academic"],
"total_factors": 452,
"factors": [
{
"id": "gtja191_171",
"zoo": "gtja191",
"name": "...",
"expression": "rank(ts_av_diff(close, 10))",
"theme": "momentum",
"source": "Guotai Junan 2014"
}
]
}Response: Factor metadata + expression + source attribution.
Benchmark factors against a universe.
Request:
{
"factor_ids": ["gtja191_171", "gtja191_111"],
"universe": "csi300",
"period_start": "2018-01-01",
"period_end": "2025-12-31",
"top_n": 20
}Response: { "job_id": "uuid" } → SSE stream for progress
Query params: ?from=2024-01-01&to=2024-01-31&type=morning_brief
Response:
{
"insights": [DailyInsight]
}Response: DailyInsight (full)
Query params: ?hours=24&generation_id={id} (0 means the provider
default window). The web client allocates and stores the 32-character lowercase
hexadecimal ID before sending the request; repeated POSTs with the same ID are
idempotent. Omitting the ID remains supported for non-browser callers.
Response: 202 with a persisted generation job. The client keeps the active
job ID in same-tab storage before the request, sends the small request with
navigation-safe keepalive, and polls it, so navigation or reload does not own or
cancel the backend work.
{
"id": "32-character lowercase hexadecimal ID",
"hours": 24,
"status": "pending",
"progress": {},
"result_insight_id": null,
"error": null,
"created_at": "2026-07-14T00:00:00+00:00",
"started_at": null,
"completed_at": null,
"updated_at": "2026-07-14T00:00:00+00:00"
}Response: the same job envelope with pending, running, completed, or
failed status. progress is keyed by stage (market, news, llm, store).
Completed jobs set result_insight_id; failed jobs expose a safe error message.
Jobs interrupted by a backend restart become terminal failed records rather
than remaining permanently running.
Request:
{
"rating": 4,
"was_direction_correct": true,
"comment": "Accurate on tech sector outlook"
}Query params: ?status=pending
Response:
{
"pending": [Approval],
"total_pending": 3
}Response: Approval with full context (all agent reports, debate history, cross-review result)
Request: { "reviewer": "username", "notes": "Looks good" }
Request: { "reviewer": "username", "notes": "Too risky, conflicting signals" }
Request:
{
"reviewer": "username",
"modified_params": {
"target_position_pct": 30.0
},
"notes": "Reduced position size due to uncertainty"
}SHARED deterministic route. The only universe is cached tracked: the
deduplicated union of strategy tickers, watchlist tickers, and cached market
tickers. It never invokes an LLM or fetches a provider.
Request:
{
"conditions": [
{ "field": "price", "operator": ">", "value": 140.0 }
],
"universe": "tracked"
}field is restricted to price, change_pct, volume, rsi14, sma20,
sma50, pe_ratio, pb_ratio, market_cap, or sector; operators are
<, >, <=, >=, ==, and between. Missing values remain missing,
never zero.
Response 201:
{
"id": "scan-run-id",
"mode": "rule",
"status": "completed",
"input": { "conditions": [{ "field": "price", "operator": ">", "value": 140.0 }], "universe": "tracked" },
"compiled_conditions": [{ "field": "price", "operator": ">", "value": 140.0, "value2": null }],
"result": {
"scanned_count": 1,
"matched_count": 1,
"missing_data_count": 0,
"warnings": [],
"results": []
}
}SHARED persisted run history for Scanner UI and Reports. Optional query:
status=running|completed|failed, limit=1..100.
SHARED read of one durable rule, agent, or belief run. Missing IDs return 404.
ACTIVE judgment route. It runs a restricted AgentLoop that can only use
scan_tracked_universe and search_skills; the model must call the Scanner
tool exactly once. Final prose is never interpreted as a result.
Agent request:
{
"mode": "agent",
"query": "find tracked shares whose price is above 140"
}Belief request:
{
"mode": "belief",
"strategy_id": "strategy-uuid",
"belief_text": "Technology momentum"
}belief_text must exactly exist in the selected strategy's beliefs; its
persisted belief_weights entry is included in the compiler prompt. This API
does not accept a belief_id. Invalid/missing sources, unavailable LLMs, no
tool call, or invalid tool output return 422; an explicit failed run is
persisted without chain-of-thought.
Request: { "name": "My AI Stocks", "tickers": ["NVDA", "AMD", "INTC"] }
Request: { "ticker": "SMCI" }
Request:
{
"ticker": "AAPL",
"type": "price_below",
"threshold_value": 200.0
}Risk is a deterministic SHARED domain. It reads the latest persisted decision target per ticker and cached market history. It does not represent executed or live holdings, invoke an LLM, normalize exposure above 100%, or fabricate equal weights.
Returns one coherent snapshot with source="decision_target", decision
IDs/timestamps, decimal weights, cash, market as_of, daily portfolio returns,
cumulative and drawdown curves, historical VaR/CVaR, correlation, and
ticker/sector concentration. lookback_days must be 60..252. Tail metrics keep
their negative decimal sign; for example -0.023 means -2.3%.
status is complete, partial for insufficient common history,
unavailable for no/non-zero targets, or invalid for an out-of-range target
or total exposure above 100%. Unavailable fields are null or empty, never
zero-valued sample data. Missing strategies return 404.
Request:
{ "uniform_market_shock": -0.1, "lookback_days": 252 }Both request and response use decimal returns. Modeled impact is
uniform_market_shock * gross_exposure, labeled with the
uniform_market_shock assumption. The actual worst day is returned from the
overview series when available. VIX/rate factor inputs are unsupported and
rejected with 422.
Creates a bounded background PDF job from one completed analysis snapshot. The
server validates that ticker, strategy_id, and optional session_id match
the persisted snapshot. Supported sections are decision, market, news,
fundamentals, and risk.
{
"ticker": "AAPL",
"strategy_id": "strategy-id",
"session_id": "completed-analysis-session-id",
"sections": ["decision", "market", "risk"]
}Creates a report from an already completed persisted Scanner run. Tickers are
derived exclusively from the run; the request never accepts a ticker list.
Supported sections are overview, constituents, decision, data_gaps, and
sources.
{
"scan_run_id": "completed-scan-run-id",
"sections": ["overview", "constituents", "data_gaps", "sources"]
}Returns { "items": ReportJob[], "total": number } in newest-first order.
Jobs expose source IDs, parameters, safe status/error metadata, and timestamps;
they never expose an artifact path or basename.
Returns one persisted job. Status is pending, running, completed, or
failed. Clients poll only unfinished jobs and show downloads only for
completed jobs.
Returns PDF binary (application/pdf) with a metadata-derived sanitized
filename. Missing jobs/artifacts return 404; unfinished or failed jobs return
409. File paths are never accepted from clients.
Response: SystemConfig (see data-models.md §19)
Partial update supported.
Send test WeChat message.
Each test route returns { "channel": string, "ok": boolean, "message": string }.
When configuration is incomplete, message lists missing field names only.
Transport failures return stable provider/configuration guidance without raw
URLs, response bodies, tokens, recipients, or provider exceptions.
GET and PUT /api/settings include notification_status for email,
telegram, wechat, and whatsapp, each with configured and
missing_fields. Secret fields remain masked.
Response:
{ "status": "ok", "uptime_seconds": 3600, "version": "1.0.0" }All errors use a consistent format:
{
"error": {
"code": "STRATEGY_NOT_FOUND",
"message": "Strategy with id 'xxx' not found",
"details": {}
}
}HTTP status codes:
400— bad request (invalid params)404— resource not found409— conflict (e.g., strategy already running)422— validation error429— rate limited500— internal error503— data source unavailable
Used by /api/analyze and /api/alpha-zoo/bench for real-time progress:
event: progress
data: {"agent": "market_analyst", "status": "started", "timestamp": "..."}
event: progress
data: {"agent": "news_analyst", "status": "tool_call", "tool": "get_news", "timestamp": "..."}
event: debate
data: {"type": "investment", "round": 1, "bull_claim": "...", "bear_claim": "...", "timestamp": "..."}
event: debate
data: {"type": "risk", "round": 1, "aggressive": "...", "safe": "...", "neutral": "...", "timestamp": "..."}
event: error
data: {"agent": "fundamentals_analyst", "error": "Data source unavailable", "timestamp": "..."}
event: result
data: {"session_id": "uuid", "action": "BUY", "debate_records": [...], ...}
Status values: started, tool_call, tool_result, completed, error
Frontend should:
- Display agent status cards in real time with parallel grouping (Stage 1: 6 analysts; Stage 2: debate; Stage 3: risk)
- Display debate events in a side-by-side panel (bull vs bear) or 3-column (risk debate)
- Handle
errorevents gracefully (show which agent failed, allow retry) - Parse the final
resultevent as the complete AgentState + TradingDecision + debate records