1df4b11aef
CI / lint-test-build (push) Failing after 1m7s
- Introduced new HTML templates for the dashboard, metrics, overview, and backtesting functionalities. - Implemented partial templates for metrics, overview, audit, controls, and charts to enhance modularity. - Updated the Jinja2 template resolution logic to support different deployment environments. - Added a health check template to display the service status. - Included a test suite to verify the template resolution logic. - Updated `pyproject.toml` to include new HTML templates in the package data.
99 lines
3.6 KiB
Python
99 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
|
|
from arbitrade.alerting.notifier import SupportsAlerts, dispatch_alert_nowait
|
|
|
|
|
|
class TradeLimitsGuard:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
max_concurrent_trades: int | None = None,
|
|
max_exposure_per_asset: float | None = None,
|
|
alert_notifier: SupportsAlerts | None = None,
|
|
) -> None:
|
|
if max_concurrent_trades is not None and max_concurrent_trades <= 0:
|
|
raise ValueError("max_concurrent_trades must be > 0")
|
|
if max_exposure_per_asset is not None and max_exposure_per_asset <= 0.0:
|
|
raise ValueError("max_exposure_per_asset must be > 0.0")
|
|
|
|
self._max_concurrent_trades = max_concurrent_trades
|
|
self._max_exposure_per_asset = max_exposure_per_asset
|
|
self._active_trades = 0
|
|
self._asset_exposure: dict[str, float] = {}
|
|
self._alert_notifier = alert_notifier
|
|
|
|
@property
|
|
def active_trades(self) -> int:
|
|
return self._active_trades
|
|
|
|
def exposure_for_asset(self, asset: str) -> float:
|
|
return self._asset_exposure.get(asset.upper(), 0.0)
|
|
|
|
def is_trade_allowed(self, exposure_by_asset: Mapping[str, float]) -> bool:
|
|
if (
|
|
self._max_concurrent_trades is not None
|
|
and self._active_trades >= self._max_concurrent_trades
|
|
):
|
|
dispatch_alert_nowait(
|
|
self._alert_notifier,
|
|
category="threshold",
|
|
severity="warning",
|
|
title="Concurrent trade limit reached",
|
|
message="Trade rejected by concurrent trade cap.",
|
|
details={
|
|
"active_trades": f"{self._active_trades}",
|
|
"max_concurrent_trades": f"{self._max_concurrent_trades}",
|
|
},
|
|
)
|
|
return False
|
|
|
|
if self._max_exposure_per_asset is None:
|
|
return True
|
|
|
|
for asset, exposure in exposure_by_asset.items():
|
|
if exposure <= 0.0:
|
|
continue
|
|
key = asset.upper()
|
|
next_exposure = self._asset_exposure.get(key, 0.0) + exposure
|
|
if next_exposure > self._max_exposure_per_asset:
|
|
dispatch_alert_nowait(
|
|
self._alert_notifier,
|
|
category="threshold",
|
|
severity="warning",
|
|
title="Asset exposure limit reached",
|
|
message="Trade rejected by per-asset exposure cap.",
|
|
details={
|
|
"asset": key,
|
|
"next_exposure": f"{next_exposure}",
|
|
"max_exposure_per_asset": f"{self._max_exposure_per_asset}",
|
|
},
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
def open_trade(self, exposure_by_asset: Mapping[str, float]) -> None:
|
|
self._active_trades += 1
|
|
for asset, exposure in exposure_by_asset.items():
|
|
if exposure <= 0.0:
|
|
continue
|
|
key = asset.upper()
|
|
self._asset_exposure[key] = self._asset_exposure.get(key, 0.0) + exposure
|
|
|
|
def close_trade(self, exposure_by_asset: Mapping[str, float]) -> None:
|
|
if self._active_trades > 0:
|
|
self._active_trades -= 1
|
|
|
|
for asset, exposure in exposure_by_asset.items():
|
|
if exposure <= 0.0:
|
|
continue
|
|
key = asset.upper()
|
|
current = self._asset_exposure.get(key, 0.0)
|
|
next_exposure = max(current - exposure, 0.0)
|
|
if next_exposure == 0.0:
|
|
self._asset_exposure.pop(key, None)
|
|
else:
|
|
self._asset_exposure[key] = next_exposure
|