From 74c3dfbc096a13de305f28f92821d878bdbb2af1 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:15:12 +0800 Subject: [PATCH 01/16] feat: implement initial structure and interfaces for Strategy Agent components --- .../valuecell/agents/strategy_agent/README.md | 174 ++++++++++++ .../agents/strategy_agent/__init__.py | 0 .../agents/strategy_agent/__main__.py | 10 + .../valuecell/agents/strategy_agent/agent.py | 31 +++ .../agents/strategy_agent/constants.py | 0 .../valuecell/agents/strategy_agent/core.py | 51 ++++ .../agents/strategy_agent/data/__init__.py | 0 .../agents/strategy_agent/data/interfaces.py | 32 +++ .../agents/strategy_agent/data/market.py | 0 .../agents/strategy_agent/data/news.py | 0 .../strategy_agent/decision/__init__.py | 0 .../strategy_agent/decision/composer.py | 0 .../strategy_agent/decision/interfaces.py | 26 ++ .../strategy_agent/decision/system_prompt.py | 0 .../strategy_agent/decision/validator.py | 0 .../strategy_agent/execution/__init__.py | 0 .../strategy_agent/execution/exchanges.py | 0 .../strategy_agent/execution/interfaces.py | 22 ++ .../strategy_agent/execution/paper_trading.py | 0 .../strategy_agent/features/__init__.py | 0 .../strategy_agent/features/interfaces.py | 31 +++ .../features/multimodal_analysis.py | 0 .../strategy_agent/features/news_analysis.py | 0 .../features/technical_indicators.py | 0 .../valuecell/agents/strategy_agent/models.py | 255 ++++++++++++++++++ .../trading_history/__init__.py | 0 .../strategy_agent/trading_history/digest.py | 0 .../trading_history/interfaces.py | 26 ++ .../trading_history/recorder.py | 0 29 files changed, 658 insertions(+) create mode 100644 python/valuecell/agents/strategy_agent/README.md create mode 100644 python/valuecell/agents/strategy_agent/__init__.py create mode 100644 python/valuecell/agents/strategy_agent/__main__.py create mode 100644 python/valuecell/agents/strategy_agent/agent.py create mode 100644 python/valuecell/agents/strategy_agent/constants.py create mode 100644 python/valuecell/agents/strategy_agent/core.py create mode 100644 python/valuecell/agents/strategy_agent/data/__init__.py create mode 100644 python/valuecell/agents/strategy_agent/data/interfaces.py create mode 100644 python/valuecell/agents/strategy_agent/data/market.py create mode 100644 python/valuecell/agents/strategy_agent/data/news.py create mode 100644 python/valuecell/agents/strategy_agent/decision/__init__.py create mode 100644 python/valuecell/agents/strategy_agent/decision/composer.py create mode 100644 python/valuecell/agents/strategy_agent/decision/interfaces.py create mode 100644 python/valuecell/agents/strategy_agent/decision/system_prompt.py create mode 100644 python/valuecell/agents/strategy_agent/decision/validator.py create mode 100644 python/valuecell/agents/strategy_agent/execution/__init__.py create mode 100644 python/valuecell/agents/strategy_agent/execution/exchanges.py create mode 100644 python/valuecell/agents/strategy_agent/execution/interfaces.py create mode 100644 python/valuecell/agents/strategy_agent/execution/paper_trading.py create mode 100644 python/valuecell/agents/strategy_agent/features/__init__.py create mode 100644 python/valuecell/agents/strategy_agent/features/interfaces.py create mode 100644 python/valuecell/agents/strategy_agent/features/multimodal_analysis.py create mode 100644 python/valuecell/agents/strategy_agent/features/news_analysis.py create mode 100644 python/valuecell/agents/strategy_agent/features/technical_indicators.py create mode 100644 python/valuecell/agents/strategy_agent/models.py create mode 100644 python/valuecell/agents/strategy_agent/trading_history/__init__.py create mode 100644 python/valuecell/agents/strategy_agent/trading_history/digest.py create mode 100644 python/valuecell/agents/strategy_agent/trading_history/interfaces.py create mode 100644 python/valuecell/agents/strategy_agent/trading_history/recorder.py diff --git a/python/valuecell/agents/strategy_agent/README.md b/python/valuecell/agents/strategy_agent/README.md new file mode 100644 index 000000000..4d3dce875 --- /dev/null +++ b/python/valuecell/agents/strategy_agent/README.md @@ -0,0 +1,174 @@ +# Strategy Agent (Design Overview) + +This document describes the design for the Strategy Agent: a lightweight, LLM-driven trading decision pipeline with a short, testable chain from market data to executable instructions, plus history and digest for feedback. + +- Assumptions (current stage): + - Real-time data (no explicit handling of late/out-of-order data yet) + - No complex live-trading order/fill/cancel processing (kept out of scope for now) + - Decisions are generated by an LLM inside the composer; guardrails normalize the output into executable instructions. + +## Goals + +- Keep the dependency flow one-way and simple: data → features → composer(LLM + guardrails) → execution → history/digest +- Clearly defined DTOs and interfaces so each module can be developed and tested in isolation +- Minimal surface area for configuration: the strategy prompt is a plain string (prompt_text) +- Idempotent and auditable: each composition run has a compose_id; any optional + auditing metadata (prompt hash, model name, token usage, latency, filters) + is recorded as a HistoryRecord payload (no separate report object). + +## Module Layout + +- `data/` + - `market_data.py` — Market data source (candles) abstraction(s) +- `features/` + - `technical_indicators.py`, `multimodal_analysis.py`, etc. — Feature computation from raw data +- `decision/` + - `composer.py` — LLM decision + normalization + guardrails (core) + - `system_prompt.py` (optional) — prompt templates, or store in config/constants +- `execution/` + - `exchanges.py`, `paper_trading.py` — Gateways to real or paper execution (only instructions input for now) +- `trading_history/` + - `recorder.py` — Persist key checkpoints + - `digest.py` — Build `TradeDigest` for historical guidance +- Root files + - `models.py` — DTOs only (interfaces live in module-level files) + - `core.py` — DecisionCoordinator (wires the full decision cycle) + - `constants.py` — Basic configuration/limits; can hold prompt_text initially + +## Data Flow (one decision cycle) + +1. DecisionCoordinator pulls `PortfolioView` (positions, cash, optional constraints) +1. DecisionCoordinator gets recent `Candle` from `MarketDataSource` +1. `FeatureComputer` produces `FeatureVector[]` +1. DecisionCoordinator assembles `ComposeContext`: features, portfolio, digest, prompt_text (string), optional market_snapshot and extra constraints + +1. `Composer.compose(context)`: calls LLM with `ComposeContext` → `LlmPlanProposal`; normalizes plan (target position logic, limits, step size, min notional, cool-down, etc.); returns `TradeInstruction[]` + +1. `ExecutionGateway.execute(instructions)` (no detailed order/fill handling at this stage) +1. `HistoryRecorder.record(...)` checkpoints (including optional auditing metadata); + + DigestBuilder updates `TradeDigest` + +ASCII overview: + +```text +Data → Features → Composer(LLM+Guardrails) → Execution → History → Digest + ↑ ↓ ↑ + PortfolioView ----------------------------- | + prompt_text ----------------------------------------→ +``` + +## DTOs (Pydantic models) + +Defined in `models.py`: + +- Identification and raw data + - `InstrumentRef { symbol, venue?, quote_ccy? }` + - `Candle { ts, instrument, open, high, low, close, volume, interval }` +- Features and portfolio + - `FeatureVector { ts, instrument, values: Dict[str, float], meta? }` + - `PositionSnapshot { instrument, quantity, avg_price?, unrealized_pnl? }` + - `PortfolioView { ts, cash, positions: Dict[symbol, PositionSnapshot], gross_exposure?, net_exposure?, constraints? }` +- LLM decision and normalization + - `LlmDecisionItem { instrument, action: (buy|sell|flat), target_qty, confidence?, rationale? }` + - `LlmPlanProposal { ts, items: List[LlmDecisionItem], notes?, model_meta? }` + - `TradeInstruction { instruction_id, instrument, side: (buy|sell), quantity, price_mode, limit_price?, max_slippage_bps?, meta? }` + - `ComposeContext { ts, features, portfolio, digest, prompt_text, market_snapshot?, constraints? }` +- History and digest + - `HistoryRecord { ts, kind, reference_id, payload }` + - `TradeDigestEntry { instrument, trade_count, realized_pnl, win_rate?, avg_holding_ms?, last_trade_ts?, avg_entry_price?, max_drawdown?, recent_performance_score? }` + - `TradeDigest { ts, by_instrument: Dict[symbol, TradeDigestEntry] }` + +Notes: + +- Only `target_qty` is used (no `delta_qty`). Composer computes `order_qty = target_qty − current_qty` and turns it into a `TradeInstruction` (side + quantity). +- Initial versions can set `price_mode = "market"` for simplicity. + +## Abstract Interfaces (contracts) + +Interfaces live in their respective modules as ABCs (not Pydantic models): + +- `data/interfaces.py` + - `MarketDataSource.get_recent_candles(symbols, interval, lookback) -> List[Candle]` +- `features/interfaces.py` + - `FeatureComputer.compute_features(candles?: List[Candle]) -> List[FeatureVector]` +- `core.py` + - `PortfolioService.get_view() -> PortfolioView` + - `DecisionCoordinator.run_once() -> None` + - `PortfolioSnapshotStore.load_latest() -> Optional[PortfolioView]` + - `PortfolioSnapshotStore.save(view: PortfolioView) -> None` +- `decision/interfaces.py` + - `Composer.compose(context: ComposeContext) -> List[TradeInstruction]` +- `execution/interfaces.py` + - `ExecutionGateway.execute(instructions: List[TradeInstruction]) -> None` +- `trading_history/interfaces.py` + - `HistoryRecorder.record(record: HistoryRecord) -> None` + - `DigestBuilder.build(records: List[HistoryRecord]) -> TradeDigest` + +## Guardrails (composer) + +- Position targeting: compute `order_qty` from `target_qty` vs current position +- Rounding: step size, minimum order quantity/nominal +- Limits: per-instrument cap, net exposure cap, optional shorting allowance +- Cool-down/recent performance: use `TradeDigest` to suppress or downweight +- Confidence threshold and invalid field filtering +- Audit: record optional metadata (prompt hash, model name, token usage, latency, rejection reasons) as a `HistoryRecord` payload at the "compose" checkpoint +- Fallback: if LLM output is invalid/empty, optionally use a simple deterministic rule from features or return no-op + +## History and Digest (clarified) + +We record a few compact checkpoints using `HistoryRecord { ts, kind, reference_id, payload }`: + +- kind = "features": + - reference_id: compose_id + - payload: a small summary (e.g., per-symbol feature keys and last values, or a hash) +- kind = "compose": + - reference_id: compose_id + - payload: optional auditing metadata (e.g., prompt_hash, model_name, token_usage, latency_ms, reasons filtered) +- kind = "instructions": + - reference_id: compose_id + - payload: the normalized `TradeInstruction[]` as a compact list or summary (symbol, side, qty) +- kind = "execution" (optional at this stage): + - reference_id: compose_id + - payload: ack/status if available from the gateway + +DigestBuilder consumes these records (recent N bars or N decisions) to build `TradeDigest`: + +- Per-instrument aggregates in `TradeDigestEntry`: + - trade_count, realized_pnl, win_rate, avg_holding_ms, last_trade_ts, + avg_entry_price, max_drawdown, recent_performance_score +- Update cadence: periodically (e.g., every M decisions or T minutes) or incrementally per instruction/execution +- Usage in composer: cool-down (skip recent losers), down-weight bad performers, + enforce simple risk heuristics (e.g., cap net additions if recent_performance_score < threshold) + +This keeps recording simple and purpose-driven for composer feedback without inventing a separate report object. + +## Runtime Modes + +- Paper trading: default mode (via `execution/paper_trading.py`) +- Live and backtest: future extensions; the same interfaces remain stable + +## Extensibility + +- Add new features by extending `FeatureComputer` +- Plug different LLM providers/parsers within `Composer` +- Add more execution backends by implementing `ExecutionGateway` +- Evolve digests: additional stats inside `TradeDigestEntry` without breaking composer + +## Out of Scope (current stage) + +- Order lifecycle (partial fills, cancels, rejections) +- Late/out-of-order data handling +- Complex portfolio accounting beyond `PortfolioView` + +## Minimal DecisionCoordinator Contract + +A typical `run_once()` should: + +1. `view = portfolio.get_view()` +2. Pull candles via `data` and compute `features = features.compute_features(candles=...)` +3. `context = ComposeContext(ts=..., features=features, portfolio=view, digest=..., prompt_text=..., market_snapshot=..., constraints=...)` +4. `instructions = composer.compose(context)` +5. `executor.execute(instructions)` +6. Record `HistoryRecord` for features, compose auditing metadata, and instructions +7. Update `TradeDigest` periodically or incrementally diff --git a/python/valuecell/agents/strategy_agent/__init__.py b/python/valuecell/agents/strategy_agent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/__main__.py b/python/valuecell/agents/strategy_agent/__main__.py new file mode 100644 index 000000000..dc1437314 --- /dev/null +++ b/python/valuecell/agents/strategy_agent/__main__.py @@ -0,0 +1,10 @@ +import asyncio + +from valuecell.core.agent import create_wrapped_agent + +from .agent import StrategyAgent + + +if __name__ == "__main__": + agent = create_wrapped_agent(StrategyAgent) + asyncio.run(agent.serve()) diff --git a/python/valuecell/agents/strategy_agent/agent.py b/python/valuecell/agents/strategy_agent/agent.py new file mode 100644 index 000000000..37d6382ec --- /dev/null +++ b/python/valuecell/agents/strategy_agent/agent.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import AsyncGenerator, Dict, Optional + +from valuecell.core.agent.responses import streaming +from valuecell.core.types import BaseAgent, StreamResponse + + +class StrategyAgent(BaseAgent): + """Minimal StrategyAgent entry for system integration. + + This is a placeholder agent that streams a short greeting and completes. + It can be extended to wire the Strategy Agent decision loop + (data -> features -> composer -> execution -> history/digest). + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + async def stream( + self, + query: str, + conversation_id: str, + task_id: str, + dependencies: Optional[Dict] = None, + ) -> AsyncGenerator[StreamResponse, None]: + # Minimal streaming lifecycle: one message and done + yield streaming.message_chunk( + "StrategyAgent is online. Decision pipeline will be wired here." + ) + yield streaming.done() diff --git a/python/valuecell/agents/strategy_agent/constants.py b/python/valuecell/agents/strategy_agent/constants.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/core.py b/python/valuecell/agents/strategy_agent/core.py new file mode 100644 index 000000000..c62e48dda --- /dev/null +++ b/python/valuecell/agents/strategy_agent/core.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +# Core interfaces for orchestration and portfolio service. +# Plain ABCs to avoid runtime dependencies on pydantic. Concrete implementations +# wire the pipeline: data -> features -> composer -> execution -> history/digest. + +from abc import ABC, abstractmethod +from typing import Optional + +from .models import PortfolioView + + +class PortfolioService(ABC): + """Provides current portfolio state to decision modules.""" + + @abstractmethod + def get_view(self) -> PortfolioView: + """Return the latest portfolio view (positions, cash, optional constraints).""" + raise NotImplementedError + + +class DecisionCoordinator(ABC): + """Coordinates a single decision cycle end-to-end. + + A typical run performs: + 1) fetch portfolio view + 2) pull data and compute features + 3) build compose context (prompt_text, digest, constraints) + 4) compose (LLM + guardrails) -> trade instructions + 5) execute instructions + 6) record checkpoints and update digest + """ + + @abstractmethod + def run_once(self) -> None: + """Execute one decision cycle.""" + raise NotImplementedError + + +class PortfolioSnapshotStore(ABC): + """Persist/load portfolio snapshots (optional for paper/backtest modes).""" + + @abstractmethod + def load_latest(self) -> Optional[PortfolioView]: + """Load the latest persisted portfolio snapshot, if any.""" + raise NotImplementedError + + @abstractmethod + def save(self, view: PortfolioView) -> None: + """Persist the provided portfolio view as a snapshot.""" + raise NotImplementedError diff --git a/python/valuecell/agents/strategy_agent/data/__init__.py b/python/valuecell/agents/strategy_agent/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/data/interfaces.py b/python/valuecell/agents/strategy_agent/data/interfaces.py new file mode 100644 index 000000000..8af1d9ca0 --- /dev/null +++ b/python/valuecell/agents/strategy_agent/data/interfaces.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +# Contracts for market data sources (module-local abstract interfaces). +# These are plain ABCs (not Pydantic models) so implementations can be +# synchronous or asynchronous without runtime overhead. + +from abc import ABC, abstractmethod +from typing import List + +from ..models import Candle + + +class MarketDataSource(ABC): + """Abstract market data access used by feature computation. + + Implementations should fetch recent ticks or candles for the requested + symbols and intervals. Caching and batching policies are left to the + concrete classes. + """ + + @abstractmethod + def get_recent_candles( + self, symbols: List[str], interval: str, lookback: int + ) -> List[Candle]: + """Return recent candles (OHLCV) for the given symbols/interval. + + Args: + symbols: list of symbols (e.g., ["BTCUSDT", "ETHUSDT"]) + interval: candle interval string (e.g., "1m", "5m") + lookback: number of bars to retrieve + """ + raise NotImplementedError diff --git a/python/valuecell/agents/strategy_agent/data/market.py b/python/valuecell/agents/strategy_agent/data/market.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/data/news.py b/python/valuecell/agents/strategy_agent/data/news.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/decision/__init__.py b/python/valuecell/agents/strategy_agent/decision/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/decision/composer.py b/python/valuecell/agents/strategy_agent/decision/composer.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/decision/interfaces.py b/python/valuecell/agents/strategy_agent/decision/interfaces.py new file mode 100644 index 000000000..25b75349e --- /dev/null +++ b/python/valuecell/agents/strategy_agent/decision/interfaces.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +# Contracts for decision making (module-local abstract interfaces). +# Composer hosts the LLM call and guardrails, producing executable instructions. + +from abc import ABC, abstractmethod +from typing import List + +from ..models import ComposeContext, TradeInstruction + + +class Composer(ABC): + """LLM-driven decision composer with guardrails. + + Input: ComposeContext + Output: TradeInstruction list + """ + + @abstractmethod + def compose(self, context: ComposeContext) -> List[TradeInstruction]: + """Produce normalized trade instructions given the current context. + Call the LLM, parse/validate output, apply guardrails (limits, step size, + min notional, cool-down), and return executable instructions. + Any optional auditing metadata should be recorded via HistoryRecorder. + """ + raise NotImplementedError diff --git a/python/valuecell/agents/strategy_agent/decision/system_prompt.py b/python/valuecell/agents/strategy_agent/decision/system_prompt.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/decision/validator.py b/python/valuecell/agents/strategy_agent/decision/validator.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/execution/__init__.py b/python/valuecell/agents/strategy_agent/execution/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/execution/exchanges.py b/python/valuecell/agents/strategy_agent/execution/exchanges.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/execution/interfaces.py b/python/valuecell/agents/strategy_agent/execution/interfaces.py new file mode 100644 index 000000000..0402586c2 --- /dev/null +++ b/python/valuecell/agents/strategy_agent/execution/interfaces.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +# Contracts for execution gateways (module-local abstract interfaces). +# An implementation may route to a real exchange or a paper broker. + +from abc import ABC, abstractmethod +from typing import List + +from ..models import TradeInstruction + + +class ExecutionGateway(ABC): + """Executes normalized trade instructions against an exchange/broker.""" + + @abstractmethod + def execute(self, instructions: List[TradeInstruction]) -> None: + """Submit the provided instructions for execution. + Implementors may be synchronous or asynchronous. At this stage we + do not model order/fill/cancel lifecycles. + """ + + raise NotImplementedError \ No newline at end of file diff --git a/python/valuecell/agents/strategy_agent/execution/paper_trading.py b/python/valuecell/agents/strategy_agent/execution/paper_trading.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/features/__init__.py b/python/valuecell/agents/strategy_agent/features/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/features/interfaces.py b/python/valuecell/agents/strategy_agent/features/interfaces.py new file mode 100644 index 000000000..3c78bc422 --- /dev/null +++ b/python/valuecell/agents/strategy_agent/features/interfaces.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +# Contracts for feature computation (module-local abstract interfaces). +# Plain ABCs (not Pydantic) to keep implementations lightweight. + +from abc import ABC, abstractmethod +from typing import List, Optional + +from ..models import Candle, FeatureVector + + +class FeatureComputer(ABC): + """Computes feature vectors from raw market data (ticks/candles). + + Implementations may cache windows, offload CPU-heavy parts, or compose + multiple feature families. The output should be per-instrument features. + """ + + @abstractmethod + def compute_features( + self, + candles: Optional[List[Candle]] = None, + ) -> List[FeatureVector]: + """Build feature vectors from the given inputs. + + Args: + candles: optional window of candles + Returns: + A list of FeatureVector items, one or more per instrument. + """ + raise NotImplementedError diff --git a/python/valuecell/agents/strategy_agent/features/multimodal_analysis.py b/python/valuecell/agents/strategy_agent/features/multimodal_analysis.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/features/news_analysis.py b/python/valuecell/agents/strategy_agent/features/news_analysis.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/features/technical_indicators.py b/python/valuecell/agents/strategy_agent/features/technical_indicators.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py new file mode 100644 index 000000000..bd3860161 --- /dev/null +++ b/python/valuecell/agents/strategy_agent/models.py @@ -0,0 +1,255 @@ +from enum import Enum +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field, field_validator + +DEFAULT_INITIAL_CAPITAL = 100000.0 +DEFAULT_AGENT_MODEL = "deepseek-ai/DeepSeek-V3.1-Terminus" +DEFAULT_MODEL_PROVIDER = "siliconflow" +DEFAULT_MAX_POSITIONS = 3 +DEFAULT_MAX_SYMBOLS = 5 +DEFAULT_MAX_LEVERAGE = 10 +DEFAULT_RISK_PER_TRADE = 0.02 + + +class Strategy(BaseModel): + symbols: List[str] = Field( + ..., + description="List of crypto symbols to trade (e.g., ['BTC-USD', 'ETH-USD'])", + ) + max_positions: int = Field( + default=DEFAULT_MAX_POSITIONS, + description="Maximum number of concurrent positions", + gt=0, + ) + max_leverage: int = Field( + default=DEFAULT_MAX_LEVERAGE, + description="Maximum leverage", + gt=0, + ) + initial_capital: Optional[float] = Field( + default=DEFAULT_INITIAL_CAPITAL, + description="Initial capital for trading in USD", + gt=0, + ) + decide_interval: int = Field( + default=60, + description="Check interval in seconds", + gt=0, + ) + + model_provider: Optional[str] = Field( + default=DEFAULT_MODEL_PROVIDER, + description="Provider for model", + ) + model_id: Optional[str] = Field( + DEFAULT_AGENT_MODEL, + description="Model id for this strategy", + ) + exchange_id: Optional[str] = Field( + None, + description="Exchange id for this strategy", + ) + + @field_validator("symbols") + @classmethod + def validate_symbols(cls, v): + if not v or len(v) == 0: + raise ValueError("At least one symbol is required") + if len(v) > DEFAULT_MAX_SYMBOLS: + raise ValueError(f"Maximum {DEFAULT_MAX_SYMBOLS} symbols allowed") + # Normalize symbols to uppercase + return [s.upper() for s in v] + + +# ========================= +# Minimal DTOs for Strategy Agent (LLM-driven composer, no StrategyHint) +# These DTOs define the data contract across modules following the +# simplified pipeline: data -> features -> composer(LLM+rules) -> execution -> history/digest. +# ========================= + + +class InstrumentRef(BaseModel): + """Identifies a tradable instrument. + + - symbol: exchange symbol, e.g., "BTCUSDT" + - venue: optional venue/exchange id, e.g., "binance", "virtual" + - quote_ccy: optional quote currency, e.g., "USDT" + """ + + symbol: str = Field(..., description="Exchange symbol, e.g., BTCUSDT") + venue: Optional[str] = Field( + default=None, description="Venue/exchange identifier (e.g., binance)" + ) + quote_ccy: Optional[str] = Field( + default=None, description="Quote currency (e.g., USDT)" + ) + + +class Candle(BaseModel): + """Aggregated OHLCV candle for a fixed interval.""" + + ts: int = Field(..., description="Candle end timestamp in ms") + instrument: InstrumentRef + open: float + high: float + low: float + close: float + volume: float + interval: str = Field(..., description='Interval string, e.g., "1m", "5m"') + + +class FeatureVector(BaseModel): + """Computed features for a single instrument at a point in time.""" + + ts: int + instrument: InstrumentRef + values: Dict[str, float] = Field( + default_factory=dict, description="Feature name to numeric value" + ) + meta: Optional[Dict[str, float | int | str]] = Field( + default=None, description="Optional metadata (e.g., window lengths)" + ) + + +class PositionSnapshot(BaseModel): + """Current position snapshot for one instrument.""" + + instrument: InstrumentRef + quantity: float = Field(..., description="Position quantity (+long, -short)") + avg_price: Optional[float] = Field(default=None, description="Average entry price") + unrealized_pnl: Optional[float] = Field(default=None, description="Unrealized PnL") + + +class PortfolioView(BaseModel): + """Portfolio state used by the composer for decision making.""" + + ts: int + cash: float + positions: Dict[str, PositionSnapshot] = Field( + default_factory=dict, description="Map symbol -> PositionSnapshot" + ) + gross_exposure: Optional[float] = Field( + default=None, description="Absolute exposure (optional)" + ) + net_exposure: Optional[float] = Field( + default=None, description="Net exposure (optional)" + ) + constraints: Optional[Dict[str, float | int]] = Field( + default=None, + description="Optional risk/limits snapshot (e.g., max position, step size)", + ) + + +class LlmDecisionAction(str, Enum): + """Normalized high-level action from LLM plan item.""" + + BUY = "buy" + SELL = "sell" + FLAT = "flat" + + +class LlmDecisionItem(BaseModel): + """One LLM plan item. Uses target_qty only (no delta). + + The composer will compute order quantity as: target_qty - current_qty. + """ + + instrument: InstrumentRef + action: LlmDecisionAction + target_qty: float = Field( + ..., description="Desired position quantity after execution" + ) + confidence: Optional[float] = Field( + default=None, description="Optional confidence score [0,1]" + ) + rationale: Optional[str] = Field( + default=None, description="Optional natural language rationale" + ) + + +class LlmPlanProposal(BaseModel): + """Structured LLM output before rule normalization.""" + + ts: int + items: List[LlmDecisionItem] = Field(default_factory=list) + notes: Optional[List[str]] = Field(default=None) + model_meta: Optional[Dict[str, str]] = Field( + default=None, description="Optional model metadata (e.g., model_name)" + ) + + +class TradeSide(str, Enum): + """Side for executable trade instruction.""" + + BUY = "buy" + SELL = "sell" + + +class TradeInstruction(BaseModel): + """Executable instruction emitted by the composer after normalization.""" + + instruction_id: str = Field( + ..., description="Deterministic id for idempotency (e.g., compose_id+symbol)" + ) + instrument: InstrumentRef + side: TradeSide + quantity: float = Field(..., description="Order quantity in instrument units") + price_mode: str = Field( + ..., description='"market" or "limit" (initial versions may use only "market")' + ) + limit_price: Optional[float] = Field(default=None) + max_slippage_bps: Optional[float] = Field(default=None) + meta: Optional[Dict[str, str | float]] = Field( + default=None, description="Optional metadata for auditing" + ) + + +class ComposeContext(BaseModel): + """Context assembled for the LLM-driven composer.""" + + ts: int + features: List[FeatureVector] = Field( + default_factory=list, description="Feature vectors across instruments" + ) + portfolio: PortfolioView + digest: "TradeDigest" + prompt_text: str = Field(..., description="Strategy/style prompt text") + market_snapshot: Optional[Dict[str, float]] = Field( + default=None, description="Optional map symbol -> current reference price" + ) + constraints: Optional[Dict[str, float | int]] = Field( + default=None, description="Optional extra constraints for guardrails" + ) + + +class HistoryRecord(BaseModel): + """Generic persisted record for post-hoc analysis and digest building.""" + + ts: int + kind: str = Field( + ..., description='"features" | "compose" | "instructions" | "execution"' + ) + reference_id: str = Field(..., description="Correlation id (e.g., compose_id)") + payload: Dict[str, object] = Field(default_factory=dict) + + +class TradeDigestEntry(BaseModel): + """Digest stats per instrument for historical guidance in composer.""" + + instrument: InstrumentRef + trade_count: int + realized_pnl: float + win_rate: Optional[float] = Field(default=None) + avg_holding_ms: Optional[int] = Field(default=None) + last_trade_ts: Optional[int] = Field(default=None) + avg_entry_price: Optional[float] = Field(default=None) + max_drawdown: Optional[float] = Field(default=None) + recent_performance_score: Optional[float] = Field(default=None) + + +class TradeDigest(BaseModel): + """Compact digest used by the composer as historical reference.""" + + ts: int + by_instrument: Dict[str, TradeDigestEntry] = Field(default_factory=dict) diff --git a/python/valuecell/agents/strategy_agent/trading_history/__init__.py b/python/valuecell/agents/strategy_agent/trading_history/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/trading_history/digest.py b/python/valuecell/agents/strategy_agent/trading_history/digest.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/trading_history/interfaces.py b/python/valuecell/agents/strategy_agent/trading_history/interfaces.py new file mode 100644 index 000000000..8b3d226fe --- /dev/null +++ b/python/valuecell/agents/strategy_agent/trading_history/interfaces.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +# Contracts for history recording and digest building (module-local abstract interfaces). + +from abc import ABC, abstractmethod +from typing import List + +from ..models import HistoryRecord, TradeDigest + + +class HistoryRecorder(ABC): + """Persists important checkpoints for later analysis and digest building.""" + + @abstractmethod + def record(self, record: HistoryRecord) -> None: + """Persist a single history record.""" + raise NotImplementedError + + +class DigestBuilder(ABC): + """Builds TradeDigest from historical records (incremental or batch).""" + + @abstractmethod + def build(self, records: List[HistoryRecord]) -> TradeDigest: + """Construct a digest object from given history records.""" + raise NotImplementedError diff --git a/python/valuecell/agents/strategy_agent/trading_history/recorder.py b/python/valuecell/agents/strategy_agent/trading_history/recorder.py new file mode 100644 index 000000000..e69de29bb From c056b2ea77c30b3444dade5c97e3db18ab477c32 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:43:35 +0800 Subject: [PATCH 02/16] feat: refactor portfolio service interfaces and remove unused classes --- .../valuecell/agents/strategy_agent/README.md | 3 +- .../valuecell/agents/strategy_agent/core.py | 26 --------------- .../strategy_agent/portfolio/__init__.py | 0 .../strategy_agent/portfolio/interfaces.py | 32 +++++++++++++++++++ 4 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 python/valuecell/agents/strategy_agent/portfolio/__init__.py create mode 100644 python/valuecell/agents/strategy_agent/portfolio/interfaces.py diff --git a/python/valuecell/agents/strategy_agent/README.md b/python/valuecell/agents/strategy_agent/README.md index 4d3dce875..543df253f 100644 --- a/python/valuecell/agents/strategy_agent/README.md +++ b/python/valuecell/agents/strategy_agent/README.md @@ -93,8 +93,9 @@ Interfaces live in their respective modules as ABCs (not Pydantic models): - `features/interfaces.py` - `FeatureComputer.compute_features(candles?: List[Candle]) -> List[FeatureVector]` - `core.py` - - `PortfolioService.get_view() -> PortfolioView` - `DecisionCoordinator.run_once() -> None` +- `portfolio/interfaces.py` + - `PortfolioService.get_view() -> PortfolioView` - `PortfolioSnapshotStore.load_latest() -> Optional[PortfolioView]` - `PortfolioSnapshotStore.save(view: PortfolioView) -> None` - `decision/interfaces.py` diff --git a/python/valuecell/agents/strategy_agent/core.py b/python/valuecell/agents/strategy_agent/core.py index c62e48dda..c3414e8d6 100644 --- a/python/valuecell/agents/strategy_agent/core.py +++ b/python/valuecell/agents/strategy_agent/core.py @@ -5,18 +5,6 @@ # wire the pipeline: data -> features -> composer -> execution -> history/digest. from abc import ABC, abstractmethod -from typing import Optional - -from .models import PortfolioView - - -class PortfolioService(ABC): - """Provides current portfolio state to decision modules.""" - - @abstractmethod - def get_view(self) -> PortfolioView: - """Return the latest portfolio view (positions, cash, optional constraints).""" - raise NotImplementedError class DecisionCoordinator(ABC): @@ -35,17 +23,3 @@ class DecisionCoordinator(ABC): def run_once(self) -> None: """Execute one decision cycle.""" raise NotImplementedError - - -class PortfolioSnapshotStore(ABC): - """Persist/load portfolio snapshots (optional for paper/backtest modes).""" - - @abstractmethod - def load_latest(self) -> Optional[PortfolioView]: - """Load the latest persisted portfolio snapshot, if any.""" - raise NotImplementedError - - @abstractmethod - def save(self, view: PortfolioView) -> None: - """Persist the provided portfolio view as a snapshot.""" - raise NotImplementedError diff --git a/python/valuecell/agents/strategy_agent/portfolio/__init__.py b/python/valuecell/agents/strategy_agent/portfolio/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/valuecell/agents/strategy_agent/portfolio/interfaces.py b/python/valuecell/agents/strategy_agent/portfolio/interfaces.py new file mode 100644 index 000000000..a81e366fa --- /dev/null +++ b/python/valuecell/agents/strategy_agent/portfolio/interfaces.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Optional + +from ..models import PortfolioView + + +class PortfolioService(ABC): + """Provides current portfolio state to decision modules. + + Keep this as a read-only service used by DecisionCoordinator and Composer. + """ + + @abstractmethod + def get_view(self) -> PortfolioView: + """Return the latest portfolio view (positions, cash, optional constraints).""" + raise NotImplementedError + + +class PortfolioSnapshotStore(ABC): + """Persist/load portfolio snapshots (optional for paper/backtest modes).""" + + @abstractmethod + def load_latest(self) -> Optional[PortfolioView]: + """Load the latest persisted portfolio snapshot, if any.""" + raise NotImplementedError + + @abstractmethod + def save(self, view: PortfolioView) -> None: + """Persist the provided portfolio view as a snapshot.""" + raise NotImplementedError From abceb81cba16f875b5ea8b019eff1d649f1325e2 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:40:06 +0800 Subject: [PATCH 03/16] make format --- python/valuecell/agents/strategy_agent/__main__.py | 5 ++--- python/valuecell/agents/strategy_agent/core.py | 3 ++- .../valuecell/agents/strategy_agent/data/interfaces.py | 10 ++++++---- .../agents/strategy_agent/decision/interfaces.py | 8 +++++--- .../agents/strategy_agent/execution/interfaces.py | 10 ++++++---- .../agents/strategy_agent/features/interfaces.py | 8 +++++--- .../strategy_agent/trading_history/interfaces.py | 6 ++++-- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/__main__.py b/python/valuecell/agents/strategy_agent/__main__.py index dc1437314..0f6d27cf7 100644 --- a/python/valuecell/agents/strategy_agent/__main__.py +++ b/python/valuecell/agents/strategy_agent/__main__.py @@ -4,7 +4,6 @@ from .agent import StrategyAgent - if __name__ == "__main__": - agent = create_wrapped_agent(StrategyAgent) - asyncio.run(agent.serve()) + agent = create_wrapped_agent(StrategyAgent) + asyncio.run(agent.serve()) diff --git a/python/valuecell/agents/strategy_agent/core.py b/python/valuecell/agents/strategy_agent/core.py index c3414e8d6..ddf4aa7e1 100644 --- a/python/valuecell/agents/strategy_agent/core.py +++ b/python/valuecell/agents/strategy_agent/core.py @@ -1,10 +1,11 @@ from __future__ import annotations +from abc import ABC, abstractmethod + # Core interfaces for orchestration and portfolio service. # Plain ABCs to avoid runtime dependencies on pydantic. Concrete implementations # wire the pipeline: data -> features -> composer -> execution -> history/digest. -from abc import ABC, abstractmethod class DecisionCoordinator(ABC): diff --git a/python/valuecell/agents/strategy_agent/data/interfaces.py b/python/valuecell/agents/strategy_agent/data/interfaces.py index 8af1d9ca0..cd2a4da43 100644 --- a/python/valuecell/agents/strategy_agent/data/interfaces.py +++ b/python/valuecell/agents/strategy_agent/data/interfaces.py @@ -1,13 +1,15 @@ from __future__ import annotations +from abc import ABC, abstractmethod +from typing import List + +from ..models import Candle + # Contracts for market data sources (module-local abstract interfaces). # These are plain ABCs (not Pydantic models) so implementations can be # synchronous or asynchronous without runtime overhead. -from abc import ABC, abstractmethod -from typing import List -from ..models import Candle class MarketDataSource(ABC): @@ -25,7 +27,7 @@ def get_recent_candles( """Return recent candles (OHLCV) for the given symbols/interval. Args: - symbols: list of symbols (e.g., ["BTCUSDT", "ETHUSDT"]) + symbols: list of symbols (e.g., ["BTCUSDT", "ETHUSDT"]) interval: candle interval string (e.g., "1m", "5m") lookback: number of bars to retrieve """ diff --git a/python/valuecell/agents/strategy_agent/decision/interfaces.py b/python/valuecell/agents/strategy_agent/decision/interfaces.py index 25b75349e..28e26c5c7 100644 --- a/python/valuecell/agents/strategy_agent/decision/interfaces.py +++ b/python/valuecell/agents/strategy_agent/decision/interfaces.py @@ -1,13 +1,15 @@ from __future__ import annotations -# Contracts for decision making (module-local abstract interfaces). -# Composer hosts the LLM call and guardrails, producing executable instructions. - from abc import ABC, abstractmethod from typing import List from ..models import ComposeContext, TradeInstruction +# Contracts for decision making (module-local abstract interfaces). +# Composer hosts the LLM call and guardrails, producing executable instructions. + + + class Composer(ABC): """LLM-driven decision composer with guardrails. diff --git a/python/valuecell/agents/strategy_agent/execution/interfaces.py b/python/valuecell/agents/strategy_agent/execution/interfaces.py index 0402586c2..d81d27a9b 100644 --- a/python/valuecell/agents/strategy_agent/execution/interfaces.py +++ b/python/valuecell/agents/strategy_agent/execution/interfaces.py @@ -1,13 +1,15 @@ from __future__ import annotations -# Contracts for execution gateways (module-local abstract interfaces). -# An implementation may route to a real exchange or a paper broker. - from abc import ABC, abstractmethod from typing import List from ..models import TradeInstruction +# Contracts for execution gateways (module-local abstract interfaces). +# An implementation may route to a real exchange or a paper broker. + + + class ExecutionGateway(ABC): """Executes normalized trade instructions against an exchange/broker.""" @@ -19,4 +21,4 @@ def execute(self, instructions: List[TradeInstruction]) -> None: do not model order/fill/cancel lifecycles. """ - raise NotImplementedError \ No newline at end of file + raise NotImplementedError diff --git a/python/valuecell/agents/strategy_agent/features/interfaces.py b/python/valuecell/agents/strategy_agent/features/interfaces.py index 3c78bc422..0fde7de73 100644 --- a/python/valuecell/agents/strategy_agent/features/interfaces.py +++ b/python/valuecell/agents/strategy_agent/features/interfaces.py @@ -1,13 +1,15 @@ from __future__ import annotations -# Contracts for feature computation (module-local abstract interfaces). -# Plain ABCs (not Pydantic) to keep implementations lightweight. - from abc import ABC, abstractmethod from typing import List, Optional from ..models import Candle, FeatureVector +# Contracts for feature computation (module-local abstract interfaces). +# Plain ABCs (not Pydantic) to keep implementations lightweight. + + + class FeatureComputer(ABC): """Computes feature vectors from raw market data (ticks/candles). diff --git a/python/valuecell/agents/strategy_agent/trading_history/interfaces.py b/python/valuecell/agents/strategy_agent/trading_history/interfaces.py index 8b3d226fe..b591156d3 100644 --- a/python/valuecell/agents/strategy_agent/trading_history/interfaces.py +++ b/python/valuecell/agents/strategy_agent/trading_history/interfaces.py @@ -1,12 +1,14 @@ from __future__ import annotations -# Contracts for history recording and digest building (module-local abstract interfaces). - from abc import ABC, abstractmethod from typing import List from ..models import HistoryRecord, TradeDigest +# Contracts for history recording and digest building (module-local abstract interfaces). + + + class HistoryRecorder(ABC): """Persists important checkpoints for later analysis and digest building.""" From 7080c5bd85d5826cf7f0954a96d4804aaccd20c0 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:33:34 +0800 Subject: [PATCH 04/16] feat: enhance strategy agent models with additional fields and DTOs for improved functionality --- .../valuecell/agents/strategy_agent/README.md | 21 ++- .../valuecell/agents/strategy_agent/models.py | 132 +++++++++++++++++- 2 files changed, 149 insertions(+), 4 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/README.md b/python/valuecell/agents/strategy_agent/README.md index 543df253f..f05d07440 100644 --- a/python/valuecell/agents/strategy_agent/README.md +++ b/python/valuecell/agents/strategy_agent/README.md @@ -67,10 +67,10 @@ Defined in `models.py`: - `Candle { ts, instrument, open, high, low, close, volume, interval }` - Features and portfolio - `FeatureVector { ts, instrument, values: Dict[str, float], meta? }` - - `PositionSnapshot { instrument, quantity, avg_price?, unrealized_pnl? }` - - `PortfolioView { ts, cash, positions: Dict[symbol, PositionSnapshot], gross_exposure?, net_exposure?, constraints? }` + - `PositionSnapshot { instrument, quantity, avg_price?, mark_price?, unrealized_pnl?, notional?, leverage?, entry_ts?, pnl_pct?, trade_type? }` + - `PortfolioView { ts, cash, positions: Dict[symbol, PositionSnapshot], gross_exposure?, net_exposure?, constraints?, total_value?, total_unrealized_pnl?, available_cash? }` - LLM decision and normalization - - `LlmDecisionItem { instrument, action: (buy|sell|flat), target_qty, confidence?, rationale? }` + - `LlmDecisionItem { instrument, action: (buy|sell|flat|noop), target_qty, confidence?, rationale? }` - `LlmPlanProposal { ts, items: List[LlmDecisionItem], notes?, model_meta? }` - `TradeInstruction { instruction_id, instrument, side: (buy|sell), quantity, price_mode, limit_price?, max_slippage_bps?, meta? }` - `ComposeContext { ts, features, portfolio, digest, prompt_text, market_snapshot?, constraints? }` @@ -78,11 +78,26 @@ Defined in `models.py`: - `HistoryRecord { ts, kind, reference_id, payload }` - `TradeDigestEntry { instrument, trade_count, realized_pnl, win_rate?, avg_holding_ms?, last_trade_ts?, avg_entry_price?, max_drawdown?, recent_performance_score? }` - `TradeDigest { ts, by_instrument: Dict[symbol, TradeDigestEntry] }` +- UI/summary and series (optional; for leaderboard and charts) + - `TradingMode = (live|paper)` + - `StrategyStatus = (running|paused|stopped|error)` + - `StrategySummary { strategy_id?, name?, model_provider?, model_id?, exchange_id?, mode?, status?, pnl_abs?, pnl_pct?, last_updated_ts? }` + - `MetricPoint { ts, value }` + - `PortfolioValueSeries { strategy_id?, points: List[MetricPoint] }` Notes: - Only `target_qty` is used (no `delta_qty`). Composer computes `order_qty = target_qty − current_qty` and turns it into a `TradeInstruction` (side + quantity). - Initial versions can set `price_mode = "market"` for simplicity. +Action semantics: + +- `flat`: target position is zero (may emit close-out instructions) +- `noop`: target equals current (delta == 0), emit no instruction + +Additional notes: + +- `mark_price` in `PositionSnapshot` allows consistent P&L visualization without coupling to feed-specific last trade logic. +- The UI-oriented DTOs (`StrategySummary`, `PortfolioValueSeries`, etc.) are additive and do not affect the core compose/execute pipeline. ## Abstract Interfaces (contracts) diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index bd3860161..c7617d8f8 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -37,7 +37,19 @@ class Strategy(BaseModel): description="Check interval in seconds", gt=0, ) + strategy_template: Optional[str] = Field( + default=None, + description="Optional strategy template text to guide the agent", + ) + user_prompt: Optional[str] = Field( + default=None, + description="Optional user prompt to customize strategy behavior", + ) + name: Optional[str] = Field( + default=None, + description="Optional strategy name for identification", + ) model_provider: Optional[str] = Field( default=DEFAULT_MODEL_PROVIDER, description="Provider for model", @@ -112,13 +124,55 @@ class FeatureVector(BaseModel): ) +class TradeType(str, Enum): + """Semantic trade type for positions.""" + + LONG = "long" + SHORT = "short" + + +class TradingMode(str, Enum): + """Trading mode for a strategy used by UI/leaderboard tags.""" + + LIVE = "live" + PAPER = "paper" + + +class StrategyStatus(str, Enum): + """High-level runtime status for strategies (for UI health dot).""" + + RUNNING = "running" + PAUSED = "paused" + STOPPED = "stopped" + ERROR = "error" + + class PositionSnapshot(BaseModel): """Current position snapshot for one instrument.""" instrument: InstrumentRef quantity: float = Field(..., description="Position quantity (+long, -short)") avg_price: Optional[float] = Field(default=None, description="Average entry price") + mark_price: Optional[float] = Field( + default=None, description="Current mark/reference price for P&L calc" + ) unrealized_pnl: Optional[float] = Field(default=None, description="Unrealized PnL") + # Optional fields useful for UI and reporting + notional: Optional[float] = Field( + default=None, description="Position notional in quote currency" + ) + leverage: Optional[float] = Field( + default=None, description="Leverage applied to the position (if any)" + ) + entry_ts: Optional[int] = Field( + default=None, description="Entry timestamp (ms) for the current position" + ) + pnl_pct: Optional[float] = Field( + default=None, description="Unrealized P&L as a percent of position value" + ) + trade_type: Optional[TradeType] = Field( + default=None, description="Semantic trade type, e.g., 'long' or 'short'" + ) class PortfolioView(BaseModel): @@ -139,14 +193,31 @@ class PortfolioView(BaseModel): default=None, description="Optional risk/limits snapshot (e.g., max position, step size)", ) + # Optional aggregated fields convenient for UI + total_value: Optional[float] = Field( + default=None, description="Total portfolio value (cash + positions)" + ) + total_unrealized_pnl: Optional[float] = Field( + default=None, description="Sum of unrealized PnL across positions" + ) + available_cash: Optional[float] = Field( + default=None, description="Cash available for new positions" + ) class LlmDecisionAction(str, Enum): - """Normalized high-level action from LLM plan item.""" + """Normalized high-level action from LLM plan item. + + Semantics: + - BUY/SELL: directional intent; final TradeSide is decided by delta (target - current) + - FLAT: target position is zero (may produce close-out instructions) + - NOOP: target equals current (delta == 0), no instruction should be emitted + """ BUY = "buy" SELL = "sell" FLAT = "flat" + NOOP = "noop" class LlmDecisionItem(BaseModel): @@ -205,6 +276,20 @@ class TradeInstruction(BaseModel): ) +class MetricPoint(BaseModel): + """Generic time-value point, used for value history charts.""" + + ts: int + value: float + + +class PortfolioValueSeries(BaseModel): + """Series for portfolio total value over time (for performance charts).""" + + strategy_id: Optional[str] = Field(default=None) + points: List[MetricPoint] = Field(default_factory=list) + + class ComposeContext(BaseModel): """Context assembled for the LLM-driven composer.""" @@ -248,8 +333,53 @@ class TradeDigestEntry(BaseModel): recent_performance_score: Optional[float] = Field(default=None) +class TradeHistoryEntry(BaseModel): + """Executed trade record for UI history and auditing. + + This model is intended to be a compact, display-friendly representation + of a completed trade (entry + exit). Fields are optional to allow + use for partially filled / in-progress records. + """ + + trade_id: Optional[str] = Field(default=None, description="Unique trade id") + instrument: InstrumentRef + side: TradeSide + quantity: float + entry_price: Optional[float] = Field(default=None) + exit_price: Optional[float] = Field(default=None) + notional_entry: Optional[float] = Field(default=None) + notional_exit: Optional[float] = Field(default=None) + entry_ts: Optional[int] = Field(default=None, description="Entry timestamp ms") + exit_ts: Optional[int] = Field(default=None, description="Exit timestamp ms") + holding_ms: Optional[int] = Field(default=None, description="Holding time in ms") + realized_pnl: Optional[float] = Field(default=None) + realized_pnl_pct: Optional[float] = Field(default=None) + leverage: Optional[float] = Field(default=None) + notes: Optional[str] = Field(default=None) + + class TradeDigest(BaseModel): """Compact digest used by the composer as historical reference.""" ts: int by_instrument: Dict[str, TradeDigestEntry] = Field(default_factory=dict) + + +class StrategySummary(BaseModel): + """Minimal summary for leaderboard and quick status views. + + Purely for UI aggregation; does not affect the compose pipeline. + All fields are optional to avoid breaking callers and allow + progressive enhancement by the backend. + """ + + strategy_id: Optional[str] = Field(default=None) + name: Optional[str] = Field(default=None) + model_provider: Optional[str] = Field(default=None) + model_id: Optional[str] = Field(default=None) + exchange_id: Optional[str] = Field(default=None) + mode: Optional[TradingMode] = Field(default=None) + status: Optional[StrategyStatus] = Field(default=None) + pnl_abs: Optional[float] = Field(default=None, description="P&L in quote CCY") + pnl_pct: Optional[float] = Field(default=None, description="P&L as percent of equity or initial capital") + last_updated_ts: Optional[int] = Field(default=None) From 52548c89c607d22ee51b08e6127c50c8aae4b465 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:34:33 +0800 Subject: [PATCH 05/16] feat: rename venue to exchange_id in InstrumentRef and update TradingMode to use VIRTUAL --- python/valuecell/agents/strategy_agent/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index c7617d8f8..c21f7d7c0 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -85,13 +85,13 @@ class InstrumentRef(BaseModel): """Identifies a tradable instrument. - symbol: exchange symbol, e.g., "BTCUSDT" - - venue: optional venue/exchange id, e.g., "binance", "virtual" + - exchange_id: optional exchange id, e.g., "binance", "virtual" - quote_ccy: optional quote currency, e.g., "USDT" """ symbol: str = Field(..., description="Exchange symbol, e.g., BTCUSDT") - venue: Optional[str] = Field( - default=None, description="Venue/exchange identifier (e.g., binance)" + exchange_id: Optional[str] = Field( + default=None, description="exchange identifier (e.g., binance)" ) quote_ccy: Optional[str] = Field( default=None, description="Quote currency (e.g., USDT)" @@ -135,7 +135,7 @@ class TradingMode(str, Enum): """Trading mode for a strategy used by UI/leaderboard tags.""" LIVE = "live" - PAPER = "paper" + VIRTUAL = "virtual" class StrategyStatus(str, Enum): From 2f25d08ab2c30a6981f4a3bd944f9673dbe6318d Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:34:45 +0800 Subject: [PATCH 06/16] make format --- python/valuecell/agents/strategy_agent/core.py | 1 - python/valuecell/agents/strategy_agent/data/interfaces.py | 2 -- python/valuecell/agents/strategy_agent/decision/interfaces.py | 2 -- .../valuecell/agents/strategy_agent/execution/interfaces.py | 2 -- python/valuecell/agents/strategy_agent/features/interfaces.py | 2 -- python/valuecell/agents/strategy_agent/models.py | 4 +++- .../agents/strategy_agent/trading_history/interfaces.py | 2 -- 7 files changed, 3 insertions(+), 12 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/core.py b/python/valuecell/agents/strategy_agent/core.py index ddf4aa7e1..6c9969f4d 100644 --- a/python/valuecell/agents/strategy_agent/core.py +++ b/python/valuecell/agents/strategy_agent/core.py @@ -7,7 +7,6 @@ # wire the pipeline: data -> features -> composer -> execution -> history/digest. - class DecisionCoordinator(ABC): """Coordinates a single decision cycle end-to-end. diff --git a/python/valuecell/agents/strategy_agent/data/interfaces.py b/python/valuecell/agents/strategy_agent/data/interfaces.py index cd2a4da43..3899ab5ba 100644 --- a/python/valuecell/agents/strategy_agent/data/interfaces.py +++ b/python/valuecell/agents/strategy_agent/data/interfaces.py @@ -10,8 +10,6 @@ # synchronous or asynchronous without runtime overhead. - - class MarketDataSource(ABC): """Abstract market data access used by feature computation. diff --git a/python/valuecell/agents/strategy_agent/decision/interfaces.py b/python/valuecell/agents/strategy_agent/decision/interfaces.py index 28e26c5c7..41568bd05 100644 --- a/python/valuecell/agents/strategy_agent/decision/interfaces.py +++ b/python/valuecell/agents/strategy_agent/decision/interfaces.py @@ -9,8 +9,6 @@ # Composer hosts the LLM call and guardrails, producing executable instructions. - - class Composer(ABC): """LLM-driven decision composer with guardrails. diff --git a/python/valuecell/agents/strategy_agent/execution/interfaces.py b/python/valuecell/agents/strategy_agent/execution/interfaces.py index d81d27a9b..c1e745bec 100644 --- a/python/valuecell/agents/strategy_agent/execution/interfaces.py +++ b/python/valuecell/agents/strategy_agent/execution/interfaces.py @@ -9,8 +9,6 @@ # An implementation may route to a real exchange or a paper broker. - - class ExecutionGateway(ABC): """Executes normalized trade instructions against an exchange/broker.""" diff --git a/python/valuecell/agents/strategy_agent/features/interfaces.py b/python/valuecell/agents/strategy_agent/features/interfaces.py index 0fde7de73..5ef4bc4bd 100644 --- a/python/valuecell/agents/strategy_agent/features/interfaces.py +++ b/python/valuecell/agents/strategy_agent/features/interfaces.py @@ -9,8 +9,6 @@ # Plain ABCs (not Pydantic) to keep implementations lightweight. - - class FeatureComputer(ABC): """Computes feature vectors from raw market data (ticks/candles). diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index c21f7d7c0..b8be0839c 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -381,5 +381,7 @@ class StrategySummary(BaseModel): mode: Optional[TradingMode] = Field(default=None) status: Optional[StrategyStatus] = Field(default=None) pnl_abs: Optional[float] = Field(default=None, description="P&L in quote CCY") - pnl_pct: Optional[float] = Field(default=None, description="P&L as percent of equity or initial capital") + pnl_pct: Optional[float] = Field( + default=None, description="P&L as percent of equity or initial capital" + ) last_updated_ts: Optional[int] = Field(default=None) diff --git a/python/valuecell/agents/strategy_agent/trading_history/interfaces.py b/python/valuecell/agents/strategy_agent/trading_history/interfaces.py index b591156d3..831ca7385 100644 --- a/python/valuecell/agents/strategy_agent/trading_history/interfaces.py +++ b/python/valuecell/agents/strategy_agent/trading_history/interfaces.py @@ -8,8 +8,6 @@ # Contracts for history recording and digest building (module-local abstract interfaces). - - class HistoryRecorder(ABC): """Persists important checkpoints for later analysis and digest building.""" From a56ed384d6f1881b5a780bac1eebbe09e5dd878a Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:56:36 +0800 Subject: [PATCH 07/16] feat: update data models to enhance user request handling and correlation tracking --- .../valuecell/agents/strategy_agent/README.md | 22 ++++++++++++--- .../valuecell/agents/strategy_agent/models.py | 27 +++++++++++++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/README.md b/python/valuecell/agents/strategy_agent/README.md index f05d07440..021739150 100644 --- a/python/valuecell/agents/strategy_agent/README.md +++ b/python/valuecell/agents/strategy_agent/README.md @@ -63,23 +63,30 @@ Data → Features → Composer(LLM+Guardrails) → Execution → History → Dig Defined in `models.py`: - Identification and raw data - - `InstrumentRef { symbol, venue?, quote_ccy? }` + - `InstrumentRef { symbol, exchange_id?, quote_ccy? }` - `Candle { ts, instrument, open, high, low, close, volume, interval }` + +- User request / configuration + - `UserRequest { symbols, max_positions?, max_leverage?, initial_capital?, decide_interval?, strategy_template?, user_prompt?, name?, model_provider?, model_id?, exchange_id? }` + - Features and portfolio - `FeatureVector { ts, instrument, values: Dict[str, float], meta? }` - `PositionSnapshot { instrument, quantity, avg_price?, mark_price?, unrealized_pnl?, notional?, leverage?, entry_ts?, pnl_pct?, trade_type? }` - `PortfolioView { ts, cash, positions: Dict[symbol, PositionSnapshot], gross_exposure?, net_exposure?, constraints?, total_value?, total_unrealized_pnl?, available_cash? }` + - LLM decision and normalization - `LlmDecisionItem { instrument, action: (buy|sell|flat|noop), target_qty, confidence?, rationale? }` - `LlmPlanProposal { ts, items: List[LlmDecisionItem], notes?, model_meta? }` - - `TradeInstruction { instruction_id, instrument, side: (buy|sell), quantity, price_mode, limit_price?, max_slippage_bps?, meta? }` - - `ComposeContext { ts, features, portfolio, digest, prompt_text, market_snapshot?, constraints? }` + - `TradeInstruction { instruction_id, compose_id, instrument, side: (buy|sell), quantity, price_mode, limit_price?, max_slippage_bps?, meta? }` + - `ComposeContext { ts, compose_id, strategy_id?, features, portfolio, digest, prompt_text, market_snapshot?, constraints? }` + - History and digest - `HistoryRecord { ts, kind, reference_id, payload }` - `TradeDigestEntry { instrument, trade_count, realized_pnl, win_rate?, avg_holding_ms?, last_trade_ts?, avg_entry_price?, max_drawdown?, recent_performance_score? }` - `TradeDigest { ts, by_instrument: Dict[symbol, TradeDigestEntry] }` + - UI/summary and series (optional; for leaderboard and charts) - - `TradingMode = (live|paper)` + - `TradingMode = (live|virtual)` - `StrategyStatus = (running|paused|stopped|error)` - `StrategySummary { strategy_id?, name?, model_provider?, model_id?, exchange_id?, mode?, status?, pnl_abs?, pnl_pct?, last_updated_ts? }` - `MetricPoint { ts, value }` @@ -99,6 +106,13 @@ Additional notes: - `mark_price` in `PositionSnapshot` allows consistent P&L visualization without coupling to feed-specific last trade logic. - The UI-oriented DTOs (`StrategySummary`, `PortfolioValueSeries`, etc.) are additive and do not affect the core compose/execute pipeline. +## ID and correlation model + +- `strategy_id`: identity of a running strategy; used by UI aggregation (`StrategySummary`, `PortfolioValueSeries`). +- `compose_id`: unique id generated per decision cycle by the coordinator. It is carried in `ComposeContext` and copied into each `TradeInstruction` for correlation. `HistoryRecord.reference_id` uses this id. +- `instruction_id`: deterministic id for idempotency, recommended format: `${compose_id}:${instrument.symbol}` (or include an ordinal if multiple instructions per instrument). +- `trade_id`: execution-layer id for a closed trade. `TradeHistoryEntry` can store `compose_id` and `instruction_id` optionally to link back to the decision that initiated it. + ## Abstract Interfaces (contracts) Interfaces live in their respective modules as ABCs (not Pydantic models): diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index b8be0839c..5da8a2a0c 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -12,7 +12,13 @@ DEFAULT_RISK_PER_TRADE = 0.02 -class Strategy(BaseModel): +class UserRequest(BaseModel): + """User-specified strategy request / configuration. + + This model captures the inputs a user (or frontend) sends to create or + update a strategy instance. It was previously named `Strategy`. + """ + symbols: List[str] = Field( ..., description="List of crypto symbols to trade (e.g., ['BTC-USD', 'ETH-USD'])", @@ -48,7 +54,7 @@ class Strategy(BaseModel): name: Optional[str] = Field( default=None, - description="Optional strategy name for identification", + description="Optional user-friendly name for this request/strategy", ) model_provider: Optional[str] = Field( default=DEFAULT_MODEL_PROVIDER, @@ -263,6 +269,9 @@ class TradeInstruction(BaseModel): instruction_id: str = Field( ..., description="Deterministic id for idempotency (e.g., compose_id+symbol)" ) + compose_id: str = Field( + ..., description="Decision cycle id to correlate instructions and history" + ) instrument: InstrumentRef side: TradeSide quantity: float = Field(..., description="Order quantity in instrument units") @@ -294,6 +303,12 @@ class ComposeContext(BaseModel): """Context assembled for the LLM-driven composer.""" ts: int + compose_id: str = Field( + ..., description="Decision cycle id generated by coordinator per strategy" + ) + strategy_id: Optional[str] = Field( + default=None, description="Owning strategy id for logging/aggregation" + ) features: List[FeatureVector] = Field( default_factory=list, description="Feature vectors across instruments" ) @@ -342,8 +357,16 @@ class TradeHistoryEntry(BaseModel): """ trade_id: Optional[str] = Field(default=None, description="Unique trade id") + compose_id: Optional[str] = Field( + default=None, description="Originating decision cycle id (if applicable)" + ) + instruction_id: Optional[str] = Field( + default=None, description="Instruction id that initiated this trade" + ) + strategy_id: Optional[str] = Field(default=None) instrument: InstrumentRef side: TradeSide + type: TradeType quantity: float entry_price: Optional[float] = Field(default=None) exit_price: Optional[float] = Field(default=None) From 4d6da2a7d54ea81bab7d2f52aacfac8f5e1b66b7 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:20:03 +0800 Subject: [PATCH 08/16] feat: enhance UserRequest and StrategySummary models with additional fields for improved configuration and tracking --- .../valuecell/agents/strategy_agent/README.md | 5 +- .../valuecell/agents/strategy_agent/models.py | 62 +++++++++++-------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/README.md b/python/valuecell/agents/strategy_agent/README.md index 021739150..c33dc7969 100644 --- a/python/valuecell/agents/strategy_agent/README.md +++ b/python/valuecell/agents/strategy_agent/README.md @@ -67,12 +67,12 @@ Defined in `models.py`: - `Candle { ts, instrument, open, high, low, close, volume, interval }` - User request / configuration - - `UserRequest { symbols, max_positions?, max_leverage?, initial_capital?, decide_interval?, strategy_template?, user_prompt?, name?, model_provider?, model_id?, exchange_id? }` + - `UserRequest { symbols, max_positions?, max_leverage?, initial_capital?, decide_interval?, strategy_template?, user_prompt?, strategy_name?, model_provider?, model_id?, exchange_id?, trading_mode? }` - Features and portfolio - `FeatureVector { ts, instrument, values: Dict[str, float], meta? }` - `PositionSnapshot { instrument, quantity, avg_price?, mark_price?, unrealized_pnl?, notional?, leverage?, entry_ts?, pnl_pct?, trade_type? }` - - `PortfolioView { ts, cash, positions: Dict[symbol, PositionSnapshot], gross_exposure?, net_exposure?, constraints?, total_value?, total_unrealized_pnl?, available_cash? }` + - `PortfolioView { strategy_id?, ts, cash, positions: Dict[symbol, PositionSnapshot], gross_exposure?, net_exposure?, constraints?, total_value?, total_unrealized_pnl?, available_cash? }` - LLM decision and normalization - `LlmDecisionItem { instrument, action: (buy|sell|flat|noop), target_qty, confidence?, rationale? }` @@ -89,6 +89,7 @@ Defined in `models.py`: - `TradingMode = (live|virtual)` - `StrategyStatus = (running|paused|stopped|error)` - `StrategySummary { strategy_id?, name?, model_provider?, model_id?, exchange_id?, mode?, status?, pnl_abs?, pnl_pct?, last_updated_ts? }` + - `StrategySummary { strategy_id?, name?, model_provider?, model_id?, exchange_id?, mode?, status?, realized_pnl?, unrealized_pnl?, pnl_pct?, last_updated_ts? }` - `MetricPoint { ts, value }` - `PortfolioValueSeries { strategy_id?, points: List[MetricPoint] }` diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index 5da8a2a0c..12aed7ce9 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -8,10 +8,31 @@ DEFAULT_MODEL_PROVIDER = "siliconflow" DEFAULT_MAX_POSITIONS = 3 DEFAULT_MAX_SYMBOLS = 5 -DEFAULT_MAX_LEVERAGE = 10 +DEFAULT_MAX_LEVERAGE = 10.0 DEFAULT_RISK_PER_TRADE = 0.02 +class TradingMode(str, Enum): + """Trading mode for a strategy used by UI/leaderboard tags.""" + + LIVE = "live" + VIRTUAL = "virtual" + + +class TradeType(str, Enum): + """Semantic trade type for positions.""" + + LONG = "long" + SHORT = "short" + + +class TradeSide(str, Enum): + """Side for executable trade instruction.""" + + BUY = "buy" + SELL = "sell" + + class UserRequest(BaseModel): """User-specified strategy request / configuration. @@ -28,7 +49,7 @@ class UserRequest(BaseModel): description="Maximum number of concurrent positions", gt=0, ) - max_leverage: int = Field( + max_leverage: float = Field( default=DEFAULT_MAX_LEVERAGE, description="Maximum leverage", gt=0, @@ -52,7 +73,7 @@ class UserRequest(BaseModel): description="Optional user prompt to customize strategy behavior", ) - name: Optional[str] = Field( + strategy_name: Optional[str] = Field( default=None, description="Optional user-friendly name for this request/strategy", ) @@ -68,6 +89,10 @@ class UserRequest(BaseModel): None, description="Exchange id for this strategy", ) + trading_mode: Optional[TradingMode] = Field( + default=TradingMode.VIRTUAL, + description="Trading mode for this strategy", + ) @field_validator("symbols") @classmethod @@ -130,20 +155,6 @@ class FeatureVector(BaseModel): ) -class TradeType(str, Enum): - """Semantic trade type for positions.""" - - LONG = "long" - SHORT = "short" - - -class TradingMode(str, Enum): - """Trading mode for a strategy used by UI/leaderboard tags.""" - - LIVE = "live" - VIRTUAL = "virtual" - - class StrategyStatus(str, Enum): """High-level runtime status for strategies (for UI health dot).""" @@ -184,6 +195,9 @@ class PositionSnapshot(BaseModel): class PortfolioView(BaseModel): """Portfolio state used by the composer for decision making.""" + strategy_id: Optional[str] = Field( + default=None, description="Owning strategy id for this portfolio snapshot" + ) ts: int cash: float positions: Dict[str, PositionSnapshot] = Field( @@ -256,13 +270,6 @@ class LlmPlanProposal(BaseModel): ) -class TradeSide(str, Enum): - """Side for executable trade instruction.""" - - BUY = "buy" - SELL = "sell" - - class TradeInstruction(BaseModel): """Executable instruction emitted by the composer after normalization.""" @@ -403,7 +410,12 @@ class StrategySummary(BaseModel): exchange_id: Optional[str] = Field(default=None) mode: Optional[TradingMode] = Field(default=None) status: Optional[StrategyStatus] = Field(default=None) - pnl_abs: Optional[float] = Field(default=None, description="P&L in quote CCY") + realized_pnl: Optional[float] = Field( + default=None, description="Realized P&L in quote CCY" + ) + unrealized_pnl: Optional[float] = Field( + default=None, description="Unrealized P&L in quote CCY" + ) pnl_pct: Optional[float] = Field( default=None, description="P&L as percent of equity or initial capital" ) From c8b8819e26d87928a759a70aab494682d4cdbd9b Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:22:18 +0800 Subject: [PATCH 09/16] feat: centralize default constants for strategy agent in a separate module --- .../valuecell/agents/strategy_agent/constants.py | 11 +++++++++++ python/valuecell/agents/strategy_agent/models.py | 15 ++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/constants.py b/python/valuecell/agents/strategy_agent/constants.py index e69de29bb..d5135e033 100644 --- a/python/valuecell/agents/strategy_agent/constants.py +++ b/python/valuecell/agents/strategy_agent/constants.py @@ -0,0 +1,11 @@ +"""Default constants used across the strategy_agent package. + +Centralizes defaults so they can be imported from one place. +""" + +DEFAULT_INITIAL_CAPITAL = 100000.0 +DEFAULT_AGENT_MODEL = "deepseek-ai/DeepSeek-V3.1-Terminus" +DEFAULT_MODEL_PROVIDER = "siliconflow" +DEFAULT_MAX_POSITIONS = 3 +DEFAULT_MAX_SYMBOLS = 5 +DEFAULT_MAX_LEVERAGE = 10.0 diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index 12aed7ce9..05b5aa58c 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -3,13 +3,14 @@ from pydantic import BaseModel, Field, field_validator -DEFAULT_INITIAL_CAPITAL = 100000.0 -DEFAULT_AGENT_MODEL = "deepseek-ai/DeepSeek-V3.1-Terminus" -DEFAULT_MODEL_PROVIDER = "siliconflow" -DEFAULT_MAX_POSITIONS = 3 -DEFAULT_MAX_SYMBOLS = 5 -DEFAULT_MAX_LEVERAGE = 10.0 -DEFAULT_RISK_PER_TRADE = 0.02 +from .constants import ( + DEFAULT_INITIAL_CAPITAL, + DEFAULT_AGENT_MODEL, + DEFAULT_MODEL_PROVIDER, + DEFAULT_MAX_POSITIONS, + DEFAULT_MAX_SYMBOLS, + DEFAULT_MAX_LEVERAGE, +) class TradingMode(str, Enum): From 258442484b5ca9777d45be87a5033658f9446f45 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:42:39 +0800 Subject: [PATCH 10/16] make format --- python/valuecell/agents/strategy_agent/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index 05b5aa58c..7788b2145 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -4,12 +4,12 @@ from pydantic import BaseModel, Field, field_validator from .constants import ( - DEFAULT_INITIAL_CAPITAL, DEFAULT_AGENT_MODEL, - DEFAULT_MODEL_PROVIDER, + DEFAULT_INITIAL_CAPITAL, + DEFAULT_MAX_LEVERAGE, DEFAULT_MAX_POSITIONS, DEFAULT_MAX_SYMBOLS, - DEFAULT_MAX_LEVERAGE, + DEFAULT_MODEL_PROVIDER, ) From 9ad0038395a9ab882c97306e5e64e82f25020b6b Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:58:06 +0800 Subject: [PATCH 11/16] fix: standardize trade type and trade side enum values to uppercase --- python/valuecell/agents/strategy_agent/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index 7788b2145..fadede36d 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -23,15 +23,15 @@ class TradingMode(str, Enum): class TradeType(str, Enum): """Semantic trade type for positions.""" - LONG = "long" - SHORT = "short" + LONG = "LONG" + SHORT = "SHORT" class TradeSide(str, Enum): """Side for executable trade instruction.""" - BUY = "buy" - SELL = "sell" + BUY = "BUY" + SELL = "SELL" class UserRequest(BaseModel): From ae1302aa777eff8990158595156cd1decdb8f626 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:05:50 +0800 Subject: [PATCH 12/16] feat: enhance TradeHistoryEntry model with additional fields for trade timestamp and notes --- python/valuecell/agents/strategy_agent/README.md | 2 ++ python/valuecell/agents/strategy_agent/models.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/python/valuecell/agents/strategy_agent/README.md b/python/valuecell/agents/strategy_agent/README.md index c33dc7969..d17ea47c4 100644 --- a/python/valuecell/agents/strategy_agent/README.md +++ b/python/valuecell/agents/strategy_agent/README.md @@ -93,6 +93,8 @@ Defined in `models.py`: - `MetricPoint { ts, value }` - `PortfolioValueSeries { strategy_id?, points: List[MetricPoint] }` +`TradeHistoryEntry { trade_id?, compose_id?, instruction_id?, strategy_id?, trade_ts?, entry_ts?, exit_ts?, instrument, side, type, quantity, entry_price?, exit_price?, realized_pnl?, realized_pnl_pct?, holding_ms?, leverage?, note? }` + Notes: - Only `target_qty` is used (no `delta_qty`). Composer computes `order_qty = target_qty − current_qty` and turns it into a `TradeInstruction` (side + quantity). diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index fadede36d..053d1b40c 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -382,11 +382,12 @@ class TradeHistoryEntry(BaseModel): notional_exit: Optional[float] = Field(default=None) entry_ts: Optional[int] = Field(default=None, description="Entry timestamp ms") exit_ts: Optional[int] = Field(default=None, description="Exit timestamp ms") + trade_ts: Optional[int] = Field(default=None, description="Trade timestamp in ms") holding_ms: Optional[int] = Field(default=None, description="Holding time in ms") realized_pnl: Optional[float] = Field(default=None) realized_pnl_pct: Optional[float] = Field(default=None) leverage: Optional[float] = Field(default=None) - notes: Optional[str] = Field(default=None) + note: Optional[str] = Field(default=None, description="Optional free-form note or comment about the trade") class TradeDigest(BaseModel): From 298d022960d97a4a6e188af7978eadad81eaa78e Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:25:32 +0800 Subject: [PATCH 13/16] make format --- python/valuecell/agents/strategy_agent/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index 053d1b40c..fd09bc049 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -387,7 +387,9 @@ class TradeHistoryEntry(BaseModel): realized_pnl: Optional[float] = Field(default=None) realized_pnl_pct: Optional[float] = Field(default=None) leverage: Optional[float] = Field(default=None) - note: Optional[str] = Field(default=None, description="Optional free-form note or comment about the trade") + note: Optional[str] = Field( + default=None, description="Optional free-form note or comment about the trade" + ) class TradeDigest(BaseModel): From 55d51b91a43df3e6d6f455defe5cd29c404ba869 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:18:26 +0800 Subject: [PATCH 14/16] feat: restructure UserRequest model to include separate configurations for model, exchange, and trading --- .../valuecell/agents/strategy_agent/README.md | 5 +- .../valuecell/agents/strategy_agent/models.py | 116 ++++++++++++------ 2 files changed, 81 insertions(+), 40 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/README.md b/python/valuecell/agents/strategy_agent/README.md index d17ea47c4..7b2c8c58d 100644 --- a/python/valuecell/agents/strategy_agent/README.md +++ b/python/valuecell/agents/strategy_agent/README.md @@ -67,7 +67,10 @@ Defined in `models.py`: - `Candle { ts, instrument, open, high, low, close, volume, interval }` - User request / configuration - - `UserRequest { symbols, max_positions?, max_leverage?, initial_capital?, decide_interval?, strategy_template?, user_prompt?, strategy_name?, model_provider?, model_id?, exchange_id?, trading_mode? }` + - `UserRequest { model_config: ModelConfig, exchange_config: ExchangeConfig, trading_config: TradingConfig }` + - `ModelConfig { provider, model_id, api_key }` + - `ExchangeConfig { exchange_id?, trading_mode, api_key?, secret_key? }` + - `TradingConfig { strategy_name?, initial_capital?, max_leverage?, max_positions?, symbols, decide_interval?, template_id?, custom_prompt? }` - Features and portfolio - `FeatureVector { ts, instrument, values: Dict[str, float], meta? }` diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index fd09bc049..f4276fbd7 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -34,20 +34,54 @@ class TradeSide(str, Enum): SELL = "SELL" -class UserRequest(BaseModel): - """User-specified strategy request / configuration. +class ModelConfig(BaseModel): + """AI model configuration for strategy.""" - This model captures the inputs a user (or frontend) sends to create or - update a strategy instance. It was previously named `Strategy`. - """ - - symbols: List[str] = Field( + provider: str = Field( + default=DEFAULT_MODEL_PROVIDER, + description="Model provider (e.g., 'openrouter', 'google', 'openai')" + ) + model_id: str = Field( + default=DEFAULT_AGENT_MODEL, + description="Model identifier (e.g., 'deepseek-ai/deepseek-v3.1', 'gpt-4o')" + ) + api_key: str = Field( ..., - description="List of crypto symbols to trade (e.g., ['BTC-USD', 'ETH-USD'])", + description="API key for the model provider" ) - max_positions: int = Field( - default=DEFAULT_MAX_POSITIONS, - description="Maximum number of concurrent positions", + + +class ExchangeConfig(BaseModel): + """Exchange configuration for trading.""" + + exchange_id: Optional[str] = Field( + default=None, + description="Exchange identifier (e.g., 'okx', 'binance')" + ) + trading_mode: TradingMode = Field( + default=TradingMode.VIRTUAL, + description="Trading mode for this strategy" + ) + api_key: Optional[str] = Field( + default=None, + description="Exchange API key (required for live trading)" + ) + secret_key: Optional[str] = Field( + default=None, + description="Exchange secret key (required for live trading)" + ) + + +class TradingConfig(BaseModel): + """Trading strategy configuration.""" + + strategy_name: Optional[str] = Field( + default=None, + description="User-friendly name for this strategy" + ) + initial_capital: Optional[float] = Field( + default=DEFAULT_INITIAL_CAPITAL, + description="Initial capital for trading in USD", gt=0, ) max_leverage: float = Field( @@ -55,44 +89,27 @@ class UserRequest(BaseModel): description="Maximum leverage", gt=0, ) - initial_capital: Optional[float] = Field( - default=DEFAULT_INITIAL_CAPITAL, - description="Initial capital for trading in USD", + max_positions: int = Field( + default=DEFAULT_MAX_POSITIONS, + description="Maximum number of concurrent positions", gt=0, ) + symbols: List[str] = Field( + ..., + description="List of crypto symbols to trade (e.g., ['BTC-USD', 'ETH-USD'])", + ) decide_interval: int = Field( default=60, description="Check interval in seconds", gt=0, ) - strategy_template: Optional[str] = Field( - default=None, - description="Optional strategy template text to guide the agent", - ) - user_prompt: Optional[str] = Field( + template_id: Optional[str] = Field( default=None, - description="Optional user prompt to customize strategy behavior", + description="Strategy template identifier to guide the agent" ) - - strategy_name: Optional[str] = Field( + custom_prompt: Optional[str] = Field( default=None, - description="Optional user-friendly name for this request/strategy", - ) - model_provider: Optional[str] = Field( - default=DEFAULT_MODEL_PROVIDER, - description="Provider for model", - ) - model_id: Optional[str] = Field( - DEFAULT_AGENT_MODEL, - description="Model id for this strategy", - ) - exchange_id: Optional[str] = Field( - None, - description="Exchange id for this strategy", - ) - trading_mode: Optional[TradingMode] = Field( - default=TradingMode.VIRTUAL, - description="Trading mode for this strategy", + description="Optional custom prompt to customize strategy behavior" ) @field_validator("symbols") @@ -106,6 +123,27 @@ def validate_symbols(cls, v): return [s.upper() for s in v] +class UserRequest(BaseModel): + """User-specified strategy request / configuration. + + This model captures the inputs a user (or frontend) sends to create or + update a strategy instance. It was previously named `Strategy`. + """ + + model_config: ModelConfig = Field( + default_factory=ModelConfig, + description="AI model configuration" + ) + exchange_config: ExchangeConfig = Field( + default_factory=ExchangeConfig, + description="Exchange configuration for trading" + ) + trading_config: TradingConfig = Field( + ..., + description="Trading strategy configuration" + ) + + # ========================= # Minimal DTOs for Strategy Agent (LLM-driven composer, no StrategyHint) # These DTOs define the data contract across modules following the From 006423fa3ec5da556aa6a9ef430e095b5abed4ed Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:18:33 +0800 Subject: [PATCH 15/16] fix: update DEFAULT_MAX_POSITIONS to 5 for consistency with trading strategy --- python/valuecell/agents/strategy_agent/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/valuecell/agents/strategy_agent/constants.py b/python/valuecell/agents/strategy_agent/constants.py index d5135e033..5b9efdbb8 100644 --- a/python/valuecell/agents/strategy_agent/constants.py +++ b/python/valuecell/agents/strategy_agent/constants.py @@ -6,6 +6,6 @@ DEFAULT_INITIAL_CAPITAL = 100000.0 DEFAULT_AGENT_MODEL = "deepseek-ai/DeepSeek-V3.1-Terminus" DEFAULT_MODEL_PROVIDER = "siliconflow" -DEFAULT_MAX_POSITIONS = 3 +DEFAULT_MAX_POSITIONS = 5 DEFAULT_MAX_SYMBOLS = 5 DEFAULT_MAX_LEVERAGE = 10.0 From aea27bb7ad22e90e5c96cdeeae4004688bb15187 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:18:48 +0800 Subject: [PATCH 16/16] make format --- .../valuecell/agents/strategy_agent/models.py | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index f4276fbd7..42276f9bf 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -39,36 +39,29 @@ class ModelConfig(BaseModel): provider: str = Field( default=DEFAULT_MODEL_PROVIDER, - description="Model provider (e.g., 'openrouter', 'google', 'openai')" + description="Model provider (e.g., 'openrouter', 'google', 'openai')", ) model_id: str = Field( default=DEFAULT_AGENT_MODEL, - description="Model identifier (e.g., 'deepseek-ai/deepseek-v3.1', 'gpt-4o')" - ) - api_key: str = Field( - ..., - description="API key for the model provider" + description="Model identifier (e.g., 'deepseek-ai/deepseek-v3.1', 'gpt-4o')", ) + api_key: str = Field(..., description="API key for the model provider") class ExchangeConfig(BaseModel): """Exchange configuration for trading.""" exchange_id: Optional[str] = Field( - default=None, - description="Exchange identifier (e.g., 'okx', 'binance')" + default=None, description="Exchange identifier (e.g., 'okx', 'binance')" ) trading_mode: TradingMode = Field( - default=TradingMode.VIRTUAL, - description="Trading mode for this strategy" + default=TradingMode.VIRTUAL, description="Trading mode for this strategy" ) api_key: Optional[str] = Field( - default=None, - description="Exchange API key (required for live trading)" + default=None, description="Exchange API key (required for live trading)" ) secret_key: Optional[str] = Field( - default=None, - description="Exchange secret key (required for live trading)" + default=None, description="Exchange secret key (required for live trading)" ) @@ -76,8 +69,7 @@ class TradingConfig(BaseModel): """Trading strategy configuration.""" strategy_name: Optional[str] = Field( - default=None, - description="User-friendly name for this strategy" + default=None, description="User-friendly name for this strategy" ) initial_capital: Optional[float] = Field( default=DEFAULT_INITIAL_CAPITAL, @@ -104,12 +96,11 @@ class TradingConfig(BaseModel): gt=0, ) template_id: Optional[str] = Field( - default=None, - description="Strategy template identifier to guide the agent" + default=None, description="Strategy template identifier to guide the agent" ) custom_prompt: Optional[str] = Field( default=None, - description="Optional custom prompt to customize strategy behavior" + description="Optional custom prompt to customize strategy behavior", ) @field_validator("symbols") @@ -131,16 +122,13 @@ class UserRequest(BaseModel): """ model_config: ModelConfig = Field( - default_factory=ModelConfig, - description="AI model configuration" + default_factory=ModelConfig, description="AI model configuration" ) exchange_config: ExchangeConfig = Field( - default_factory=ExchangeConfig, - description="Exchange configuration for trading" + default_factory=ExchangeConfig, description="Exchange configuration for trading" ) trading_config: TradingConfig = Field( - ..., - description="Trading strategy configuration" + ..., description="Trading strategy configuration" )