first commit
This commit is contained in:
84
risk_manager.py
Normal file
84
risk_manager.py
Normal file
@@ -0,0 +1,84 @@
|
||||
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
|
||||
Reference in New Issue
Block a user