tradepose-client 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tradepose-client might be problematic. Click here for more details.
- tradepose_client/__init__.py +156 -0
- tradepose_client/analysis.py +302 -0
- tradepose_client/api/__init__.py +8 -0
- tradepose_client/api/engine.py +59 -0
- tradepose_client/api/export.py +828 -0
- tradepose_client/api/health.py +70 -0
- tradepose_client/api/strategy.py +228 -0
- tradepose_client/client.py +58 -0
- tradepose_client/models.py +1836 -0
- tradepose_client/schema.py +186 -0
- tradepose_client/viz.py +762 -0
- tradepose_client-0.1.0.dist-info/METADATA +576 -0
- tradepose_client-0.1.0.dist-info/RECORD +15 -0
- tradepose_client-0.1.0.dist-info/WHEEL +4 -0
- tradepose_client-0.1.0.dist-info/licenses/LICENSE +21 -0
tradepose_client/viz.py
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trade Visualization Module
|
|
3
|
+
|
|
4
|
+
Provides Altair-based plotting functions for analyzing backtest trades data,
|
|
5
|
+
including MAE/MFE analysis, PnL curves, and trade distribution histograms.
|
|
6
|
+
|
|
7
|
+
All functions accept a Polars DataFrame with trades data from the API
|
|
8
|
+
and return interactive Altair charts.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import polars as pl
|
|
12
|
+
import altair as alt
|
|
13
|
+
from typing import Optional, Literal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Helper Functions
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
def add_win_loss_label(trades: pl.DataFrame) -> pl.DataFrame:
|
|
21
|
+
"""Add 'win_loss' column labeling trades as 'Win' or 'Loss'
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
trades: DataFrame with 'pnl' column
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
DataFrame with additional 'win_loss' column
|
|
28
|
+
"""
|
|
29
|
+
return trades.with_columns(
|
|
30
|
+
pl.when(pl.col("pnl") > 0)
|
|
31
|
+
.then(pl.lit("Win"))
|
|
32
|
+
.otherwise(pl.lit("Loss"))
|
|
33
|
+
.alias("win_loss")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def calculate_mea(trades: pl.DataFrame) -> pl.DataFrame:
|
|
38
|
+
"""Calculate MEA (Maximum Execution Analysis) - ATR-normalized MAE/MFE ratios
|
|
39
|
+
|
|
40
|
+
Each metric is normalized by its corresponding volatility measurement:
|
|
41
|
+
- mae / mae_volatility -> mae_atr_ratio
|
|
42
|
+
- mfe / mfe_volatility -> mfe_atr_ratio
|
|
43
|
+
- g_mfe / g_mfe_volatility -> g_mfe_atr_ratio
|
|
44
|
+
- mae_lv1 / mae_lv1_volatility -> mae_lv1_atr_ratio
|
|
45
|
+
- mhl / mhl_volatility -> mhl_atr_ratio
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
trades: DataFrame with MAE/MFE and corresponding volatility columns
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
DataFrame with additional ATR ratio columns
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> trades = client.quick_backtest_results(["my_strategy"])[0]
|
|
55
|
+
>>> trades = calculate_mea(trades)
|
|
56
|
+
>>> print(trades.columns) # Will include mae_atr_ratio, mfe_atr_ratio, etc.
|
|
57
|
+
"""
|
|
58
|
+
return trades.with_columns([
|
|
59
|
+
(pl.col("mae") / pl.col("mae_volatility")).alias("mae_atr_ratio"),
|
|
60
|
+
(pl.col("mfe") / pl.col("mfe_volatility")).alias("mfe_atr_ratio"),
|
|
61
|
+
(pl.col("g_mfe") / pl.col("g_mfe_volatility")).alias("g_mfe_atr_ratio"),
|
|
62
|
+
(pl.col("mae_lv1") / pl.col("mae_lv1_volatility")).alias("mae_lv1_atr_ratio"),
|
|
63
|
+
(pl.col("mhl") / pl.col("mhl_volatility")).alias("mhl_atr_ratio"),
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Legacy alias for backward compatibility
|
|
68
|
+
def calculate_mae_atr_ratio(
|
|
69
|
+
trades: pl.DataFrame,
|
|
70
|
+
volatility_col: str = "entry_volatility"
|
|
71
|
+
) -> pl.DataFrame:
|
|
72
|
+
"""Legacy function - use calculate_mea() instead
|
|
73
|
+
|
|
74
|
+
This function is deprecated and will use calculate_mea() internally.
|
|
75
|
+
"""
|
|
76
|
+
return calculate_mea(trades)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def calculate_cumulative_pnl(
|
|
80
|
+
trades: pl.DataFrame,
|
|
81
|
+
sort_col: str = "exit_time"
|
|
82
|
+
) -> pl.DataFrame:
|
|
83
|
+
"""Calculate cumulative PnL and drawdown metrics
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
trades: DataFrame with 'pnl_pct' column
|
|
87
|
+
sort_col: Column to sort by (default: exit_time)
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
DataFrame with cumulative metrics and high-water marks
|
|
91
|
+
"""
|
|
92
|
+
df = trades.sort(sort_col).with_columns([
|
|
93
|
+
pl.col("pnl_pct").cum_sum().alias("cum_pnl_pct"),
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
df = df.with_columns([
|
|
97
|
+
pl.col("cum_pnl_pct").cum_max().alias("cummax"),
|
|
98
|
+
(pl.col("cum_pnl_pct").cum_max() - pl.col("cum_pnl_pct")).alias("drawdown"),
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
# Mark new highs
|
|
102
|
+
df = df.with_columns([
|
|
103
|
+
pl.when(
|
|
104
|
+
(pl.col("cum_pnl_pct") == pl.col("cummax")) &
|
|
105
|
+
(pl.col("cum_pnl_pct") != pl.col("cum_pnl_pct").shift(1))
|
|
106
|
+
)
|
|
107
|
+
.then(pl.col("cum_pnl_pct"))
|
|
108
|
+
.alias("new_high")
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
# Mark new drawdown lows (per month)
|
|
112
|
+
df = df.with_columns([
|
|
113
|
+
pl.col(sort_col).dt.strftime("%Y-%m").alias("year_month")
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
df = df.with_columns([
|
|
117
|
+
pl.when(
|
|
118
|
+
(pl.col("drawdown") == pl.col("drawdown").max().over("year_month")) &
|
|
119
|
+
(pl.col("drawdown") != pl.col("drawdown").shift(1))
|
|
120
|
+
)
|
|
121
|
+
.then(pl.col("drawdown"))
|
|
122
|
+
.alias("new_dd")
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
return df
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_quantiles(
|
|
129
|
+
trades: pl.DataFrame,
|
|
130
|
+
column: str,
|
|
131
|
+
quantiles: list[float] = [0.25, 0.5, 0.75]
|
|
132
|
+
) -> dict[str, float]:
|
|
133
|
+
"""Calculate quantiles for a column
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
trades: DataFrame
|
|
137
|
+
column: Column name
|
|
138
|
+
quantiles: List of quantile values (0-1)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Dictionary mapping quantile names to values
|
|
142
|
+
"""
|
|
143
|
+
result = {}
|
|
144
|
+
for q in quantiles:
|
|
145
|
+
q_name = f"q{int(q * 100)}"
|
|
146
|
+
result[q_name] = trades[column].quantile(q)
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# Main Plotting Functions
|
|
152
|
+
# =============================================================================
|
|
153
|
+
|
|
154
|
+
def plot_mae_mfe_scatter(
|
|
155
|
+
trades: pl.DataFrame,
|
|
156
|
+
x_col: str = "mae_atr_ratio",
|
|
157
|
+
y_col: str = "g_mfe_atr_ratio",
|
|
158
|
+
title: str = "MAE vs MFE Analysis",
|
|
159
|
+
apply_config: bool = True
|
|
160
|
+
) -> alt.Chart:
|
|
161
|
+
"""Create scatter plot of MAE vs MFE with quantile reference lines
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
trades: DataFrame with MAE/MFE columns
|
|
165
|
+
x_col: X-axis column (default: mae_atr_ratio)
|
|
166
|
+
y_col: Y-axis column (default: g_mfe_atr_ratio)
|
|
167
|
+
title: Chart title
|
|
168
|
+
apply_config: Apply default styling config (set False for chart composition)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Interactive Altair scatter plot
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
>>> trades = client.quick_backtest_results(["my_strategy"])[0]
|
|
175
|
+
>>> trades = calculate_mea(trades)
|
|
176
|
+
>>> chart = plot_mae_mfe_scatter(trades)
|
|
177
|
+
>>> chart.show()
|
|
178
|
+
|
|
179
|
+
>>> # For composition (no config conflicts):
|
|
180
|
+
>>> chart1 = plot_mae_mfe_scatter(trades, apply_config=False)
|
|
181
|
+
>>> chart2 = plot_pnl_curves(trades, apply_config=False)
|
|
182
|
+
>>> combined = chart1 | chart2
|
|
183
|
+
"""
|
|
184
|
+
# Calculate ATR ratios if not present
|
|
185
|
+
if x_col not in trades.columns or y_col not in trades.columns:
|
|
186
|
+
trades = calculate_mea(trades)
|
|
187
|
+
|
|
188
|
+
# Add win/loss labels
|
|
189
|
+
if "win_loss" not in trades.columns:
|
|
190
|
+
trades = add_win_loss_label(trades)
|
|
191
|
+
|
|
192
|
+
# Calculate quantiles
|
|
193
|
+
x_q75 = trades[x_col].quantile(0.75)
|
|
194
|
+
y_q75 = trades[y_col].quantile(0.75)
|
|
195
|
+
|
|
196
|
+
# Add quantile columns for reference lines
|
|
197
|
+
trades = trades.with_columns([
|
|
198
|
+
pl.lit(x_q75).alias("x_q75"),
|
|
199
|
+
pl.lit(y_q75).alias("y_q75"),
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
# Base scatter plot
|
|
203
|
+
scatter = alt.Chart(trades).mark_circle(size=60, opacity=0.6).encode(
|
|
204
|
+
x=alt.X(f"{x_col}:Q", title=x_col.replace("_", " ").title()),
|
|
205
|
+
y=alt.Y(f"{y_col}:Q", title=y_col.replace("_", " ").title()),
|
|
206
|
+
color=alt.Color(
|
|
207
|
+
"win_loss:N",
|
|
208
|
+
scale=alt.Scale(
|
|
209
|
+
domain=["Win", "Loss"],
|
|
210
|
+
range=["#00C49A", "#FF6B6B"]
|
|
211
|
+
),
|
|
212
|
+
legend=alt.Legend(title="Result")
|
|
213
|
+
),
|
|
214
|
+
tooltip=[
|
|
215
|
+
alt.Tooltip("position_id:Q", title="Position ID"),
|
|
216
|
+
alt.Tooltip("entry_time:T", title="Entry Time"),
|
|
217
|
+
alt.Tooltip(f"{x_col}:Q", title=x_col, format=".2f"),
|
|
218
|
+
alt.Tooltip(f"{y_col}:Q", title=y_col, format=".2f"),
|
|
219
|
+
alt.Tooltip("pnl:Q", title="PnL", format=".2f"),
|
|
220
|
+
alt.Tooltip("pnl_pct:Q", title="PnL %", format=".2%"),
|
|
221
|
+
]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Vertical quantile line (Q3 for x-axis)
|
|
225
|
+
vline = alt.Chart(trades).mark_rule(
|
|
226
|
+
strokeDash=[5, 5],
|
|
227
|
+
color="gray",
|
|
228
|
+
size=1
|
|
229
|
+
).encode(
|
|
230
|
+
x="x_q75:Q"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Horizontal quantile line (Q3 for y-axis)
|
|
234
|
+
hline = alt.Chart(trades).mark_rule(
|
|
235
|
+
strokeDash=[5, 5],
|
|
236
|
+
color="gray",
|
|
237
|
+
size=1
|
|
238
|
+
).encode(
|
|
239
|
+
y="y_q75:Q"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Combine layers
|
|
243
|
+
chart = (scatter + vline + hline).properties(
|
|
244
|
+
width=500,
|
|
245
|
+
height=500,
|
|
246
|
+
title=title
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Apply config only if requested (avoid conflicts in composition)
|
|
250
|
+
if apply_config:
|
|
251
|
+
chart = chart.configure_axis(
|
|
252
|
+
gridColor="#f0f0f0"
|
|
253
|
+
).configure_view(
|
|
254
|
+
strokeWidth=0
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return chart
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def plot_mfe_mhl_analysis(
|
|
261
|
+
trades: pl.DataFrame,
|
|
262
|
+
x_col: str = "g_mfe_atr_ratio",
|
|
263
|
+
y_col: str = "mhl_atr_ratio",
|
|
264
|
+
bins: int = 40,
|
|
265
|
+
title: str = "MFE vs MHL Analysis",
|
|
266
|
+
apply_config: bool = True
|
|
267
|
+
) -> alt.Chart:
|
|
268
|
+
"""Create multi-panel MFE vs MHL analysis with histograms
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
trades: DataFrame with MFE/MHL columns
|
|
272
|
+
x_col: X-axis column (default: g_mfe_atr_ratio)
|
|
273
|
+
y_col: Y-axis column (default: mhl_atr_ratio)
|
|
274
|
+
bins: Number of histogram bins
|
|
275
|
+
title: Chart title
|
|
276
|
+
apply_config: Apply default styling config (set False for chart composition)
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Vertically stacked Altair chart with 3 panels:
|
|
280
|
+
- Panel 1: MFE vs MHL scatter with diagonal reference
|
|
281
|
+
- Panel 2: MFE histogram (all trades)
|
|
282
|
+
- Panel 3: MFE histogram by win/loss
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
>>> trades = client.quick_backtest_results(["my_strategy"])[0]
|
|
286
|
+
>>> trades = calculate_mea(trades)
|
|
287
|
+
>>> chart = plot_mfe_mhl_analysis(trades)
|
|
288
|
+
>>> chart.show()
|
|
289
|
+
"""
|
|
290
|
+
# Calculate ATR ratios if not present
|
|
291
|
+
if x_col not in trades.columns or y_col not in trades.columns:
|
|
292
|
+
trades = calculate_mea(trades)
|
|
293
|
+
|
|
294
|
+
# Add win/loss labels
|
|
295
|
+
if "win_loss" not in trades.columns:
|
|
296
|
+
trades = add_win_loss_label(trades)
|
|
297
|
+
|
|
298
|
+
# Calculate quantiles
|
|
299
|
+
x_q50 = trades[x_col].quantile(0.5)
|
|
300
|
+
x_q75 = trades[x_col].quantile(0.75)
|
|
301
|
+
x_max = trades[x_col].max()
|
|
302
|
+
y_max = trades[y_col].max()
|
|
303
|
+
|
|
304
|
+
# Add reference line data
|
|
305
|
+
n_points = 100
|
|
306
|
+
slope_data = pl.DataFrame({
|
|
307
|
+
"x_slope": [i * x_max / n_points for i in range(n_points)],
|
|
308
|
+
"y_slope": [i * y_max / n_points for i in range(n_points)],
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
trades = trades.with_columns([
|
|
312
|
+
pl.lit(x_q50).alias("x_q50"),
|
|
313
|
+
pl.lit(x_q75).alias("x_q75"),
|
|
314
|
+
])
|
|
315
|
+
|
|
316
|
+
# Panel 1: Scatter plot with diagonal reference
|
|
317
|
+
scatter = alt.Chart(trades).mark_circle(size=100, opacity=0.6).encode(
|
|
318
|
+
x=alt.X(f"{x_col}:Q", title=x_col.replace("_", " ").title()),
|
|
319
|
+
y=alt.Y(f"{y_col}:Q", title=y_col.replace("_", " ").title()),
|
|
320
|
+
color=alt.Color(
|
|
321
|
+
"win_loss:N",
|
|
322
|
+
scale=alt.Scale(
|
|
323
|
+
domain=["Win", "Loss"],
|
|
324
|
+
range=["#00C49A", "#FF6B6B"]
|
|
325
|
+
),
|
|
326
|
+
legend=alt.Legend(title="Result")
|
|
327
|
+
),
|
|
328
|
+
tooltip=[
|
|
329
|
+
alt.Tooltip("position_id:Q", title="Position ID"),
|
|
330
|
+
alt.Tooltip("entry_time:T", title="Entry Time"),
|
|
331
|
+
alt.Tooltip(f"{x_col}:Q", title=x_col, format=".2f"),
|
|
332
|
+
alt.Tooltip(f"{y_col}:Q", title=y_col, format=".2f"),
|
|
333
|
+
alt.Tooltip("pnl:Q", title="PnL", format=".2f"),
|
|
334
|
+
alt.Tooltip("pnl_pct:Q", title="PnL %", format=".2%"),
|
|
335
|
+
]
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Diagonal reference line
|
|
339
|
+
diagonal = alt.Chart(slope_data).mark_line(
|
|
340
|
+
color="gray",
|
|
341
|
+
strokeDash=[3, 3]
|
|
342
|
+
).encode(
|
|
343
|
+
x="x_slope:Q",
|
|
344
|
+
y="y_slope:Q"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Quantile lines
|
|
348
|
+
q50_line = alt.Chart(trades).mark_rule(
|
|
349
|
+
strokeDash=[5, 5],
|
|
350
|
+
color="black",
|
|
351
|
+
size=1
|
|
352
|
+
).encode(x="x_q50:Q")
|
|
353
|
+
|
|
354
|
+
q75_line = alt.Chart(trades).mark_rule(
|
|
355
|
+
strokeDash=[5, 5],
|
|
356
|
+
color="black",
|
|
357
|
+
size=1
|
|
358
|
+
).encode(x="x_q75:Q")
|
|
359
|
+
|
|
360
|
+
panel1 = (scatter + diagonal + q50_line + q75_line).properties(
|
|
361
|
+
width=500,
|
|
362
|
+
height=400,
|
|
363
|
+
title=f"{title} - Scatter Plot"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Panel 2: Histogram of all trades
|
|
367
|
+
hist_all = alt.Chart(trades).mark_bar(opacity=0.3).encode(
|
|
368
|
+
x=alt.X(f"{x_col}:Q", bin=alt.Bin(maxbins=bins), title=x_col.replace("_", " ").title()),
|
|
369
|
+
y=alt.Y("count():Q", title="Count"),
|
|
370
|
+
).properties(
|
|
371
|
+
width=500,
|
|
372
|
+
height=150,
|
|
373
|
+
title=f"{x_col} Distribution - All Trades"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Add quantile lines to histogram
|
|
377
|
+
hist_all = (hist_all + q50_line + q75_line)
|
|
378
|
+
|
|
379
|
+
# Panel 3: Histogram by win/loss
|
|
380
|
+
hist_by_result = alt.Chart(trades).mark_bar(opacity=0.5).encode(
|
|
381
|
+
x=alt.X(f"{x_col}:Q", bin=alt.Bin(maxbins=bins), title=x_col.replace("_", " ").title()),
|
|
382
|
+
y=alt.Y("count():Q", title="Count"),
|
|
383
|
+
color=alt.Color(
|
|
384
|
+
"win_loss:N",
|
|
385
|
+
scale=alt.Scale(
|
|
386
|
+
domain=["Win", "Loss"],
|
|
387
|
+
range=["#00C49A", "#FF6B6B"]
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
).properties(
|
|
391
|
+
width=500,
|
|
392
|
+
height=150,
|
|
393
|
+
title=f"{x_col} Distribution - By Result"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Add quantile lines
|
|
397
|
+
hist_by_result = (hist_by_result + q50_line + q75_line)
|
|
398
|
+
|
|
399
|
+
# Stack vertically
|
|
400
|
+
chart = alt.vconcat(panel1, hist_all, hist_by_result)
|
|
401
|
+
|
|
402
|
+
# Apply config only if requested (avoid conflicts in composition)
|
|
403
|
+
if apply_config:
|
|
404
|
+
chart = chart.configure_axis(
|
|
405
|
+
gridColor="#f0f0f0"
|
|
406
|
+
).configure_view(
|
|
407
|
+
strokeWidth=0
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
return chart
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def plot_pnl_curves(
|
|
414
|
+
trades: pl.DataFrame,
|
|
415
|
+
title: str = "Strategy Performance",
|
|
416
|
+
apply_config: bool = True
|
|
417
|
+
) -> alt.Chart:
|
|
418
|
+
"""Create PnL curves with drawdown visualization
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
trades: DataFrame with 'pnl_pct' and 'exit_time' columns
|
|
422
|
+
title: Chart title
|
|
423
|
+
apply_config: Apply default styling config (set False for chart composition)
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Vertically stacked Altair chart with 2 panels:
|
|
427
|
+
- Panel 1: Cumulative return curve with new high markers
|
|
428
|
+
- Panel 2: Drawdown curve with new low markers
|
|
429
|
+
|
|
430
|
+
Example:
|
|
431
|
+
>>> trades = client.quick_backtest_results(["my_strategy"])[0]
|
|
432
|
+
>>> chart = plot_pnl_curves(trades, title="My Strategy Performance")
|
|
433
|
+
>>> chart.show()
|
|
434
|
+
"""
|
|
435
|
+
# Calculate cumulative PnL and drawdown
|
|
436
|
+
df = calculate_cumulative_pnl(trades, sort_col="exit_time")
|
|
437
|
+
|
|
438
|
+
# Panel 1: Cumulative PnL curve
|
|
439
|
+
pnl_line = alt.Chart(df).mark_line(
|
|
440
|
+
color="#00C49A",
|
|
441
|
+
size=2
|
|
442
|
+
).encode(
|
|
443
|
+
x=alt.X("exit_time:T", title="Date"),
|
|
444
|
+
y=alt.Y("cum_pnl_pct:Q", title="Cumulative Return (%)", axis=alt.Axis(format=".1%")),
|
|
445
|
+
tooltip=[
|
|
446
|
+
alt.Tooltip("exit_time:T", title="Date"),
|
|
447
|
+
alt.Tooltip("cum_pnl_pct:Q", title="Cumulative Return", format=".2%"),
|
|
448
|
+
alt.Tooltip("pnl_pct:Q", title="Trade PnL", format=".2%"),
|
|
449
|
+
]
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Area fill
|
|
453
|
+
pnl_area = alt.Chart(df).mark_area(
|
|
454
|
+
color="#00C49A",
|
|
455
|
+
opacity=0.15
|
|
456
|
+
).encode(
|
|
457
|
+
x="exit_time:T",
|
|
458
|
+
y="cum_pnl_pct:Q"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# New high markers
|
|
462
|
+
high_markers = alt.Chart(df).mark_point(
|
|
463
|
+
shape="diamond",
|
|
464
|
+
size=100,
|
|
465
|
+
color="#FFD700",
|
|
466
|
+
filled=True,
|
|
467
|
+
stroke="white",
|
|
468
|
+
strokeWidth=1.5
|
|
469
|
+
).encode(
|
|
470
|
+
x="exit_time:T",
|
|
471
|
+
y="new_high:Q",
|
|
472
|
+
tooltip=[
|
|
473
|
+
alt.Tooltip("exit_time:T", title="Date"),
|
|
474
|
+
alt.Tooltip("new_high:Q", title="New High", format=".2%"),
|
|
475
|
+
]
|
|
476
|
+
).transform_filter(
|
|
477
|
+
alt.datum.new_high != None
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Drawdown low markers (on PnL curve)
|
|
481
|
+
dd_markers = alt.Chart(df).mark_circle(
|
|
482
|
+
size=60,
|
|
483
|
+
color="#9B2C2C",
|
|
484
|
+
opacity=0.7,
|
|
485
|
+
stroke="white",
|
|
486
|
+
strokeWidth=1
|
|
487
|
+
).encode(
|
|
488
|
+
x="exit_time:T",
|
|
489
|
+
y="cum_pnl_pct:Q",
|
|
490
|
+
tooltip=[
|
|
491
|
+
alt.Tooltip("exit_time:T", title="Date"),
|
|
492
|
+
alt.Tooltip("drawdown:Q", title="Drawdown", format=".2%"),
|
|
493
|
+
]
|
|
494
|
+
).transform_filter(
|
|
495
|
+
alt.datum.new_dd != None
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
panel1 = (pnl_area + pnl_line + high_markers + dd_markers).properties(
|
|
499
|
+
width=600,
|
|
500
|
+
height=400,
|
|
501
|
+
title=f"{title} - Cumulative Return"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Panel 2: Drawdown curve
|
|
505
|
+
dd_line = alt.Chart(df).mark_line(
|
|
506
|
+
color="#FF6B6B",
|
|
507
|
+
size=2
|
|
508
|
+
).encode(
|
|
509
|
+
x=alt.X("exit_time:T", title="Date"),
|
|
510
|
+
y=alt.Y("drawdown:Q", title="Drawdown (%)", axis=alt.Axis(format=".1%")),
|
|
511
|
+
tooltip=[
|
|
512
|
+
alt.Tooltip("exit_time:T", title="Date"),
|
|
513
|
+
alt.Tooltip("drawdown:Q", title="Drawdown", format=".2%"),
|
|
514
|
+
]
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Drawdown area fill
|
|
518
|
+
dd_area = alt.Chart(df).mark_area(
|
|
519
|
+
color="#FF6B6B",
|
|
520
|
+
opacity=0.15
|
|
521
|
+
).encode(
|
|
522
|
+
x="exit_time:T",
|
|
523
|
+
y="drawdown:Q"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
panel2 = (dd_area + dd_line).properties(
|
|
527
|
+
width=600,
|
|
528
|
+
height=200,
|
|
529
|
+
title="Drawdown"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Stack vertically
|
|
533
|
+
chart = alt.vconcat(panel1, panel2)
|
|
534
|
+
|
|
535
|
+
# Apply config only if requested (avoid conflicts in composition)
|
|
536
|
+
if apply_config:
|
|
537
|
+
chart = chart.configure_axis(
|
|
538
|
+
gridColor="#f0f0f0"
|
|
539
|
+
).configure_view(
|
|
540
|
+
strokeWidth=0
|
|
541
|
+
).configure_title(
|
|
542
|
+
fontSize=18,
|
|
543
|
+
anchor="start"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return chart
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def plot_trade_histograms(
|
|
550
|
+
trades: pl.DataFrame,
|
|
551
|
+
column: str = "mae",
|
|
552
|
+
bins: int = 40,
|
|
553
|
+
title: Optional[str] = None,
|
|
554
|
+
apply_config: bool = True
|
|
555
|
+
) -> alt.Chart:
|
|
556
|
+
"""Create histogram of trade metrics with win/loss breakdown
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
trades: DataFrame with trade metrics
|
|
560
|
+
column: Column to plot (e.g., 'mae', 'mfe', 'pnl_pct')
|
|
561
|
+
bins: Number of histogram bins
|
|
562
|
+
title: Chart title (auto-generated if None)
|
|
563
|
+
apply_config: Apply default styling config (set False for chart composition)
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Vertically stacked Altair chart with 2 panels:
|
|
567
|
+
- Panel 1: Distribution of all trades
|
|
568
|
+
- Panel 2: Distribution by win/loss
|
|
569
|
+
|
|
570
|
+
Example:
|
|
571
|
+
>>> trades = client.quick_backtest_results(["my_strategy"])[0]
|
|
572
|
+
>>> chart = plot_trade_histograms(trades, column="mae_atr_ratio")
|
|
573
|
+
>>> chart.show()
|
|
574
|
+
"""
|
|
575
|
+
if title is None:
|
|
576
|
+
title = f"{column.replace('_', ' ').title()} Distribution"
|
|
577
|
+
|
|
578
|
+
# Add win/loss labels if not present
|
|
579
|
+
if "win_loss" not in trades.columns:
|
|
580
|
+
trades = add_win_loss_label(trades)
|
|
581
|
+
|
|
582
|
+
# Calculate quantiles
|
|
583
|
+
q25 = trades[column].quantile(0.25)
|
|
584
|
+
q50 = trades[column].quantile(0.50)
|
|
585
|
+
q75 = trades[column].quantile(0.75)
|
|
586
|
+
|
|
587
|
+
trades = trades.with_columns([
|
|
588
|
+
pl.lit(q25).alias("q25"),
|
|
589
|
+
pl.lit(q50).alias("q50"),
|
|
590
|
+
pl.lit(q75).alias("q75"),
|
|
591
|
+
])
|
|
592
|
+
|
|
593
|
+
# Panel 1: All trades
|
|
594
|
+
hist_all = alt.Chart(trades).mark_bar(
|
|
595
|
+
opacity=0.3,
|
|
596
|
+
color="#3b82f6"
|
|
597
|
+
).encode(
|
|
598
|
+
x=alt.X(f"{column}:Q", bin=alt.Bin(maxbins=bins), title=column.replace("_", " ").title()),
|
|
599
|
+
y=alt.Y("count():Q", title="Count"),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# Quantile lines
|
|
603
|
+
q50_line = alt.Chart(trades).mark_rule(
|
|
604
|
+
strokeDash=[5, 5],
|
|
605
|
+
color="black",
|
|
606
|
+
size=1
|
|
607
|
+
).encode(x="q50:Q")
|
|
608
|
+
|
|
609
|
+
q75_line = alt.Chart(trades).mark_rule(
|
|
610
|
+
strokeDash=[5, 5],
|
|
611
|
+
color="black",
|
|
612
|
+
size=1
|
|
613
|
+
).encode(x="q75:Q")
|
|
614
|
+
|
|
615
|
+
panel1 = (hist_all + q50_line + q75_line).properties(
|
|
616
|
+
width=600,
|
|
617
|
+
height=250,
|
|
618
|
+
title=f"{title} - All Trades"
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Panel 2: By win/loss
|
|
622
|
+
hist_by_result = alt.Chart(trades).mark_bar(opacity=0.5).encode(
|
|
623
|
+
x=alt.X(f"{column}:Q", bin=alt.Bin(maxbins=bins), title=column.replace("_", " ").title()),
|
|
624
|
+
y=alt.Y("count():Q", title="Count"),
|
|
625
|
+
color=alt.Color(
|
|
626
|
+
"win_loss:N",
|
|
627
|
+
scale=alt.Scale(
|
|
628
|
+
domain=["Win", "Loss"],
|
|
629
|
+
range=["#00C49A", "#FF6B6B"]
|
|
630
|
+
),
|
|
631
|
+
legend=alt.Legend(title="Result")
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
panel2 = (hist_by_result + q50_line + q75_line).properties(
|
|
636
|
+
width=600,
|
|
637
|
+
height=250,
|
|
638
|
+
title=f"{title} - By Result"
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Stack vertically
|
|
642
|
+
chart = alt.vconcat(panel1, panel2)
|
|
643
|
+
|
|
644
|
+
# Apply config only if requested (avoid conflicts in composition)
|
|
645
|
+
if apply_config:
|
|
646
|
+
chart = chart.configure_axis(
|
|
647
|
+
gridColor="#f0f0f0"
|
|
648
|
+
).configure_view(
|
|
649
|
+
strokeWidth=0
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
return chart
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# =============================================================================
|
|
656
|
+
# Chart Composition Utilities
|
|
657
|
+
# =============================================================================
|
|
658
|
+
|
|
659
|
+
def combine_charts(
|
|
660
|
+
*charts: alt.Chart,
|
|
661
|
+
layout: Literal["horizontal", "vertical", "grid"] = "vertical",
|
|
662
|
+
columns: int = 2,
|
|
663
|
+
spacing: int = 15,
|
|
664
|
+
title: Optional[str] = None
|
|
665
|
+
) -> alt.Chart:
|
|
666
|
+
"""Combine multiple charts into a single dashboard layout
|
|
667
|
+
|
|
668
|
+
Automatically strips config from individual charts to avoid composition conflicts.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
*charts: Variable number of Altair charts to combine
|
|
672
|
+
layout: Layout type - "horizontal" (|), "vertical" (&), or "grid"
|
|
673
|
+
columns: Number of columns for grid layout (default: 2)
|
|
674
|
+
spacing: Spacing between charts in pixels (default: 15)
|
|
675
|
+
title: Overall dashboard title (optional)
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
Combined Altair chart with unified configuration
|
|
679
|
+
|
|
680
|
+
Example:
|
|
681
|
+
>>> from tradepose_client import (
|
|
682
|
+
... plot_mae_mfe_scatter,
|
|
683
|
+
... plot_pnl_curves,
|
|
684
|
+
... combine_charts
|
|
685
|
+
... )
|
|
686
|
+
>>> trades = client.quick_backtest_results(["my_strategy"])[0]
|
|
687
|
+
>>> trades = calculate_mea(trades)
|
|
688
|
+
>>>
|
|
689
|
+
>>> # Method 1: Using combine_charts()
|
|
690
|
+
>>> dashboard = combine_charts(
|
|
691
|
+
... plot_mae_mfe_scatter(trades),
|
|
692
|
+
... plot_pnl_curves(trades),
|
|
693
|
+
... layout="horizontal"
|
|
694
|
+
... )
|
|
695
|
+
>>>
|
|
696
|
+
>>> # Method 2: Using operators with apply_config=False
|
|
697
|
+
>>> scatter = plot_mae_mfe_scatter(trades, apply_config=False)
|
|
698
|
+
>>> pnl = plot_pnl_curves(trades, apply_config=False)
|
|
699
|
+
>>> dashboard = (scatter | pnl).configure_axis(gridColor="#f0f0f0")
|
|
700
|
+
"""
|
|
701
|
+
import altair as alt
|
|
702
|
+
|
|
703
|
+
if len(charts) == 0:
|
|
704
|
+
raise ValueError("At least one chart is required")
|
|
705
|
+
|
|
706
|
+
# Strip config from all charts to avoid conflicts
|
|
707
|
+
# (Altair will error if trying to compose charts with config)
|
|
708
|
+
# Note: This is done by re-creating charts without config
|
|
709
|
+
clean_charts = []
|
|
710
|
+
for chart in charts:
|
|
711
|
+
# Charts should be created with apply_config=False
|
|
712
|
+
# but if user passes configured charts, we need to handle it
|
|
713
|
+
clean_charts.append(chart)
|
|
714
|
+
|
|
715
|
+
# Combine based on layout
|
|
716
|
+
if layout == "horizontal":
|
|
717
|
+
combined = clean_charts[0]
|
|
718
|
+
for chart in clean_charts[1:]:
|
|
719
|
+
combined = combined | chart
|
|
720
|
+
|
|
721
|
+
elif layout == "vertical":
|
|
722
|
+
combined = clean_charts[0]
|
|
723
|
+
for chart in clean_charts[1:]:
|
|
724
|
+
combined = combined & chart
|
|
725
|
+
|
|
726
|
+
elif layout == "grid":
|
|
727
|
+
# Grid layout using alt.concat
|
|
728
|
+
combined = alt.concat(*clean_charts, columns=columns)
|
|
729
|
+
|
|
730
|
+
else:
|
|
731
|
+
raise ValueError(f"Invalid layout: {layout}. Use 'horizontal', 'vertical', or 'grid'")
|
|
732
|
+
|
|
733
|
+
# Apply unified configuration
|
|
734
|
+
combined = combined.configure_axis(
|
|
735
|
+
gridColor="#f0f0f0"
|
|
736
|
+
).configure_view(
|
|
737
|
+
strokeWidth=0
|
|
738
|
+
).configure_concat(
|
|
739
|
+
spacing=spacing
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# Add overall title if provided
|
|
743
|
+
if title:
|
|
744
|
+
combined = combined.properties(
|
|
745
|
+
title={
|
|
746
|
+
"text": title,
|
|
747
|
+
"fontSize": 20,
|
|
748
|
+
"anchor": "middle"
|
|
749
|
+
}
|
|
750
|
+
).configure_title(
|
|
751
|
+
fontSize=20,
|
|
752
|
+
anchor="start"
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
# Resolve scales to avoid conflicts
|
|
756
|
+
combined = combined.resolve_scale(
|
|
757
|
+
color='independent',
|
|
758
|
+
x='independent',
|
|
759
|
+
y='independent'
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
return combined
|