-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtrade_engine.py
More file actions
153 lines (124 loc) · 3.84 KB
/
trade_engine.py
File metadata and controls
153 lines (124 loc) · 3.84 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""Trade simulation engine for direction predictions."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
import pandas as pd
from config import GameConfig, DEFAULT_CONFIG
Prediction = Literal["UP", "DOWN", "SAME"]
def classify_outcome(
entry_price: float,
exit_price: float,
threshold: float,
) -> Prediction:
"""Classify price move as UP, DOWN, or SAME based on threshold."""
if entry_price <= 0:
return "SAME"
pct = (exit_price - entry_price) / entry_price
if pct > threshold:
return "UP"
if pct < -threshold:
return "DOWN"
return "SAME"
def get_trade_return(
prediction: Prediction,
entry_price: float,
exit_price: float,
) -> float:
"""
Simulate trade return:
UP -> long: (exit - entry) / entry
DOWN -> short: (entry - exit) / entry
SAME -> flat: 0
"""
if entry_price <= 0:
return 0.0
if prediction == "UP":
return (exit_price - entry_price) / entry_price
if prediction == "DOWN":
return (entry_price - exit_price) / entry_price
return 0.0
@dataclass
class TradeResult:
"""Result of a simulated trade."""
prediction: Prediction
actual: Prediction
correct: bool
trade_return: float
entry_price: float
exit_price: float
def simulate_trade(
df: pd.DataFrame,
start_idx: int,
prediction: Prediction,
config: GameConfig | None = None,
) -> TradeResult | None:
"""
Simulate trade: enter at close of window, hold for forward_days, exit.
Returns TradeResult or None if insufficient data.
"""
config = config or DEFAULT_CONFIG
end_idx = start_idx + config.window_candles
exit_idx = end_idx + config.forward_candles - 1
if exit_idx >= len(df):
return None
df = df.copy()
df.columns = [c.lower() for c in df.columns]
entry_price = float(df.iloc[end_idx - 1]["close"])
exit_price = float(df.iloc[exit_idx]["close"])
actual = classify_outcome(
entry_price,
exit_price,
config.threshold,
)
correct = prediction == actual
trade_return = get_trade_return(prediction, entry_price, exit_price)
return TradeResult(
prediction=prediction,
actual=actual,
correct=correct,
trade_return=trade_return,
entry_price=entry_price,
exit_price=exit_price,
)
def get_future_prices(
df: pd.DataFrame,
start_idx: int,
config: GameConfig | None = None,
) -> pd.DataFrame | None:
"""Get the future price series for the forward window (for reveal chart)."""
config = config or DEFAULT_CONFIG
end_idx = start_idx + config.window_candles
exit_idx = end_idx + config.forward_candles - 1
if exit_idx >= len(df):
return None
df = df.copy()
df.columns = [c.lower() for c in df.columns]
future = df.iloc[end_idx : exit_idx + 1][["datetime", "open", "high", "low", "close"]].copy()
future.columns = [c.lower() for c in future.columns]
return future
def compute_max_drawdown(equity_curve: list[float]) -> float:
"""Compute max drawdown from equity curve (cumulative returns)."""
if not equity_curve:
return 0.0
peak = equity_curve[0]
max_dd = 0.0
for v in equity_curve:
peak = max(peak, v)
dd = (peak - v) / peak if peak > 0 else 0
max_dd = max(max_dd, dd)
return max_dd
def compute_sharpe(
returns: list[float],
risk_free: float = 0.0,
) -> float:
"""Annualized Sharpe ratio (simplified). Assumes 4h returns."""
if not returns or len(returns) < 2:
return 0.0
import pandas as pd
s = pd.Series(returns)
excess = s - risk_free
if excess.std() == 0:
return 0.0
# Annualization: ~6*365 periods per year for 4h
ann_factor = (6 * 365) ** 0.5
return float(excess.mean() / excess.std() * ann_factor)