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

+44 -28
View File
@@ -11,6 +11,7 @@ from app.broker import build_broker
from app.broker.base import Broker
from app.config import Settings
from app.engine.engine import TradingEngine
from app.engine.manager import MultiStrategyManager
from app.engine.strategies import get_strategy
from app.market import build_market_data
from app.market.base import MarketData
@@ -18,7 +19,10 @@ from app.notify import Notifier, build_notifier
from app.schemas import (
BacktestRequest,
BacktestResult,
MultiStatusOut,
StartManyRequest,
StatusOut,
StrategySpec,
SweepRequest,
SweepResult,
SweepRowOut,
@@ -52,10 +56,22 @@ class Runtime:
self._broker_factory = broker_factory
self.notifier = notifier if notifier is not None else notifier_factory(settings)
self.notify_levels = notify_levels or {"trade", "error"}
self.engine: TradingEngine | None = None
self.manager = MultiStrategyManager(
store=self.store,
settings=settings,
feed_factory=feed_factory,
broker_factory=broker_factory,
)
self.manager.set_publisher(self._broadcast)
self._subscribers: set[asyncio.Queue] = set()
self._notify_tasks: set[asyncio.Task] = set()
# The first (usually only) engine, kept for the single-strategy API and
# backwards compatibility with existing callers and tests.
@property
def engine(self) -> TradingEngine | None:
return self.manager.runs[0].engine if self.manager.runs else None
# --- pub/sub ------------------------------------------------------
def subscribe(self) -> asyncio.Queue:
queue: asyncio.Queue = asyncio.Queue(maxsize=100)
@@ -96,36 +112,37 @@ class Runtime:
timeframe: str | None = None,
starting_balance: float | None = None,
) -> StatusOut:
if self.engine is not None and self.engine.running:
await self.stop()
engine = TradingEngine(
feed=self._feed_factory(self.settings),
broker=self._broker_factory(self.settings),
store=self.store,
settings=self.settings,
await self.start_many(
StartManyRequest(
strategies=[
StrategySpec(
strategy_key=strategy_key,
params=params or {},
symbol=symbol,
timeframe=timeframe,
)
],
starting_balance=starting_balance,
)
)
engine.set_publisher(self._broadcast)
self.engine = engine
await engine.start(
strategy_key,
params,
symbol=symbol,
timeframe=timeframe,
starting_balance=starting_balance,
)
return engine.status()
engine = self.engine
return engine.status() if engine is not None else self._idle_status()
async def start_many(self, request: StartManyRequest) -> MultiStatusOut:
return await self.manager.start(request)
async def stop(self) -> StatusOut:
if self.engine is not None:
await self.engine.stop()
return self.engine.status()
return self._idle_status()
await self.manager.stop()
engine = self.engine
return engine.status() if engine is not None else self._idle_status()
async def kill(self, reason: str = "manual kill switch") -> StatusOut:
if self.engine is not None:
await self.engine.kill(reason)
return self.engine.status()
return self._idle_status()
await self.manager.kill(reason)
engine = self.engine
return engine.status() if engine is not None else self._idle_status()
def multi_status(self) -> MultiStatusOut:
return self.manager.status()
async def run_backtest(self, request: BacktestRequest) -> BacktestResult:
symbol = request.symbol or self.settings.symbol
@@ -182,8 +199,7 @@ class Runtime:
)
async def shutdown(self) -> None:
if self.engine is not None:
await self.engine.shutdown()
await self.manager.shutdown()
if self.notifier is not None:
await self.notifier.close()