-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprop_rules.py
More file actions
117 lines (95 loc) · 3.65 KB
/
Copy pathprop_rules.py
File metadata and controls
117 lines (95 loc) · 3.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""
Prop firm drawdown rules — persistent baselines and DD computation.
"""
from __future__ import annotations
import json
import logging
from dataclasses import asdict, dataclass
from datetime import date, datetime
from pathlib import Path
from zoneinfo import ZoneInfo
from config import ACCOUNT, LOGS_DIR, RISK
logger = logging.getLogger(__name__)
PROP_STATE_PATH = LOGS_DIR / "prop_state.json"
@dataclass
class PropState:
daily_date: str
daily_start_equity: float
trailing_max_equity: float
trades_today: int = 0
@classmethod
def empty(cls, equity: float) -> PropState:
today = datetime.now(ZoneInfo(RISK.timezone)).date().isoformat()
base = equity if equity > 0 else ACCOUNT.starting_balance
return cls(
daily_date=today,
daily_start_equity=base,
trailing_max_equity=base,
trades_today=0,
)
@dataclass
class DrawdownResult:
daily_start_balance: float
trailing_max_equity: float
daily_drawdown_usd: float
daily_drawdown_pct: float
trailing_drawdown_usd: float
trailing_drawdown_pct: float
is_critical: bool
def load_state(default_equity: float) -> PropState:
if not PROP_STATE_PATH.exists():
return PropState.empty(default_equity)
try:
raw = json.loads(PROP_STATE_PATH.read_text(encoding="utf-8"))
return PropState(
daily_date=str(raw.get("daily_date", "")),
daily_start_equity=float(raw.get("daily_start_equity", default_equity)),
trailing_max_equity=float(raw.get("trailing_max_equity", default_equity)),
trades_today=int(raw.get("trades_today", 0)),
)
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
logger.warning("prop_state load failed: %s", exc)
return PropState.empty(default_equity)
def save_state(state: PropState) -> None:
try:
PROP_STATE_PATH.write_text(json.dumps(asdict(state), indent=2), encoding="utf-8")
except OSError as exc:
logger.warning("prop_state save failed: %s", exc)
def update_baselines(state: PropState, equity: float, now: datetime | None = None) -> PropState:
tz = ZoneInfo(RISK.timezone)
now = now or datetime.now(tz)
today = now.astimezone(tz).date().isoformat()
if state.daily_date != today:
state = PropState(
daily_date=today,
daily_start_equity=equity,
trailing_max_equity=max(state.trailing_max_equity, equity),
trades_today=0,
)
else:
state.trailing_max_equity = max(state.trailing_max_equity, equity)
save_state(state)
return state
def record_trade_open(state: PropState) -> PropState:
state.trades_today += 1
save_state(state)
return state
def compute_drawdowns(equity: float, state: PropState) -> DrawdownResult:
dd_base = ACCOUNT.starting_balance if ACCOUNT.starting_balance > 0 else equity
daily_dd_usd = max(0.0, state.daily_start_equity - equity)
daily_dd_pct = (daily_dd_usd / dd_base * 100) if dd_base else 0.0
trailing_dd_usd = max(0.0, state.trailing_max_equity - equity)
trailing_dd_pct = (trailing_dd_usd / dd_base * 100) if dd_base else 0.0
is_critical = (
daily_dd_pct >= ACCOUNT.daily_drawdown_limit_pct
or trailing_dd_pct >= ACCOUNT.trailing_drawdown_limit_pct
)
return DrawdownResult(
daily_start_balance=state.daily_start_equity,
trailing_max_equity=state.trailing_max_equity,
daily_drawdown_usd=daily_dd_usd,
daily_drawdown_pct=daily_dd_pct,
trailing_drawdown_usd=trailing_dd_usd,
trailing_drawdown_pct=trailing_dd_pct,
is_critical=is_critical,
)