Adds MultiStrategyManager: it runs several strategies concurrently, each with its own TradingEngine seeded with starting_balance * allocation, so one strategy's losses cannot eat into another's capital. Weights are normalised across the run ([1,3] -> 25%/75%). Risk limits stay per strategy (each engine owns a RiskManager). New API: POST /api/bot/start-many and GET /api/bot/status. The existing single-strategy endpoints delegate to the manager, so their behaviour is unchanged. Adds manager tests (allocation, isolation, duplicate keys) and API tests.
286 lines
6.3 KiB
Python
286 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class Candle(BaseModel):
|
|
ts: int
|
|
open: float
|
|
high: float
|
|
low: float
|
|
close: float
|
|
volume: float
|
|
|
|
|
|
class ParamSpec(BaseModel):
|
|
name: str
|
|
type: Literal["int", "float", "bool", "str"]
|
|
default: Any
|
|
description: str = ""
|
|
min: float | None = None
|
|
max: float | None = None
|
|
|
|
|
|
class StrategyOut(BaseModel):
|
|
key: str
|
|
name: str
|
|
description: str
|
|
params: list[ParamSpec]
|
|
|
|
|
|
class PositionOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
strategy_key: str
|
|
symbol: str
|
|
side: str
|
|
qty: float
|
|
entry_price: float
|
|
status: str
|
|
stop_loss: float | None = None
|
|
take_profit: float | None = None
|
|
exit_price: float | None = None
|
|
realized_pnl: float | None = None
|
|
unrealized_pnl: float | None = None
|
|
opened_at: datetime
|
|
closed_at: datetime | None = None
|
|
|
|
|
|
class OrderOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
position_id: int | None
|
|
strategy_key: str
|
|
symbol: str
|
|
side: str
|
|
qty: float
|
|
price: float
|
|
fee: float
|
|
reason: str
|
|
created_at: datetime
|
|
|
|
|
|
class EventOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
level: str
|
|
message: str
|
|
strategy_key: str | None
|
|
created_at: datetime
|
|
|
|
|
|
class EquityPointOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
equity: float
|
|
cash: float
|
|
price: float
|
|
created_at: datetime
|
|
|
|
|
|
class StatusOut(BaseModel):
|
|
running: bool
|
|
mode: str
|
|
exchange: str
|
|
symbol: str
|
|
timeframe: str
|
|
strategy_key: str | None
|
|
strategy_name: str | None
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|
price: float | None
|
|
cash: float
|
|
equity: float
|
|
starting_balance: float
|
|
realized_pnl: float
|
|
unrealized_pnl: float
|
|
total_pnl: float
|
|
open_positions: int
|
|
last_tick: datetime | None
|
|
error: str | None = None
|
|
halted: bool = False
|
|
halt_reason: str | None = None
|
|
trades_today: int = 0
|
|
day_pnl: float = 0.0
|
|
drawdown_pct: float = 0.0
|
|
|
|
|
|
class KillRequest(BaseModel):
|
|
reason: str = "manual kill switch"
|
|
|
|
|
|
class StrategyConfigIn(BaseModel):
|
|
name: str = Field(default="", max_length=80)
|
|
strategy_key: str
|
|
symbol: str
|
|
timeframe: str
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class StrategyConfigOut(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
name: str
|
|
strategy_key: str
|
|
symbol: str
|
|
timeframe: str
|
|
params: dict[str, Any]
|
|
created_at: datetime
|
|
|
|
|
|
class StrategySpec(BaseModel):
|
|
"""One strategy in a multi-strategy run."""
|
|
|
|
strategy_key: str
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|
symbol: str | None = None
|
|
timeframe: str | None = None
|
|
# Fraction of the account allocated to this strategy (relative weights are
|
|
# normalised across the run, so [1, 3] gives 25% / 75%).
|
|
weight: float = Field(default=1.0, gt=0)
|
|
|
|
|
|
class StartRequest(BaseModel):
|
|
strategy_key: str
|
|
symbol: str | None = None
|
|
timeframe: str | None = None
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|
starting_balance: float | None = None
|
|
|
|
|
|
class StartManyRequest(BaseModel):
|
|
strategies: list[StrategySpec] = Field(min_length=1)
|
|
starting_balance: float | None = None
|
|
|
|
|
|
class StrategyStatusOut(BaseModel):
|
|
"""Per-strategy status inside a multi-strategy run."""
|
|
|
|
strategy_key: str
|
|
strategy_name: str | None
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|
symbol: str
|
|
timeframe: str
|
|
allocation: float
|
|
cash: float
|
|
equity: float
|
|
realized_pnl: float
|
|
unrealized_pnl: float
|
|
total_pnl: float
|
|
open_positions: int
|
|
halted: bool = False
|
|
halt_reason: str | None = None
|
|
trades_today: int = 0
|
|
error: str | None = None
|
|
|
|
|
|
class MultiStatusOut(BaseModel):
|
|
running: bool
|
|
mode: str
|
|
exchange: str
|
|
price: float | None = None
|
|
starting_balance: float
|
|
equity: float
|
|
total_pnl: float
|
|
strategies: list[StrategyStatusOut] = Field(default_factory=list)
|
|
last_tick: datetime | None = None
|
|
|
|
|
|
class ManualOrderRequest(BaseModel):
|
|
side: Literal["buy", "sell"]
|
|
qty: float | None = None
|
|
quote_amount: float | None = None
|
|
reason: str = "manual"
|
|
|
|
|
|
class ClosePositionRequest(BaseModel):
|
|
reason: str = "manual"
|
|
|
|
|
|
class BacktestRequest(BaseModel):
|
|
strategy_key: str
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|
symbol: str | None = None
|
|
timeframe: str | None = None
|
|
limit: int = Field(default=500, ge=20, le=1000)
|
|
starting_balance: float | None = None
|
|
|
|
|
|
class BacktestTradeOut(BaseModel):
|
|
side: str
|
|
entry_ts: int
|
|
exit_ts: int
|
|
entry_price: float
|
|
exit_price: float
|
|
qty: float
|
|
pnl: float
|
|
return_pct: float
|
|
reason: str
|
|
|
|
|
|
class BacktestPointOut(BaseModel):
|
|
ts: int
|
|
equity: float
|
|
buy_hold: float
|
|
price: float
|
|
|
|
|
|
class BacktestMetricsOut(BaseModel):
|
|
initial_equity: float
|
|
final_equity: float
|
|
total_return_pct: float
|
|
buy_hold_return_pct: float
|
|
max_drawdown_pct: float
|
|
num_trades: int
|
|
win_rate_pct: float
|
|
profit_factor: float | None
|
|
avg_trade_pct: float
|
|
best_trade_pct: float
|
|
worst_trade_pct: float
|
|
sharpe: float | None
|
|
|
|
|
|
class BacktestResult(BaseModel):
|
|
strategy_key: str
|
|
strategy_name: str
|
|
symbol: str
|
|
timeframe: str
|
|
starting_balance: float
|
|
metrics: BacktestMetricsOut
|
|
equity_curve: list[BacktestPointOut]
|
|
trades: list[BacktestTradeOut]
|
|
error: str | None = None
|
|
|
|
|
|
class SweepRequest(BaseModel):
|
|
strategy_key: str
|
|
base_params: dict[str, Any] = Field(default_factory=dict)
|
|
# param name -> list of candidate values (cartesian product)
|
|
grid: dict[str, list[Any]] = Field(default_factory=dict)
|
|
symbol: str | None = None
|
|
timeframe: str | None = None
|
|
limit: int = Field(default=500, ge=20, le=1000)
|
|
starting_balance: float | None = None
|
|
top_n: int = Field(default=20, ge=1, le=100)
|
|
|
|
|
|
class SweepRowOut(BaseModel):
|
|
params: dict[str, Any]
|
|
metrics: BacktestMetricsOut
|
|
|
|
|
|
class SweepResult(BaseModel):
|
|
strategy_key: str
|
|
strategy_name: str
|
|
symbol: str
|
|
timeframe: str
|
|
combinations: int
|
|
rows: list[SweepRowOut]
|
|
error: str | None = None
|