Multi-strategy engine with per-strategy capital allocation

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.
This commit is contained in:
matitos committed 2026-09-21 20:10:28 +00:00
1 parent e07536542f
commit 8b6cf8130c
6 files changed
+469 -28

No files matched your search

+50
View File
@@ -134,6 +134,18 @@ class StrategyConfigOut(BaseModel):
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
@@ -142,6 +154,44 @@ class StartRequest(BaseModel):
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