Files
crypto-trader/risk_manager.py
2026-05-03 23:37:55 -04:00

85 lines
3.0 KiB
Python

from __future__ import annotations
import logging
from dataclasses import dataclass
from enum import Enum
from config import Config
from portfolio import Position
log = logging.getLogger(__name__)
class ExitReason(Enum):
HARD_STOP = "hard_stop" # Price dropped too far from entry
TRAILING_STOP = "trailing_stop" # Price dropped too far from peak
TAKE_PROFIT = "take_profit" # Price hit the profit target
TIME_LIMIT = "time_limit" # Position held too long
@dataclass
class ExitSignal:
reason: ExitReason
current_price: float
pnl_pct: float
class RiskManager:
def __init__(self, config: Config):
self.config = config
def check(self, position: Position, current_price: float) -> ExitSignal | None:
"""
Evaluate a position against all exit rules. Returns an ExitSignal if any
rule triggers, otherwise None. Also updates the position's peak price.
Rules are checked in priority order:
1. Hard stop — worst-case absolute floor
2. Trailing stop — peak-relative stop, protects accumulated gains
3. Take profit — locks in the profit target
4. Time limit — exits stagnant positions to free capital
"""
position.update_peak(current_price)
pnl_pct = position.pnl_pct(current_price)
drop_from_peak = position.drop_from_peak_pct(current_price)
hours_held = position.hours_held()
# 1. Hard stop-loss
if pnl_pct <= -self.config.hard_stop_pct:
log.warning(
"%s HARD STOP: pnl=%.2f%% (limit=%.2f%%)",
position.pair, pnl_pct, -self.config.hard_stop_pct,
)
return ExitSignal(ExitReason.HARD_STOP, current_price, pnl_pct)
# 2. Trailing stop
if drop_from_peak >= self.config.trailing_stop_pct:
log.warning(
"%s TRAILING STOP: dropped %.2f%% from peak $%.6f (current $%.6f) pnl=%.2f%%",
position.pair, drop_from_peak, position.peak_price, current_price, pnl_pct,
)
return ExitSignal(ExitReason.TRAILING_STOP, current_price, pnl_pct)
# 3. Take profit
if pnl_pct >= self.config.take_profit_pct:
log.info(
"%s TAKE PROFIT: pnl=%.2f%% (target=%.2f%%)",
position.pair, pnl_pct, self.config.take_profit_pct,
)
return ExitSignal(ExitReason.TAKE_PROFIT, current_price, pnl_pct)
# 4. Time limit
if hours_held >= self.config.max_hold_hours:
log.info(
"%s TIME LIMIT: held %.1fh / %dh pnl=%.2f%%",
position.pair, hours_held, self.config.max_hold_hours, pnl_pct,
)
return ExitSignal(ExitReason.TIME_LIMIT, current_price, pnl_pct)
log.debug(
"%s HOLD: pnl=%.2f%% peak_drop=%.2f%% held=%.1fh peak=$%.6f",
position.pair, pnl_pct, drop_from_peak, hours_held, position.peak_price,
)
return None