HEX
Server: Apache/2.4.18 (Ubuntu)
System: Linux ubuntu 7.0.5-x86_64-linode173 #1 SMP PREEMPT_DYNAMIC Fri May 8 10:12:05 EDT 2026 x86_64
User: root (0)
PHP: 7.2.28-1+ubuntu16.04.1+deb.sury.org+1
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
Upload Files
File: //proc/self/root/opt/trading-bot/risk_engine.py
import logging
from datetime import datetime, timezone, timedelta
from config import settings
from db import SessionLocal
from models import FeedHeartbeat, PortfolioState, Position, PnlHistory

logger = logging.getLogger(__name__)

class RiskEngine:
    def __init__(self):
        self.startup_time = datetime.now(timezone.utc)

    def check_stale_data(self, strategy_name: str = "") -> bool:
        """Returns True if data is fresh, False if stale. Allows grace period at startup.
        Only checks feeds relevant to the strategy type."""
        now = datetime.now(timezone.utc)
        
        # Grace period for startup (300 seconds)
        if (now - self.startup_time).total_seconds() < 300:
            logger.info("RiskEngine in startup grace period. Skipping stale data checks.")
            return True

        # Determine which feeds this strategy cares about
        is_equity = strategy_name.startswith("Equity")
        
        with SessionLocal() as session:
            heartbeats = session.query(FeedHeartbeat).all()
            if not heartbeats:
                logger.warning("No feed heartbeats found in DB.")
                return False
            
            for hb in heartbeats:
                # Skip irrelevant feeds
                if is_equity and hb.feed_name == "crypto_market":
                    continue
                if not is_equity and hb.feed_name == "equity_market":
                    continue
                    
                hb_time = hb.last_updated
                if hb_time.tzinfo is None:
                    hb_time = hb_time.replace(tzinfo=timezone.utc)
                
                if (now - hb_time).total_seconds() > settings.STALE_DATA_THRESHOLD:
                    logger.error(f"Feed {hb.feed_name} is stale. Last updated: {hb_time}")
                    return False
            return True

    def check_exposure_limits(self, current_exposure: float, new_trade_usd: float) -> bool:
        """Checks if a new trade would exceed exposure limits."""
        if abs(current_exposure + new_trade_usd) > settings.MAX_EXPOSURE_USD:
            logger.warning(f"Trade would exceed MAX_EXPOSURE_USD: {current_exposure + new_trade_usd} > {settings.MAX_EXPOSURE_USD}")
            return False
        return True

    def check_daily_loss_limit(self) -> bool:
        """Returns True if within loss limits, False if exceeded."""
        with SessionLocal() as session:
            # Simple check: sum realized PnL for today
            today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
            pnl_records = session.query(PnlHistory).filter(PnlHistory.ts >= today_start).all()
            total_realized = sum((p.realized_pnl or 0.0) for p in pnl_records)
            
            if total_realized < -settings.DAILY_LOSS_LIMIT_USD:
                logger.error(f"Daily loss limit hit: {total_realized} < -{settings.DAILY_LOSS_LIMIT_USD}")
                return False
        return True

    def check_negative_balance(self, strategy_name: str = "") -> bool:
        """Returns True if balance is healthy, False if cash or equity is negative."""
        if not strategy_name:
            return True
        with SessionLocal() as session:
            latest = session.query(PortfolioState).filter_by(strategy_name=strategy_name).order_by(PortfolioState.ts.desc()).first()
            if not latest:
                return True  # No state yet, assume initial $10k
            if float(latest.cash) < 0:
                logger.error(f"KILL SWITCH: {strategy_name} has negative cash: ${float(latest.cash):.2f}")
                return False
            if float(latest.equity) < 0:
                logger.error(f"KILL SWITCH: {strategy_name} has negative equity: ${float(latest.equity):.2f}")
                return False
        return True

    def is_trading_allowed(self, strategy_name: str = "") -> bool:
        """Main safety check before any trade."""
        if not self.check_negative_balance(strategy_name):
            return False

        if not self.check_stale_data(strategy_name):
            return False
        
        if not self.check_daily_loss_limit():
            return False
            
        return True


    def get_drawdown_throttle(self, current_drawdown_pct: float) -> float:
        """Returns a multiplier (0-1) for sizing based on drawdown."""
        if current_drawdown_pct >= settings.MAX_DRAWDOWN_PCT:
            logger.warning(f"Drawdown limit hit ({current_drawdown_pct}%). Throttling sizing to 0.")
            return 0.0
        elif current_drawdown_pct >= settings.MAX_DRAWDOWN_PCT / 2:
            # Reduce sizing by half if we are halfway to the limit
            return 0.5
        return 1.0