feat(kevin_bridge): blocklist + daily risk counters
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled

This commit is contained in:
Viktor Barzin 2026-05-24 01:01:54 +00:00
parent 3347847e38
commit a417cae77b
4 changed files with 195 additions and 0 deletions

View file

@ -0,0 +1,37 @@
"""Redis-backed per-symbol blocklist with TTL.
Set on AVOID mentions. Subsequent BUY mention on the same symbol clears
the entry (handled by the strategy callsite, not here).
"""
from __future__ import annotations
from typing import Any
class KevinBlocklist:
_KEY_PREFIX = "kevin:blocked:"
def __init__(self, redis: Any) -> None:
self.redis = redis
async def add(self, symbol: str, ttl_days: int) -> None:
await self.redis.set(
f"{self._KEY_PREFIX}{symbol}",
"1",
ex=ttl_days * 86400,
)
async def remove(self, symbol: str) -> None:
await self.redis.delete(f"{self._KEY_PREFIX}{symbol}")
async def is_blocked(self, symbol: str) -> bool:
return bool(await self.redis.exists(f"{self._KEY_PREFIX}{symbol}"))
async def active_set(self) -> set[str]:
keys = await self.redis.keys(f"{self._KEY_PREFIX}*")
out: set[str] = set()
for k in keys:
key_str = k.decode() if isinstance(k, bytes) else k
out.add(key_str.replace(self._KEY_PREFIX, ""))
return out

View file

@ -0,0 +1,54 @@
"""Daily Redis counters for trade-cap + alloc-cap + pause flag."""
from __future__ import annotations
from datetime import datetime, timezone
from decimal import Decimal
from typing import Any
class KevinRiskCounters:
_PAUSE_KEY = "trading:paused"
_TRADES_KEY = "kevin:daily_trades:{date}"
_ALLOC_KEY = "kevin:daily_alloc:{date}"
def __init__(self, redis: Any) -> None:
self.redis = redis
@staticmethod
def _today_utc() -> str:
return datetime.now(timezone.utc).strftime("%Y%m%d")
async def get_daily_trades(self) -> int:
v = await self.redis.get(self._TRADES_KEY.format(date=self._today_utc()))
if v is None:
return 0
return int(v)
async def increment_daily_trades(self) -> int:
key = self._TRADES_KEY.format(date=self._today_utc())
n = await self.redis.incr(key)
await self.redis.expire(key, 172800) # 48h
return int(n)
async def get_daily_alloc(self) -> Decimal:
v = await self.redis.get(self._ALLOC_KEY.format(date=self._today_utc()))
if v is None:
return Decimal("0")
s = v.decode() if isinstance(v, bytes) else str(v)
return Decimal(s)
async def add_daily_alloc(self, usd: Decimal) -> Decimal:
key = self._ALLOC_KEY.format(date=self._today_utc())
new = await self.redis.incrbyfloat(key, float(usd))
await self.redis.expire(key, 172800)
return Decimal(str(new))
async def is_trading_paused(self) -> bool:
return bool(await self.redis.get(self._PAUSE_KEY))
async def set_trading_paused(self, ttl_seconds: int | None = None) -> None:
if ttl_seconds:
await self.redis.set(self._PAUSE_KEY, "1", ex=ttl_seconds)
else:
await self.redis.set(self._PAUSE_KEY, "PERMANENT")