Skip to content

Latest commit

 

History

History
1465 lines (1127 loc) · 43.4 KB

api-contracts.md

File metadata and controls

1465 lines (1127 loc) · 43.4 KB

API Contracts

Base URL: http://localhost:8000/api

All responses are JSON (snake_case). Streaming endpoints use text/event-stream (SSE). All timestamps are ISO 8601.


1. Analysis (Quick Ask)

POST /api/analyze

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 }

POST /api/analyze/batch

Multi-ticker analysis.

Request:

{
  "tickers": ["AAPL", "MSFT", "GOOG"],
  "mode": "standard"
}

Response: { "batch_id": "uuid", "results": [...] }


2. Strategies

GET /api/strategies

List all strategies.

Query params: ?type=agent&status=active&tag=tech&page=1&limit=20

Response:

{
  "items": [ { StrategyConfig summary } ],
  "total": 12,
  "page": 1
}

POST /api/strategies

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.

GET /api/strategies/{strategy_id}

Response: StrategyConfig (full)

PUT /api/strategies/{strategy_id}

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 /api/strategies/{strategy_id}

Delete strategy and its database. Requires confirmation: ?confirm=true

POST /api/strategies/{strategy_id}/clone

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.

POST /api/strategies/{strategy_id}/start

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.

POST /api/strategies/{strategy_id}/pause

Persist active -> paused. Pausing an already-paused strategy is idempotent and does not rewrite it; all other states return 409. Positions are unchanged.

POST /api/strategies/{strategy_id}/stop

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 /api/strategies/{strategy_id}/agents

Get agent configuration for this strategy (which agents are active, debate rounds, etc.)

PUT /api/strategies/{strategy_id}/agents

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 /api/strategies/{strategy_id}/beliefs

Get trading beliefs for this strategy.

POST /api/strategies/{strategy_id}/beliefs

Add a trading belief.

Request:

{
  "text": "专注于短期事件驱动机会:优先关注公司公告、并购重组等催化事件",
  "weight": 1.0
}

PUT /api/strategies/{strategy_id}/beliefs/{belief_id}

Update belief text or weight.

DELETE /api/strategies/{strategy_id}/beliefs/{belief_id}

Remove a belief.


3. Portfolio & Performance

GET /api/strategies/{strategy_id}/account

Response: Account

GET /api/strategies/{strategy_id}/positions

Response: { "positions": [Position], "total_value": 100500.0 }

GET /api/strategies/{strategy_id}/performance

Query params: ?period=1m&benchmark=SPY

Response: PerformanceMetrics

GET /api/strategies/{strategy_id}/trades

Query params: ?ticker=AAPL&from=2024-01-01&to=2024-06-01&page=1&limit=50

Response:

{
  "trades": [Trade],
  "total": 120,
  "page": 1
}

GET /api/strategies/{strategy_id}/decisions

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 /api/strategies/{strategy_id}/debates

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" }
        }
      ]
    }
  ]
}

GET /api/strategies/{strategy_id}/events

Audit trail. Query params: ?session_id=uuid

Response:

{
  "events": [SystemEvent],
  "session_id": "uuid"
}

4. Memory & Learning

GET /api/strategies/{strategy_id}/memory

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
}

GET /api/strategies/{strategy_id}/memory/{memory_id}

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.

POST /api/strategies/{strategy_id}/memory/search

Semantic search over memories.

Request:

{
  "query": "AAPL earnings surprise",
  "limit": 10,
  "min_similarity": 0.6
}

GET /api/strategies/{strategy_id}/reflections

Weekly reflection history.

Query params: ?page=1&limit=10

Response:

{
  "reflections": [Reflection],
  "total": 26
}

GET /api/strategies/{strategy_id}/reflections/{reflection_id}

Response: Reflection (full)

POST /api/strategies/{strategy_id}/reflections/generate

Trigger a reflection generation manually.

GET /api/strategies/{strategy_id}/pre-trade-check

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
}

5. Knowledge Base

GET /api/strategies/{strategy_id}/knowledge

Query params: ?type=rule&page=1&limit=20

Response:

{
  "entries": [KnowledgeEntry],
  "total": 35
}

POST /api/strategies/{strategy_id}/knowledge

Create knowledge entry.

Request:

{
  "type": "rule",
  "title": "AAPL rallies after positive earnings",
  "content": "...",
  "confidence": 0.9,
  "source_session_id": "uuid"
}

PUT /api/strategies/{strategy_id}/knowledge/{entry_id}

Update entry.

DELETE /api/strategies/{strategy_id}/knowledge/{entry_id}

Delete entry.

GET /api/strategies/{strategy_id}/knowledge/search

Query params: ?q=earnings+rally&type=finding


6. Hypotheses

GET /api/strategies/{strategy_id}/hypotheses

Query params: ?status=active

Response:

{
  "hypotheses": [Hypothesis],
  "total": 8
}

POST /api/strategies/{strategy_id}/hypotheses

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
}

GET /api/strategies/{strategy_id}/hypotheses/{hypothesis_id}

Response: Hypothesis (full with evidence rows)

PUT /api/strategies/{strategy_id}/hypotheses/{hypothesis_id}

Update hypothesis status or add evidence.

POST /api/strategies/{strategy_id}/hypotheses/{hypothesis_id}/evidence

Add evidence to a hypothesis.

Request:

{
  "session_id": "uuid",
  "result": "supporting",
  "note": "RSI(14)=28, news_sentiment=0.7, PnL=+3.2%"
}

7. Skills

GET /api/skills

List all skills.

Query params: ?category=analysis&active=true

Response:

{
  "skills": [Skill],
  "total": 25
}

GET /api/skills/{skill_id}

Response: Skill (full with prompt template)

POST /api/skills

Create user skill.

Request:

{
  "name": "my-custom-strategy",
  "category": "strategy",
  "description": "My custom trading strategy",
  "tools": ["get_prices", "get_indicators"],
  "prompt_template": "..."
}

PUT /api/skills/{skill_id}

Update user skill (builtin skills are read-only).

DELETE /api/skills/{skill_id}

Delete user skill.

POST /api/skills/{skill_id}/toggle

Enable/disable a skill. { "is_active": false }


8. MCP Integration

GET /api/mcp/status

MCP server status.

Response:

{
  "running": true,
  "transport": "stdio",
  "tools_exposed": 10,
  "connected_clients": 2
}

GET /api/mcp/tools

List all MCP tools exposed by this server.

GET /api/mcp/servers

List configured external MCP servers.

Response:

{
  "servers": [
    {
      "name": "financial-datasets",
      "command": "uvx",
      "args": ["financial-datasets-mcp"],
      "enabled": true,
      "tools_count": 10
    }
  ]
}

POST /api/mcp/servers

Add external MCP server.

DELETE /api/mcp/servers/{server_name}

Remove external MCP server.

POST /api/mcp/servers/{server_name}/test

Test connection to external MCP server.


9. Backtest

POST /api/agent/backtest

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.

GET /api/agent/backtest/{backtest_id}

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"
}

POST /api/agent/backtest/{backtest_id}/replay

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.

Deterministic replay integrity and sample truthfulness

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.

Export endpoints

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.csv uses text/csv; charset=utf-8 with date,ticker,side,quantity,price,realized_pl,equity_after; these are fill-level executions.
  • GET /api/agent/backtest/{backtest_id}/closed-trades.csv uses text/csv; charset=utf-8 with entry_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.csv uses text/csv; charset=utf-8 with sequence,signal_date,execution_date,status,target_position_pct,confidence,action,rationale,attempts,error_code.
  • GET /api/agent/backtest/{backtest_id}/decisions.json uses application/json and returns a JSON array with the same decision fields in sequence order.

Backtest accounting payload contract

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.


10. Alpha Zoo

GET /api/alpha-zoo

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"
    }
  ]
}

GET /api/alpha-zoo/{factor_id}

Response: Factor metadata + expression + source attribution.

POST /api/alpha-zoo/bench

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


11. Daily Insights

GET /api/insights

Query params: ?from=2024-01-01&to=2024-01-31&type=morning_brief

Response:

{
  "insights": [DailyInsight]
}

GET /api/insights/{insight_id}

Response: DailyInsight (full)

POST /api/insights/generate

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"
}

GET /api/insights/generate/{generation_id}

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.

POST /api/insights/{insight_id}/feedback

Request:

{
  "rating": 4,
  "was_direction_correct": true,
  "comment": "Accurate on tech sector outlook"
}

12. Approvals (HITL)

GET /api/approvals

Query params: ?status=pending

Response:

{
  "pending": [Approval],
  "total_pending": 3
}

GET /api/approvals/{approval_id}

Response: Approval with full context (all agent reports, debate history, cross-review result)

POST /api/approvals/{approval_id}/approve

Request: { "reviewer": "username", "notes": "Looks good" }

POST /api/approvals/{approval_id}/reject

Request: { "reviewer": "username", "notes": "Too risky, conflicting signals" }

POST /api/approvals/{approval_id}/modify

Request:

{
  "reviewer": "username",
  "modified_params": {
    "target_position_pct": 30.0
  },
  "notes": "Reduced position size due to uncertainty"
}

13. Scanner

POST /api/scanner/rule

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": []
  }
}

GET /api/scanner/runs

SHARED persisted run history for Scanner UI and Reports. Optional query: status=running|completed|failed, limit=1..100.

GET /api/scanner/runs/{scan_run_id}

SHARED read of one durable rule, agent, or belief run. Missing IDs return 404.

POST /api/agent/scanner

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.


14. Watchlists

GET /api/watchlists

POST /api/watchlists

Request: { "name": "My AI Stocks", "tickers": ["NVDA", "AMD", "INTC"] }

GET /api/watchlists/{watchlist_id}

PUT /api/watchlists/{watchlist_id}

DELETE /api/watchlists/{watchlist_id}

POST /api/watchlists/{watchlist_id}/tickers

Request: { "ticker": "SMCI" }

DELETE /api/watchlists/{watchlist_id}/tickers/{ticker}

POST /api/watchlists/{watchlist_id}/alerts

Request:

{
  "ticker": "AAPL",
  "type": "price_below",
  "threshold_value": 200.0
}

15. Risk Analytics

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.

GET /api/risk/{strategy_id}/overview?lookback_days=252

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.

POST /api/risk/{strategy_id}/stress

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.


16. Reports

POST /api/reports/stock

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"]
}

POST /api/reports/sector

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"]
}

GET /api/reports

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.

GET /api/reports/{report_id}

Returns one persisted job. Status is pending, running, completed, or failed. Clients poll only unfinished jobs and show downloads only for completed jobs.

GET /api/reports/{report_id}/download

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.


17. Conversations

GET /api/conversations

GET /api/conversations/{conversation_id}

DELETE /api/conversations/{conversation_id}

POST /api/conversations/{conversation_id}/save


18. Settings

GET /api/settings

Response: SystemConfig (see data-models.md §19)

PUT /api/settings

Partial update supported.

POST /api/settings/test-email

POST /api/settings/test-telegram

POST /api/settings/test-wechat

Send test WeChat message.

POST /api/settings/test-whatsapp

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.


19. System

GET /api/health

Response:

{ "status": "ok", "uptime_seconds": 3600, "version": "1.0.0" }

GET /api/data-sources/status


Error Format

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 found
  • 409 — conflict (e.g., strategy already running)
  • 422 — validation error
  • 429 — rate limited
  • 500 — internal error
  • 503 — data source unavailable

SSE Event Format

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 error events gracefully (show which agent failed, allow retry)
  • Parse the final result event as the complete AgentState + TradingDecision + debate records