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:
1 parent
e07536542f
commit
8b6cf8130c
6 files changed
+469
-28
No files matched your search
+44
-28
@@ -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()
|
||||
|
||||
|
||||
Reference in new issue
Block a user