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
|
@@ -0,0 +1,1836 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic 模型用於策略配置 (V2 - 自動轉換版本)
|
|
3
|
+
|
|
4
|
+
自動處理 Polars Expr 的雙向轉換:
|
|
5
|
+
- 讀取時:自動將 JSON/dict 轉為 pl.Expr
|
|
6
|
+
- 寫入時:可直接使用 pl.col() 等,自動序列化
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from io import StringIO
|
|
12
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
13
|
+
|
|
14
|
+
import polars as pl
|
|
15
|
+
from pydantic import BaseModel, Field, field_serializer, field_validator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Freq(str, Enum):
|
|
19
|
+
"""時間頻率枚舉(與 Rust Freq enum 一致)
|
|
20
|
+
|
|
21
|
+
對應 Rust 的 Freq enum
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
MIN_1 = "1min"
|
|
25
|
+
MIN_5 = "5min"
|
|
26
|
+
MIN_15 = "15min"
|
|
27
|
+
MIN_30 = "30min"
|
|
28
|
+
HOUR_1 = "1h"
|
|
29
|
+
HOUR_4 = "4h"
|
|
30
|
+
DAY_1 = "1D"
|
|
31
|
+
WEEK_1 = "1W"
|
|
32
|
+
MONTH_1 = "1M"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OrderStrategy(str, Enum):
|
|
36
|
+
"""訂單策略枚舉(與 Rust OrderStrategy enum 一致)
|
|
37
|
+
|
|
38
|
+
對應 Rust 的 OrderStrategy enum,使用字符串值以便 JSON 序列化
|
|
39
|
+
|
|
40
|
+
Rust 對應關係:
|
|
41
|
+
- Rust: OrderStrategy::ImmediateEntry → Python: OrderStrategy.IMMEDIATE_ENTRY (u32: 0)
|
|
42
|
+
- Rust: OrderStrategy::FavorableDelayEntry → Python: OrderStrategy.FAVORABLE_DELAY_ENTRY (u32: 1)
|
|
43
|
+
- Rust: OrderStrategy::AdverseDelayEntry → Python: OrderStrategy.ADVERSE_DELAY_ENTRY (u32: 2)
|
|
44
|
+
- Rust: OrderStrategy::ImmediateExit → Python: OrderStrategy.IMMEDIATE_EXIT (u32: 3)
|
|
45
|
+
- Rust: OrderStrategy::StopLoss → Python: OrderStrategy.STOP_LOSS (u32: 4)
|
|
46
|
+
- Rust: OrderStrategy::TakeProfit → Python: OrderStrategy.TAKE_PROFIT (u32: 5)
|
|
47
|
+
- Rust: OrderStrategy::TrailingStop → Python: OrderStrategy.TRAILING_STOP (u32: 6)
|
|
48
|
+
- Rust: OrderStrategy::Breakeven → Python: OrderStrategy.BREAKEVEN (u32: 7)
|
|
49
|
+
- Rust: OrderStrategy::TimeoutExit → Python: OrderStrategy.TIMEOUT_EXIT (u32: 8)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
IMMEDIATE_ENTRY = "ImmediateEntry"
|
|
53
|
+
FAVORABLE_DELAY_ENTRY = "FavorableDelayEntry"
|
|
54
|
+
ADVERSE_DELAY_ENTRY = "AdverseDelayEntry"
|
|
55
|
+
IMMEDIATE_EXIT = "ImmediateExit"
|
|
56
|
+
STOP_LOSS = "StopLoss"
|
|
57
|
+
TAKE_PROFIT = "TakeProfit"
|
|
58
|
+
TRAILING_STOP = "TrailingStop"
|
|
59
|
+
BREAKEVEN = "Breakeven"
|
|
60
|
+
TIMEOUT_EXIT = "TimeoutExit"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Weekday(str, Enum):
|
|
64
|
+
"""星期列舉(與 Rust Weekday enum 一致)
|
|
65
|
+
|
|
66
|
+
用於 Market Profile 的 WeeklyTime 配置
|
|
67
|
+
|
|
68
|
+
對應關係:
|
|
69
|
+
- 0 (週一) → "Mon"
|
|
70
|
+
- 1 (週二) → "Tue"
|
|
71
|
+
- 2 (週三) → "Wed"
|
|
72
|
+
- 3 (週四) → "Thu"
|
|
73
|
+
- 4 (週五) → "Fri"
|
|
74
|
+
- 5 (週六) → "Sat"
|
|
75
|
+
- 6 (週日) → "Sun"
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
MON = "Mon"
|
|
79
|
+
TUE = "Tue"
|
|
80
|
+
WED = "Wed"
|
|
81
|
+
THU = "Thu"
|
|
82
|
+
FRI = "Fri"
|
|
83
|
+
SAT = "Sat"
|
|
84
|
+
SUN = "Sun"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PolarsExprField:
|
|
88
|
+
"""自定義 Polars Expr 字段處理"""
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def serialize(expr: pl.Expr) -> dict:
|
|
92
|
+
"""序列化 Polars Expr 為 dict(與服務器格式一致)"""
|
|
93
|
+
json_str = expr.meta.serialize(format="json")
|
|
94
|
+
return json.loads(json_str)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def deserialize(data: Union[str, dict, pl.Expr]) -> pl.Expr:
|
|
98
|
+
"""反序列化為 Polars Expr"""
|
|
99
|
+
if isinstance(data, pl.Expr):
|
|
100
|
+
# 已經是 Expr 對象
|
|
101
|
+
return data
|
|
102
|
+
elif isinstance(data, dict):
|
|
103
|
+
# API 返回的 dict(優先處理)
|
|
104
|
+
json_str = json.dumps(data)
|
|
105
|
+
return pl.Expr.deserialize(StringIO(json_str), format="json")
|
|
106
|
+
elif isinstance(data, str):
|
|
107
|
+
# JSON 字符串(向後兼容)
|
|
108
|
+
return pl.Expr.deserialize(StringIO(data), format="json")
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError(f"無法反序列化類型: {type(data)}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ============================================================================
|
|
114
|
+
# Indicator 輔助類(用於指標配置)
|
|
115
|
+
# ============================================================================
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AtrQuantileConfig(BaseModel):
|
|
119
|
+
"""ATR 分位數配置(用於 ATR 指標)
|
|
120
|
+
|
|
121
|
+
對應 Rust: AtrQuantileConfig struct
|
|
122
|
+
|
|
123
|
+
用於計算 ATR 的滾動分位數,可用於動態止損等場景
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> config = AtrQuantileConfig(window=20, quantile=0.5) # 20 週期中位數
|
|
127
|
+
>>> config = AtrQuantileConfig(window=20, quantile=0.75) # 20 週期 75% 分位數
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
window: int = Field(..., gt=0, description="滾動窗口大小(必須 > 0)")
|
|
131
|
+
quantile: float = Field(
|
|
132
|
+
..., gt=0, lt=1, description="分位數值,範圍 (0, 1),例如 0.5 表示中位數"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ProfileShapeConfig(BaseModel):
|
|
137
|
+
"""Profile 形狀識別配置(用於 MarketProfile 指標)
|
|
138
|
+
|
|
139
|
+
對應 Rust: ProfileShapeConfig struct
|
|
140
|
+
|
|
141
|
+
控制 Market Profile 形狀識別算法的閾值參數(進階功能,通常使用預設值即可)
|
|
142
|
+
|
|
143
|
+
Profile 形狀類型:
|
|
144
|
+
- "p_shaped": P型(快速上漲 + 高位盤整,看空信號)
|
|
145
|
+
- "b_shaped": b型(快速下跌 + 低位盤整,看多信號)
|
|
146
|
+
- "b_double_distribution": B型雙峰(區間交易)
|
|
147
|
+
- "trend_day": 趨勢日(持續單向移動,順勢交易)
|
|
148
|
+
- "normal": 正態分布(對稱分布,區間交易)
|
|
149
|
+
- "undefined": 無法分類
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> # 使用預設值(推薦)
|
|
153
|
+
>>> config = ProfileShapeConfig()
|
|
154
|
+
>>>
|
|
155
|
+
>>> # 自訂閾值(進階)
|
|
156
|
+
>>> config = ProfileShapeConfig(
|
|
157
|
+
... trend_monotonic_threshold=0.65,
|
|
158
|
+
... pshape_concentration_threshold=0.65
|
|
159
|
+
... )
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
early_period_ratio: float = Field(
|
|
163
|
+
0.15, gt=0, le=1, description="早期時段比例(前 15% 視為早期)"
|
|
164
|
+
)
|
|
165
|
+
late_period_ratio: float = Field(
|
|
166
|
+
0.15, gt=0, le=1, description="晚期時段比例(後 15% 視為晚期)"
|
|
167
|
+
)
|
|
168
|
+
trend_ib_max_ratio: float = Field(
|
|
169
|
+
0.20, gt=0, le=1, description="趨勢日 IB 最大占比"
|
|
170
|
+
)
|
|
171
|
+
trend_monotonic_threshold: float = Field(
|
|
172
|
+
0.60, gt=0, le=1, description="趨勢日單向移動閾值"
|
|
173
|
+
)
|
|
174
|
+
trend_imbalance_threshold: float = Field(
|
|
175
|
+
0.70, gt=0, le=1, description="趨勢日早期/晚期 TPO 不平衡度閾值"
|
|
176
|
+
)
|
|
177
|
+
pshape_concentration_threshold: float = Field(
|
|
178
|
+
0.60, gt=0, le=1, description="P型/b型 TPO 集中度閾值"
|
|
179
|
+
)
|
|
180
|
+
bshape_valley_threshold: float = Field(
|
|
181
|
+
0.70, gt=0, le=1, description="B型雙峰之間谷的深度閾值"
|
|
182
|
+
)
|
|
183
|
+
normal_symmetry_threshold: float = Field(
|
|
184
|
+
0.30, gt=0, le=1, description="Normal 型對稱性閾值"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ============================================================================
|
|
189
|
+
# Indicator 工廠類(對應 Rust Indicator enum 的各個變體)
|
|
190
|
+
# ============================================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class Indicator:
|
|
194
|
+
"""指標工廠類(對應 Rust 的 Indicator enum)
|
|
195
|
+
|
|
196
|
+
使用靜態方法創建各種指標配置,語法類似 Rust 的關聯函數
|
|
197
|
+
|
|
198
|
+
支援的指標類型(15 種):
|
|
199
|
+
- 移動平均: SMA, EMA, SMMA, WMA
|
|
200
|
+
- 波動率: ATR, AtrQuantile
|
|
201
|
+
- 通道指標: BollingerBands
|
|
202
|
+
- 趨勢指標: SuperTrend, ADX, MACD
|
|
203
|
+
- 動量指標: RSI, CCI, Stochastic
|
|
204
|
+
- 價格分布: MarketProfile
|
|
205
|
+
- 原始數據: RawOhlcv
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
>>> from strategy_models import Indicator
|
|
209
|
+
>>>
|
|
210
|
+
>>> # 移動平均
|
|
211
|
+
>>> sma = Indicator.sma(period=20)
|
|
212
|
+
>>> sma_on_high = Indicator.sma(period=20, column="high")
|
|
213
|
+
>>> ema = Indicator.ema(period=12)
|
|
214
|
+
>>>
|
|
215
|
+
>>> # 波動率
|
|
216
|
+
>>> atr = Indicator.atr(period=14)
|
|
217
|
+
>>> atr_q = Indicator.atr_quantile("ATR|14", window=20, quantile=0.75)
|
|
218
|
+
>>>
|
|
219
|
+
>>> # 通道指標
|
|
220
|
+
>>> bb = Indicator.bollinger_bands(period=20, num_std=2.0)
|
|
221
|
+
>>>
|
|
222
|
+
>>> # 趨勢指標
|
|
223
|
+
>>> st = Indicator.supertrend(multiplier=3.0, volatility_column="ATR|14")
|
|
224
|
+
>>> macd = Indicator.macd(fast_period=12, slow_period=26, signal_period=9)
|
|
225
|
+
>>> adx = Indicator.adx(period=14)
|
|
226
|
+
>>>
|
|
227
|
+
>>> # 動量指標
|
|
228
|
+
>>> rsi = Indicator.rsi(period=14)
|
|
229
|
+
>>> cci = Indicator.cci(period=20)
|
|
230
|
+
>>> stoch = Indicator.stochastic(k_period=14, d_period=3)
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def sma(period: int, column: str = "close") -> Dict[str, Any]:
|
|
235
|
+
"""創建 SMA 指標配置
|
|
236
|
+
|
|
237
|
+
對應 Rust: Indicator::SMA { period, column }
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
period: 週期
|
|
241
|
+
column: 計算欄位(預設 "close")
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
SMA 指標配置 dict
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
>>> Indicator.sma(period=20)
|
|
248
|
+
>>> Indicator.sma(period=20, column="high")
|
|
249
|
+
"""
|
|
250
|
+
return {"type": "SMA", "period": period, "column": column}
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def ema(period: int, column: str = "close") -> Dict[str, Any]:
|
|
254
|
+
"""創建 EMA 指標配置
|
|
255
|
+
|
|
256
|
+
對應 Rust: Indicator::EMA { period, column }
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
period: 週期
|
|
260
|
+
column: 計算欄位(預設 "close")
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
EMA 指標配置 dict
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> Indicator.ema(period=12)
|
|
267
|
+
>>> Indicator.ema(period=12, column="low")
|
|
268
|
+
"""
|
|
269
|
+
return {"type": "EMA", "period": period, "column": column}
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def smma(period: int, column: str = "close") -> Dict[str, Any]:
|
|
273
|
+
"""創建 SMMA 指標配置
|
|
274
|
+
|
|
275
|
+
對應 Rust: Indicator::SMMA { period, column }
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
period: 週期
|
|
279
|
+
column: 計算欄位(預設 "close")
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
SMMA 指標配置 dict
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
>>> Indicator.smma(period=14)
|
|
286
|
+
"""
|
|
287
|
+
return {"type": "SMMA", "period": period, "column": column}
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def wma(period: int, column: str = "close") -> Dict[str, Any]:
|
|
291
|
+
"""創建 WMA 指標配置
|
|
292
|
+
|
|
293
|
+
對應 Rust: Indicator::WMA { period, column }
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
period: 週期
|
|
297
|
+
column: 計算欄位(預設 "close")
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
WMA 指標配置 dict
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
>>> Indicator.wma(period=10)
|
|
304
|
+
"""
|
|
305
|
+
return {"type": "WMA", "period": period, "column": column}
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def atr(period: int) -> Dict[str, Any]:
|
|
309
|
+
"""創建 ATR 指標配置
|
|
310
|
+
|
|
311
|
+
對應 Rust: Indicator::ATR { period }
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
period: 週期(必須 > 0)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
ATR 指標配置 dict
|
|
318
|
+
|
|
319
|
+
Note:
|
|
320
|
+
ATR 輸出為**單一數值欄位**,列名格式為 `ATR|{period}`(例如 `ATR|14`)
|
|
321
|
+
|
|
322
|
+
如需計算 ATR 的滾動分位數,請使用 `Indicator.atr_quantile()` 獨立指標
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
>>> # 基礎 ATR(返回單一數值欄位)
|
|
326
|
+
>>> Indicator.atr(period=14)
|
|
327
|
+
>>> # 輸出列名: "ATR|14"
|
|
328
|
+
>>>
|
|
329
|
+
>>> # 在 Python 中訪問
|
|
330
|
+
>>> import polars as pl
|
|
331
|
+
>>> df = pl.read_parquet("enhanced_ohlcv.parquet")
|
|
332
|
+
>>> atr_values = df["ATR|14"] # 直接訪問數值欄位
|
|
333
|
+
>>>
|
|
334
|
+
>>> # 計算止損
|
|
335
|
+
>>> stop_loss = df.with_columns([
|
|
336
|
+
... (pl.col("close") - 2 * pl.col("ATR|14")).alias("stop_loss")
|
|
337
|
+
... ])
|
|
338
|
+
"""
|
|
339
|
+
return {"type": "ATR", "period": period}
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def atr_quantile(
|
|
343
|
+
atr_column: str,
|
|
344
|
+
window: int,
|
|
345
|
+
quantile: float,
|
|
346
|
+
) -> Dict[str, Any]:
|
|
347
|
+
"""創建 AtrQuantile 指標配置
|
|
348
|
+
|
|
349
|
+
對應 Rust: Indicator::AtrQuantile { atr_column, window, quantile }
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
atr_column: ATR 欄位名稱(例如 "ATR|14")
|
|
353
|
+
window: 滾動窗口大小(必須 > 0)
|
|
354
|
+
quantile: 分位數值,範圍 (0, 1)
|
|
355
|
+
- 0.5: 中位數
|
|
356
|
+
- 0.75: 75% 分位數(較高波動率)
|
|
357
|
+
- 0.25: 25% 分位數(較低波動率)
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
AtrQuantile 指標配置 dict
|
|
361
|
+
|
|
362
|
+
Note:
|
|
363
|
+
- AtrQuantile 輸出為**單一數值欄位**(f64)
|
|
364
|
+
- 列名格式:`ATRQ|{atr_column}_Q{quantile*100}_{window}`
|
|
365
|
+
- **重要**:AtrQuantile 依賴 ATR,必須先定義 ATR 指標
|
|
366
|
+
|
|
367
|
+
Example:
|
|
368
|
+
>>> # 基礎用法:計算 ATR|14 的 50% 分位數(20 窗口)
|
|
369
|
+
>>> Indicator.atr_quantile(
|
|
370
|
+
... atr_column="ATR|14",
|
|
371
|
+
... window=20,
|
|
372
|
+
... quantile=0.5
|
|
373
|
+
... )
|
|
374
|
+
>>> # 輸出列名: "ATRQ|ATR|14_Q50_20"
|
|
375
|
+
>>>
|
|
376
|
+
>>> # 在策略中使用(完整示例)
|
|
377
|
+
>>> from tradepose_client.models import (
|
|
378
|
+
... Indicator, create_indicator_spec, Freq
|
|
379
|
+
... )
|
|
380
|
+
>>>
|
|
381
|
+
>>> # 1. 先定義 ATR
|
|
382
|
+
>>> atr_spec = create_indicator_spec(
|
|
383
|
+
... freq=Freq.DAY_1,
|
|
384
|
+
... indicator=Indicator.atr(period=14),
|
|
385
|
+
... shift=1
|
|
386
|
+
... )
|
|
387
|
+
>>>
|
|
388
|
+
>>> # 2. 再定義 AtrQuantile(動態引用 ATR 列名)
|
|
389
|
+
>>> atr_q_spec = create_indicator_spec(
|
|
390
|
+
... freq=Freq.DAY_1,
|
|
391
|
+
... indicator=Indicator.atr_quantile(
|
|
392
|
+
... atr_column=atr_spec.short_name(), # 動態引用,自動與 ATR 同步
|
|
393
|
+
... window=20,
|
|
394
|
+
... quantile=0.75 # 75% 分位數
|
|
395
|
+
... ),
|
|
396
|
+
... shift=1
|
|
397
|
+
... )
|
|
398
|
+
>>>
|
|
399
|
+
>>> # 在 Python 中訪問(直接使用列名,無需 struct.field)
|
|
400
|
+
>>> import polars as pl
|
|
401
|
+
>>> df = pl.read_parquet("enhanced_ohlcv.parquet")
|
|
402
|
+
>>>
|
|
403
|
+
>>> # 動態止損示例:直接引用 AtrQuantile 列
|
|
404
|
+
>>> df = df.with_columns([
|
|
405
|
+
... (pl.col("close") - 2 * pl.col("1D_ATRQ|ATR|14_Q75_20")).alias("dynamic_stop_loss")
|
|
406
|
+
... ])
|
|
407
|
+
>>>
|
|
408
|
+
>>> # 或在策略條件中直接使用
|
|
409
|
+
>>> from tradepose_client.models import create_trigger
|
|
410
|
+
>>> trigger = create_trigger(
|
|
411
|
+
... name="low_volatility_entry",
|
|
412
|
+
... conditions=[
|
|
413
|
+
... pl.col("1D_ATR|14") < pl.col("1D_ATRQ|ATR|14_Q75_20") # ATR < Q75
|
|
414
|
+
... ],
|
|
415
|
+
... price_expr=pl.col("open"),
|
|
416
|
+
... order_strategy="ImmediateEntry"
|
|
417
|
+
... )
|
|
418
|
+
"""
|
|
419
|
+
return {
|
|
420
|
+
"type": "AtrQuantile",
|
|
421
|
+
"atr_column": atr_column,
|
|
422
|
+
"window": window,
|
|
423
|
+
"quantile": quantile,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def supertrend(
|
|
428
|
+
multiplier: float,
|
|
429
|
+
volatility_column: str,
|
|
430
|
+
high: str = "high",
|
|
431
|
+
low: str = "low",
|
|
432
|
+
close: str = "close",
|
|
433
|
+
fields: Optional[List[str]] = None,
|
|
434
|
+
) -> Dict[str, Any]:
|
|
435
|
+
"""創建 SuperTrend 指標配置
|
|
436
|
+
|
|
437
|
+
對應 Rust: Indicator::SuperTrend { multiplier, volatility_column, high, low, close, fields }
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
multiplier: 波動率倍數
|
|
441
|
+
volatility_column: 波動率欄位名稱(例如 "ATR|14")
|
|
442
|
+
high: 高點欄位(預設 "high")
|
|
443
|
+
low: 低點欄位(預設 "low")
|
|
444
|
+
close: 收盤價欄位(預設 "close")
|
|
445
|
+
fields: 保留欄位(可選,例如 ["direction", "supertrend", "long", "short"])
|
|
446
|
+
- 可用欄位: "direction", "supertrend", "long", "short"
|
|
447
|
+
- direction: 趨勢方向(1=多頭,-1=空頭)
|
|
448
|
+
- long: 多頭 SuperTrend 線(僅多頭時有值,空頭時為 null)
|
|
449
|
+
- short: 空頭 SuperTrend 線(僅空頭時有值,多頭時為 null)
|
|
450
|
+
- 省略或 None:返回所有欄位
|
|
451
|
+
- 空數組 []:返回所有欄位(與 None 等效)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
SuperTrend 指標配置 dict
|
|
455
|
+
|
|
456
|
+
Example:
|
|
457
|
+
>>> # 先定義 ATR 指標
|
|
458
|
+
>>> atr_spec = create_indicator_spec(
|
|
459
|
+
... freq=Freq.DAY_1,
|
|
460
|
+
... indicator=Indicator.atr(period=14),
|
|
461
|
+
... shift=1
|
|
462
|
+
... )
|
|
463
|
+
>>>
|
|
464
|
+
>>> # SuperTrend 動態引用 ATR 列名
|
|
465
|
+
>>> Indicator.supertrend(
|
|
466
|
+
... multiplier=3.0,
|
|
467
|
+
... volatility_column=atr_spec.short_name() # 動態引用 "ATR|14"
|
|
468
|
+
... )
|
|
469
|
+
>>>
|
|
470
|
+
>>> # 僅保留核心欄位
|
|
471
|
+
>>> Indicator.supertrend(
|
|
472
|
+
... multiplier=3.0,
|
|
473
|
+
... volatility_column=atr_spec.short_name(),
|
|
474
|
+
... fields=["direction", "supertrend"]
|
|
475
|
+
... )
|
|
476
|
+
"""
|
|
477
|
+
config = {
|
|
478
|
+
"type": "SuperTrend",
|
|
479
|
+
"multiplier": float(multiplier),
|
|
480
|
+
"volatility_column": volatility_column,
|
|
481
|
+
"high": high,
|
|
482
|
+
"low": low,
|
|
483
|
+
"close": close,
|
|
484
|
+
}
|
|
485
|
+
if fields is not None:
|
|
486
|
+
config["fields"] = fields
|
|
487
|
+
return config
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def market_profile(
|
|
491
|
+
anchor_config: Dict[str, Any],
|
|
492
|
+
tick_size: float,
|
|
493
|
+
value_area_pct: float = 0.7,
|
|
494
|
+
fields: Optional[List[str]] = None,
|
|
495
|
+
shape_config: Optional[Dict[str, Any]] = None,
|
|
496
|
+
) -> Dict[str, Any]:
|
|
497
|
+
"""創建 Market Profile 指標配置
|
|
498
|
+
|
|
499
|
+
對應 Rust: Indicator::MarketProfile { anchor_config, tick_size, ... }
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
anchor_config: Anchor 分段配置(使用 create_daily_anchor() 或 create_weekly_anchor())
|
|
503
|
+
- end_rule: 結束時間規則
|
|
504
|
+
- DailyTime: {"type": "DailyTime", "hour": int, "minute": int}
|
|
505
|
+
- WeeklyTime: {"type": "WeeklyTime", "weekday": str, "hour": int, "minute": int}
|
|
506
|
+
- start_rule: 開始時間規則(可選)
|
|
507
|
+
- lookback_days: 回溯天數
|
|
508
|
+
tick_size: 最小價格單位
|
|
509
|
+
value_area_pct: Value Area 百分比(預設 0.7)
|
|
510
|
+
fields: 保留欄位(可選,例如 ["poc", "vah", "val", "profile_shape"])
|
|
511
|
+
- 可用欄位: "poc", "vah", "val", "value_area", "tpo_distribution",
|
|
512
|
+
"segment_id", "profile_shape"
|
|
513
|
+
- 省略或 None:返回所有欄位
|
|
514
|
+
- 空數組 []:返回所有欄位(與 None 等效)
|
|
515
|
+
- segment_id 會自動包含(已 ffill,可用於窗口操作)
|
|
516
|
+
shape_config: Profile 形狀識別配置(可選,進階功能)
|
|
517
|
+
用於控制形狀識別算法的閾值參數,通常使用預設值即可
|
|
518
|
+
可用 ProfileShapeConfig 類創建
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
Market Profile 指標配置 dict
|
|
522
|
+
|
|
523
|
+
Note:
|
|
524
|
+
- shift 只存在於 IndicatorSpec 層級
|
|
525
|
+
- 系統固定使用 "ts" 和 "segment_id" 欄位名稱
|
|
526
|
+
- Market Profile 輸出為 struct 欄位,包含 TPO 價格分布、POC、VAH、VAL 等資訊
|
|
527
|
+
- segment_id 已 ffill,可直接用於 Polars 窗口函數
|
|
528
|
+
|
|
529
|
+
Example:
|
|
530
|
+
>>> # 每日 09:15 結束,回溯 1 天
|
|
531
|
+
>>> Indicator.market_profile(
|
|
532
|
+
... anchor_config=create_daily_anchor(9, 15, 1),
|
|
533
|
+
... tick_size=0.01
|
|
534
|
+
... )
|
|
535
|
+
>>>
|
|
536
|
+
>>> # 每週一 09:15 結束,回溯 5 天,僅保留 POC/VAH/VAL/形狀
|
|
537
|
+
>>> Indicator.market_profile(
|
|
538
|
+
... anchor_config=create_weekly_anchor("Mon", 9, 15, 5),
|
|
539
|
+
... tick_size=0.01,
|
|
540
|
+
... fields=["poc", "vah", "val", "profile_shape"]
|
|
541
|
+
... )
|
|
542
|
+
>>>
|
|
543
|
+
>>> # 自訂形狀識別閾值(進階)
|
|
544
|
+
>>> from tradepose_client.models import ProfileShapeConfig
|
|
545
|
+
>>> shape_cfg = ProfileShapeConfig(
|
|
546
|
+
... trend_monotonic_threshold=0.65,
|
|
547
|
+
... pshape_concentration_threshold=0.65
|
|
548
|
+
... )
|
|
549
|
+
>>> Indicator.market_profile(
|
|
550
|
+
... anchor_config=create_daily_anchor(9, 15, 1),
|
|
551
|
+
... tick_size=0.5,
|
|
552
|
+
... shape_config=shape_cfg.model_dump()
|
|
553
|
+
... )
|
|
554
|
+
"""
|
|
555
|
+
config = {
|
|
556
|
+
"type": "MarketProfile",
|
|
557
|
+
"anchor_config": anchor_config,
|
|
558
|
+
"tick_size": tick_size,
|
|
559
|
+
"value_area_pct": value_area_pct,
|
|
560
|
+
}
|
|
561
|
+
if fields is not None:
|
|
562
|
+
config["fields"] = fields
|
|
563
|
+
if shape_config is not None:
|
|
564
|
+
config["shape_config"] = shape_config
|
|
565
|
+
return config
|
|
566
|
+
|
|
567
|
+
@staticmethod
|
|
568
|
+
def cci(period: int = 20) -> Dict[str, Any]:
|
|
569
|
+
"""創建 CCI 指標配置
|
|
570
|
+
|
|
571
|
+
對應 Rust: Indicator::CCI { period }
|
|
572
|
+
|
|
573
|
+
CCI (Commodity Channel Index) 由 Donald Lambert 開發,用於衡量價格與統計平均值的偏離程度,
|
|
574
|
+
常用於識別超買/超賣狀態。
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
period: 週期(必須 > 0,預設 20)
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
CCI 指標配置 dict
|
|
581
|
+
|
|
582
|
+
Note:
|
|
583
|
+
- CCI 輸出為**單一 f64 欄位**,值域無限制(通常在 -300 到 +300 之間)
|
|
584
|
+
- 列名格式為 `CCI|{period}`(例如 `CCI|20`)
|
|
585
|
+
- 解讀:> +100 為超買,< -100 為超賣,-100~+100 為中性區間
|
|
586
|
+
|
|
587
|
+
Example:
|
|
588
|
+
>>> # 使用預設參數(20 週期)
|
|
589
|
+
>>> Indicator.cci(period=20)
|
|
590
|
+
>>> # 輸出列名: "CCI|20"
|
|
591
|
+
>>>
|
|
592
|
+
>>> # 在策略中使用
|
|
593
|
+
>>> cci_spec = create_indicator_spec(
|
|
594
|
+
... freq=Freq.HOUR_1,
|
|
595
|
+
... indicator=Indicator.cci(period=14),
|
|
596
|
+
... shift=1
|
|
597
|
+
... )
|
|
598
|
+
>>> # 列名: "1h_CCI|14"
|
|
599
|
+
>>>
|
|
600
|
+
>>> # 超買/超賣條件
|
|
601
|
+
>>> overbought = pl.col("1h_CCI|14") > 100
|
|
602
|
+
>>> oversold = pl.col("1h_CCI|14") < -100
|
|
603
|
+
"""
|
|
604
|
+
return {"type": "CCI", "period": period}
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def rsi(period: int = 14, column: str = "close") -> Dict[str, Any]:
|
|
608
|
+
"""創建 RSI 指標配置
|
|
609
|
+
|
|
610
|
+
對應 Rust: Indicator::RSI { period, column }
|
|
611
|
+
|
|
612
|
+
RSI (Relative Strength Index) 由 J. Welles Wilder Jr. 開發,用於衡量價格變動的速度和幅度,
|
|
613
|
+
是最廣泛使用的動量指標之一。
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
period: 週期(必須 > 0,預設 14)
|
|
617
|
+
column: 計算欄位(預設 "close")
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
RSI 指標配置 dict
|
|
621
|
+
|
|
622
|
+
Note:
|
|
623
|
+
- RSI 輸出為**單一 f64 欄位**,值域 0-100
|
|
624
|
+
- 列名格式為 `RSI|{period}`(例如 `RSI|14`)
|
|
625
|
+
- 使用 Wilder's SMMA(α = 1/period)計算
|
|
626
|
+
- 解讀:> 70 為超買,< 30 為超賣,30~70 為中性
|
|
627
|
+
|
|
628
|
+
Example:
|
|
629
|
+
>>> # 使用預設參數(14 週期,close 欄位)
|
|
630
|
+
>>> Indicator.rsi(period=14, column="close")
|
|
631
|
+
>>> # 輸出列名: "RSI|14"
|
|
632
|
+
>>>
|
|
633
|
+
>>> # 計算其他欄位的 RSI
|
|
634
|
+
>>> Indicator.rsi(period=9, column="hl2")
|
|
635
|
+
>>>
|
|
636
|
+
>>> # 在策略中使用
|
|
637
|
+
>>> rsi_spec = create_indicator_spec(
|
|
638
|
+
... freq=Freq.MIN_15,
|
|
639
|
+
... indicator=Indicator.rsi(period=14),
|
|
640
|
+
... shift=1
|
|
641
|
+
... )
|
|
642
|
+
>>> # 列名: "15min_RSI|14"
|
|
643
|
+
>>>
|
|
644
|
+
>>> # 超買/超賣條件
|
|
645
|
+
>>> entry_long = pl.col("15min_RSI|14") < 30 # RSI < 30 做多
|
|
646
|
+
>>> entry_short = pl.col("15min_RSI|14") > 70 # RSI > 70 做空
|
|
647
|
+
"""
|
|
648
|
+
return {"type": "RSI", "period": period, "column": column}
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def bollinger_bands(
|
|
652
|
+
period: int = 20,
|
|
653
|
+
num_std: float = 2.0,
|
|
654
|
+
column: str = "close",
|
|
655
|
+
fields: Optional[List[str]] = None,
|
|
656
|
+
) -> Dict[str, Any]:
|
|
657
|
+
"""創建 Bollinger Bands 指標配置
|
|
658
|
+
|
|
659
|
+
對應 Rust: Indicator::BollingerBands { period, num_std, column, fields }
|
|
660
|
+
|
|
661
|
+
Bollinger Bands 由 John Bollinger 開發,使用標準差來動態調整通道寬度,
|
|
662
|
+
用於衡量價格波動率和識別超買/超賣。
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
period: 週期(必須 > 0,預設 20)
|
|
666
|
+
num_std: 標準差倍數(必須 > 0,預設 2.0)
|
|
667
|
+
column: 計算欄位(預設 "close")
|
|
668
|
+
fields: 保留欄位(可選,例如 ["upper", "lower", "middle", "bandwidth"])
|
|
669
|
+
- 可用欄位: "upper", "middle", "lower", "bandwidth"
|
|
670
|
+
- 省略或 None:返回所有欄位
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
Bollinger Bands 指標配置 dict
|
|
674
|
+
|
|
675
|
+
Note:
|
|
676
|
+
- Bollinger Bands 輸出為 **Struct 欄位**
|
|
677
|
+
- 列名格式為 `BB|{period}_{num_std}`(例如 `BB|20_2.0`)
|
|
678
|
+
- 子欄位:
|
|
679
|
+
- upper: 上軌(SMA + N×σ)
|
|
680
|
+
- middle: 中軌(SMA)
|
|
681
|
+
- lower: 下軌(SMA - N×σ)
|
|
682
|
+
- bandwidth: 通道寬度(upper - lower)
|
|
683
|
+
|
|
684
|
+
Example:
|
|
685
|
+
>>> # 使用預設參數(20 週期,2 倍標準差)
|
|
686
|
+
>>> Indicator.bollinger_bands(period=20, num_std=2.0, column="close")
|
|
687
|
+
>>> # 輸出列名: "BB|20_2.0"
|
|
688
|
+
>>>
|
|
689
|
+
>>> # 僅保留核心欄位
|
|
690
|
+
>>> Indicator.bollinger_bands(
|
|
691
|
+
... period=20,
|
|
692
|
+
... num_std=2.0,
|
|
693
|
+
... column="close",
|
|
694
|
+
... fields=["upper", "lower", "middle"]
|
|
695
|
+
... )
|
|
696
|
+
>>>
|
|
697
|
+
>>> # 在策略中使用
|
|
698
|
+
>>> bb_spec = create_indicator_spec(
|
|
699
|
+
... freq=Freq.HOUR_1,
|
|
700
|
+
... indicator=Indicator.bollinger_bands(period=20, num_std=2.5),
|
|
701
|
+
... shift=1
|
|
702
|
+
... )
|
|
703
|
+
>>> # 列名: "1h_BB|20_2.5"
|
|
704
|
+
>>>
|
|
705
|
+
>>> # 存取子欄位
|
|
706
|
+
>>> upper = pl.col("1h_BB|20_2.5").struct.field("upper")
|
|
707
|
+
>>> lower = pl.col("1h_BB|20_2.5").struct.field("lower")
|
|
708
|
+
>>> middle = pl.col("1h_BB|20_2.5").struct.field("middle")
|
|
709
|
+
>>>
|
|
710
|
+
>>> # 突破交易條件
|
|
711
|
+
>>> breakout_up = pl.col("close") > upper
|
|
712
|
+
>>> breakout_down = pl.col("close") < lower
|
|
713
|
+
"""
|
|
714
|
+
config = {
|
|
715
|
+
"type": "BollingerBands",
|
|
716
|
+
"period": period,
|
|
717
|
+
"num_std": float(num_std),
|
|
718
|
+
"column": column,
|
|
719
|
+
}
|
|
720
|
+
if fields is not None:
|
|
721
|
+
config["fields"] = fields
|
|
722
|
+
return config
|
|
723
|
+
|
|
724
|
+
@staticmethod
|
|
725
|
+
def macd(
|
|
726
|
+
fast_period: int = 12,
|
|
727
|
+
slow_period: int = 26,
|
|
728
|
+
signal_period: int = 9,
|
|
729
|
+
column: str = "close",
|
|
730
|
+
fields: Optional[List[str]] = None,
|
|
731
|
+
) -> Dict[str, Any]:
|
|
732
|
+
"""創建 MACD 指標配置
|
|
733
|
+
|
|
734
|
+
對應 Rust: Indicator::MACD { fast_period, slow_period, signal_period, column, fields }
|
|
735
|
+
|
|
736
|
+
MACD (Moving Average Convergence Divergence) 由 Gerald Appel 開發,
|
|
737
|
+
通過快慢 EMA 的差值來識別趨勢變化和動量。
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
fast_period: 快線週期(必須 > 0,預設 12)
|
|
741
|
+
slow_period: 慢線週期(必須 > 0,預設 26)
|
|
742
|
+
signal_period: 信號線週期(必須 > 0,預設 9)
|
|
743
|
+
column: 計算欄位(預設 "close")
|
|
744
|
+
fields: 保留欄位(可選,例如 ["macd", "signal", "histogram"])
|
|
745
|
+
- 可用欄位: "macd", "signal", "histogram"
|
|
746
|
+
- 省略或 None:返回所有欄位
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
MACD 指標配置 dict
|
|
750
|
+
|
|
751
|
+
Note:
|
|
752
|
+
- MACD 輸出為 **Struct 欄位**
|
|
753
|
+
- 列名格式為 `MACD|{fast_period}_{slow_period}_{signal_period}`(例如 `MACD|12_26_9`)
|
|
754
|
+
- 子欄位:
|
|
755
|
+
- macd: MACD 線(快線 - 慢線)
|
|
756
|
+
- signal: 信號線(MACD 的 EMA)
|
|
757
|
+
- histogram: 柱狀圖(MACD - Signal)
|
|
758
|
+
|
|
759
|
+
Example:
|
|
760
|
+
>>> # 使用預設參數(12, 26, 9)
|
|
761
|
+
>>> Indicator.macd(fast_period=12, slow_period=26, signal_period=9)
|
|
762
|
+
>>> # 輸出列名: "MACD|12_26_9"
|
|
763
|
+
>>>
|
|
764
|
+
>>> # 僅保留 MACD 和 Signal
|
|
765
|
+
>>> Indicator.macd(
|
|
766
|
+
... fast_period=12,
|
|
767
|
+
... slow_period=26,
|
|
768
|
+
... signal_period=9,
|
|
769
|
+
... fields=["macd", "signal"]
|
|
770
|
+
... )
|
|
771
|
+
>>>
|
|
772
|
+
>>> # 在策略中使用
|
|
773
|
+
>>> macd_spec = create_indicator_spec(
|
|
774
|
+
... freq=Freq.DAY_1,
|
|
775
|
+
... indicator=Indicator.macd(),
|
|
776
|
+
... shift=1
|
|
777
|
+
... )
|
|
778
|
+
>>> # 列名: "1D_MACD|12_26_9"
|
|
779
|
+
>>>
|
|
780
|
+
>>> # 存取子欄位
|
|
781
|
+
>>> macd_line = pl.col("1D_MACD|12_26_9").struct.field("macd")
|
|
782
|
+
>>> signal_line = pl.col("1D_MACD|12_26_9").struct.field("signal")
|
|
783
|
+
>>> histogram = pl.col("1D_MACD|12_26_9").struct.field("histogram")
|
|
784
|
+
>>>
|
|
785
|
+
>>> # 金叉/死叉條件
|
|
786
|
+
>>> golden_cross = macd_line > signal_line # 金叉(做多)
|
|
787
|
+
>>> death_cross = macd_line < signal_line # 死叉(做空)
|
|
788
|
+
"""
|
|
789
|
+
config = {
|
|
790
|
+
"type": "MACD",
|
|
791
|
+
"fast_period": fast_period,
|
|
792
|
+
"slow_period": slow_period,
|
|
793
|
+
"signal_period": signal_period,
|
|
794
|
+
"column": column,
|
|
795
|
+
}
|
|
796
|
+
if fields is not None:
|
|
797
|
+
config["fields"] = fields
|
|
798
|
+
return config
|
|
799
|
+
|
|
800
|
+
@staticmethod
|
|
801
|
+
def stochastic(
|
|
802
|
+
k_period: int = 14,
|
|
803
|
+
d_period: int = 3,
|
|
804
|
+
fields: Optional[List[str]] = None,
|
|
805
|
+
) -> Dict[str, Any]:
|
|
806
|
+
"""創建 Stochastic 指標配置
|
|
807
|
+
|
|
808
|
+
對應 Rust: Indicator::Stochastic { k_period, d_period, fields }
|
|
809
|
+
|
|
810
|
+
Stochastic (隨機指標) 由 George Lane 開發,通過比較收盤價與價格區間來衡量動量,
|
|
811
|
+
用於識別超買/超賣。
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
k_period: %K 週期(必須 > 0,預設 14)
|
|
815
|
+
d_period: %D 週期(必須 > 0,預設 3)
|
|
816
|
+
fields: 保留欄位(可選,例如 ["k", "d"])
|
|
817
|
+
- 可用欄位: "k", "d"
|
|
818
|
+
- 省略或 None:返回所有欄位
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
Stochastic 指標配置 dict
|
|
822
|
+
|
|
823
|
+
Note:
|
|
824
|
+
- Stochastic 輸出為 **Struct 欄位**
|
|
825
|
+
- 列名格式為 `STOCH|{k_period}_{d_period}`(例如 `STOCH|14_3`)
|
|
826
|
+
- 子欄位:
|
|
827
|
+
- k: %K 線(快線,0-100)
|
|
828
|
+
- d: %D 線(慢線,%K 的 SMA,0-100)
|
|
829
|
+
- 解讀:> 80 為超買,< 20 為超賣,20~80 為中性
|
|
830
|
+
|
|
831
|
+
Example:
|
|
832
|
+
>>> # 使用預設參數(14, 3)
|
|
833
|
+
>>> Indicator.stochastic(k_period=14, d_period=3)
|
|
834
|
+
>>> # 輸出列名: "STOCH|14_3"
|
|
835
|
+
>>>
|
|
836
|
+
>>> # 僅保留 %K 線
|
|
837
|
+
>>> Indicator.stochastic(k_period=14, d_period=3, fields=["k"])
|
|
838
|
+
>>>
|
|
839
|
+
>>> # 在策略中使用
|
|
840
|
+
>>> stoch_spec = create_indicator_spec(
|
|
841
|
+
... freq=Freq.MIN_15,
|
|
842
|
+
... indicator=Indicator.stochastic(),
|
|
843
|
+
... shift=1
|
|
844
|
+
... )
|
|
845
|
+
>>> # 列名: "15min_STOCH|14_3"
|
|
846
|
+
>>>
|
|
847
|
+
>>> # 存取子欄位
|
|
848
|
+
>>> k_line = pl.col("15min_STOCH|14_3").struct.field("k")
|
|
849
|
+
>>> d_line = pl.col("15min_STOCH|14_3").struct.field("d")
|
|
850
|
+
>>>
|
|
851
|
+
>>> # 超買/超賣條件
|
|
852
|
+
>>> oversold = k_line < 20 # %K < 20(做多)
|
|
853
|
+
>>> overbought = k_line > 80 # %K > 80(做空)
|
|
854
|
+
>>>
|
|
855
|
+
>>> # 金叉/死叉條件
|
|
856
|
+
>>> golden_cross = k_line > d_line # 金叉(做多)
|
|
857
|
+
>>> death_cross = k_line < d_line # 死叉(做空)
|
|
858
|
+
"""
|
|
859
|
+
config = {
|
|
860
|
+
"type": "Stochastic",
|
|
861
|
+
"k_period": k_period,
|
|
862
|
+
"d_period": d_period,
|
|
863
|
+
}
|
|
864
|
+
if fields is not None:
|
|
865
|
+
config["fields"] = fields
|
|
866
|
+
return config
|
|
867
|
+
|
|
868
|
+
@staticmethod
|
|
869
|
+
def adx(period: int = 14, fields: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
870
|
+
"""創建 ADX 指標配置
|
|
871
|
+
|
|
872
|
+
對應 Rust: Indicator::ADX { period, fields }
|
|
873
|
+
|
|
874
|
+
ADX (Average Directional Index) 由 J. Welles Wilder Jr. 開發,用於量化趨勢強度(不區分方向),
|
|
875
|
+
同時提供方向性指標(+DI/-DI)和 ADXR。
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
period: 週期(必須 > 0,預設 14)
|
|
879
|
+
fields: 保留欄位(可選,例如 ["adx", "plus_di", "minus_di", "adxr"])
|
|
880
|
+
- 可用欄位: "adx", "plus_di", "minus_di", "adxr", "dx"
|
|
881
|
+
- 預設(None 或 []):返回 ["adx", "plus_di", "minus_di", "adxr"](不包含 dx)
|
|
882
|
+
- dx 欄位僅在明確指定時才包含
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
ADX 指標配置 dict
|
|
886
|
+
|
|
887
|
+
Note:
|
|
888
|
+
- ADX 輸出為 **Struct 欄位**
|
|
889
|
+
- 列名格式為 `ADX|{period}`(例如 `ADX|14`)
|
|
890
|
+
- 子欄位:
|
|
891
|
+
- adx: ADX 值(趨勢強度,0-100)
|
|
892
|
+
- plus_di: +DI(上升動量,0-100)
|
|
893
|
+
- minus_di: -DI(下降動量,0-100)
|
|
894
|
+
- adxr: ADXR(ADX 的平滑值)
|
|
895
|
+
- dx: DX(方向性指標,ADX 的原始值,0-100,需明確指定)
|
|
896
|
+
- ADX 解讀:
|
|
897
|
+
- 0-25: 弱趨勢(避免趨勢策略)
|
|
898
|
+
- 25-50: 中等趨勢(適合趨勢跟隨)
|
|
899
|
+
- 50-75: 強趨勢(強勢跟單)
|
|
900
|
+
- 75-100: 極強趨勢(警惕反轉)
|
|
901
|
+
|
|
902
|
+
Example:
|
|
903
|
+
>>> # 使用預設參數(14 週期,返回 adx, plus_di, minus_di, adxr)
|
|
904
|
+
>>> Indicator.adx(period=14)
|
|
905
|
+
>>> # 輸出列名: "ADX|14"
|
|
906
|
+
>>>
|
|
907
|
+
>>> # 僅保留 ADX 和方向性指標
|
|
908
|
+
>>> Indicator.adx(period=14, fields=["adx", "plus_di", "minus_di"])
|
|
909
|
+
>>>
|
|
910
|
+
>>> # 包含 DX 原始值
|
|
911
|
+
>>> Indicator.adx(period=14, fields=["adx", "dx"])
|
|
912
|
+
>>>
|
|
913
|
+
>>> # 在策略中使用
|
|
914
|
+
>>> adx_spec = create_indicator_spec(
|
|
915
|
+
... freq=Freq.HOUR_1,
|
|
916
|
+
... indicator=Indicator.adx(period=14),
|
|
917
|
+
... shift=1
|
|
918
|
+
... )
|
|
919
|
+
>>> # 列名: "1h_ADX|14"
|
|
920
|
+
>>>
|
|
921
|
+
>>> # 存取子欄位
|
|
922
|
+
>>> adx_value = pl.col("1h_ADX|14").struct.field("adx")
|
|
923
|
+
>>> plus_di = pl.col("1h_ADX|14").struct.field("plus_di")
|
|
924
|
+
>>> minus_di = pl.col("1h_ADX|14").struct.field("minus_di")
|
|
925
|
+
>>>
|
|
926
|
+
>>> # 趨勢強度過濾
|
|
927
|
+
>>> strong_trend = adx_value > 25 # 中等以上趨勢
|
|
928
|
+
>>>
|
|
929
|
+
>>> # 方向判斷
|
|
930
|
+
>>> uptrend = (plus_di > minus_di) & (adx_value > 25) # 上升趨勢 + 強度足夠
|
|
931
|
+
>>> downtrend = (plus_di < minus_di) & (adx_value > 25) # 下降趨勢 + 強度足夠
|
|
932
|
+
"""
|
|
933
|
+
config = {
|
|
934
|
+
"type": "ADX",
|
|
935
|
+
"period": period,
|
|
936
|
+
}
|
|
937
|
+
if fields is not None:
|
|
938
|
+
config["fields"] = fields
|
|
939
|
+
return config
|
|
940
|
+
|
|
941
|
+
@staticmethod
|
|
942
|
+
def raw_ohlcv(column: str) -> Dict[str, Any]:
|
|
943
|
+
"""創建 RawOhlcv 指標配置
|
|
944
|
+
|
|
945
|
+
對應 Rust: Indicator::RawOhlcv { column }
|
|
946
|
+
|
|
947
|
+
用於直接訪問已 resample 到指定頻率的原始 OHLCV 列數據,無需任何額外計算。
|
|
948
|
+
|
|
949
|
+
Args:
|
|
950
|
+
column: OHLCV 列名,可選值:
|
|
951
|
+
- "open": 開盤價
|
|
952
|
+
- "high": 最高價
|
|
953
|
+
- "low": 最低價
|
|
954
|
+
- "close": 收盤價
|
|
955
|
+
- "volume": 成交量
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
RawOhlcv 指標配置 dict
|
|
959
|
+
|
|
960
|
+
Note:
|
|
961
|
+
- RawOhlcv 只支持向上採樣(freq > base_freq)
|
|
962
|
+
- 如果 freq = base_freq,應直接使用原始列名而非 RawOhlcv
|
|
963
|
+
- 向下採樣不支持(會報錯)
|
|
964
|
+
|
|
965
|
+
Example:
|
|
966
|
+
>>> # 獲取日線開盤價(在分鐘級策略中)
|
|
967
|
+
>>> Indicator.raw_ohlcv(column="open")
|
|
968
|
+
>>>
|
|
969
|
+
>>> # 獲取小時線收盤價
|
|
970
|
+
>>> Indicator.raw_ohlcv(column="close")
|
|
971
|
+
>>>
|
|
972
|
+
>>> # 完整範例:在策略中使用
|
|
973
|
+
>>> daily_open = create_indicator_spec(
|
|
974
|
+
... freq=Freq.DAY_1,
|
|
975
|
+
... indicator=Indicator.raw_ohlcv("open"),
|
|
976
|
+
... instrument_id="TXF_M1_SHIOAJI_FUTURE",
|
|
977
|
+
... shift=1
|
|
978
|
+
... )
|
|
979
|
+
>>> # 輸出列名: "TXF_M1_SHIOAJI_FUTURE_open_1D"
|
|
980
|
+
>>> # 策略條件: if price > daily_open.col(): ...
|
|
981
|
+
"""
|
|
982
|
+
return {"type": "RawOhlcv", "column": column}
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# ============================================================================
|
|
986
|
+
# Anchor Config 輔助函數(用於 Market Profile)
|
|
987
|
+
# ============================================================================
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def create_daily_anchor(
|
|
991
|
+
hour: int,
|
|
992
|
+
minute: int,
|
|
993
|
+
lookback_days: int = 1,
|
|
994
|
+
) -> Dict[str, Any]:
|
|
995
|
+
"""創建每日 Anchor 配置(用於 Market Profile)
|
|
996
|
+
|
|
997
|
+
Args:
|
|
998
|
+
hour: 小時 (0-23)
|
|
999
|
+
minute: 分鐘 (0-59)
|
|
1000
|
+
lookback_days: 回溯天數(預設 1)
|
|
1001
|
+
|
|
1002
|
+
Returns:
|
|
1003
|
+
Anchor 配置 dict
|
|
1004
|
+
|
|
1005
|
+
Note:
|
|
1006
|
+
系統固定使用 "ts" 和 "segment_id" 欄位名稱
|
|
1007
|
+
|
|
1008
|
+
Example:
|
|
1009
|
+
>>> # 每日 09:15 結束,回溯 1 天
|
|
1010
|
+
>>> anchor = create_daily_anchor(9, 15, 1)
|
|
1011
|
+
>>> mp = Indicator.market_profile(
|
|
1012
|
+
... anchor_config=anchor,
|
|
1013
|
+
... tick_size=0.01
|
|
1014
|
+
... )
|
|
1015
|
+
"""
|
|
1016
|
+
return {
|
|
1017
|
+
"end_rule": {"type": "DailyTime", "hour": hour, "minute": minute},
|
|
1018
|
+
"start_rule": None,
|
|
1019
|
+
"lookback_days": lookback_days,
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def create_weekly_anchor(
|
|
1024
|
+
weekday: Union[int, str, Weekday],
|
|
1025
|
+
hour: int,
|
|
1026
|
+
minute: int,
|
|
1027
|
+
lookback_days: int = 5,
|
|
1028
|
+
) -> Dict[str, Any]:
|
|
1029
|
+
"""創建每週 Anchor 配置(用於 Market Profile)
|
|
1030
|
+
|
|
1031
|
+
Args:
|
|
1032
|
+
weekday: 星期幾,支持多種格式:
|
|
1033
|
+
- int: 0=週一, 1=週二, ..., 6=週日
|
|
1034
|
+
- str: "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"
|
|
1035
|
+
- Weekday enum: Weekday.MON, Weekday.TUE, ...
|
|
1036
|
+
hour: 小時 (0-23)
|
|
1037
|
+
minute: 分鐘 (0-59)
|
|
1038
|
+
lookback_days: 回溯天數(預設 5)
|
|
1039
|
+
|
|
1040
|
+
Returns:
|
|
1041
|
+
Anchor 配置 dict
|
|
1042
|
+
|
|
1043
|
+
Note:
|
|
1044
|
+
系統固定使用 "ts" 和 "segment_id" 欄位名稱
|
|
1045
|
+
|
|
1046
|
+
Example:
|
|
1047
|
+
>>> # 使用整數
|
|
1048
|
+
>>> anchor = create_weekly_anchor(0, 9, 15, 5) # 週一
|
|
1049
|
+
>>> # 使用字串
|
|
1050
|
+
>>> anchor = create_weekly_anchor("Mon", 9, 15, 5)
|
|
1051
|
+
>>> # 使用 Weekday enum
|
|
1052
|
+
>>> anchor = create_weekly_anchor(Weekday.MON, 9, 15, 5)
|
|
1053
|
+
>>> mp = Indicator.market_profile(
|
|
1054
|
+
... anchor_config=anchor,
|
|
1055
|
+
... tick_size=0.01
|
|
1056
|
+
... )
|
|
1057
|
+
"""
|
|
1058
|
+
# 轉換 weekday 為字串格式
|
|
1059
|
+
weekday_map = {
|
|
1060
|
+
0: "Mon",
|
|
1061
|
+
1: "Tue",
|
|
1062
|
+
2: "Wed",
|
|
1063
|
+
3: "Thu",
|
|
1064
|
+
4: "Fri",
|
|
1065
|
+
5: "Sat",
|
|
1066
|
+
6: "Sun",
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if isinstance(weekday, int):
|
|
1070
|
+
weekday_str = weekday_map[weekday]
|
|
1071
|
+
elif isinstance(weekday, Weekday):
|
|
1072
|
+
weekday_str = weekday.value
|
|
1073
|
+
else:
|
|
1074
|
+
weekday_str = str(weekday)
|
|
1075
|
+
|
|
1076
|
+
return {
|
|
1077
|
+
"end_rule": {
|
|
1078
|
+
"type": "WeeklyTime",
|
|
1079
|
+
"weekday": weekday_str,
|
|
1080
|
+
"hour": hour,
|
|
1081
|
+
"minute": minute,
|
|
1082
|
+
},
|
|
1083
|
+
"start_rule": None,
|
|
1084
|
+
"lookback_days": lookback_days,
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def create_profile_shape_config(
|
|
1089
|
+
early_period_ratio: float = 0.15,
|
|
1090
|
+
late_period_ratio: float = 0.15,
|
|
1091
|
+
trend_ib_max_ratio: float = 0.20,
|
|
1092
|
+
trend_monotonic_threshold: float = 0.60,
|
|
1093
|
+
trend_imbalance_threshold: float = 0.70,
|
|
1094
|
+
pshape_concentration_threshold: float = 0.60,
|
|
1095
|
+
bshape_valley_threshold: float = 0.70,
|
|
1096
|
+
normal_symmetry_threshold: float = 0.30,
|
|
1097
|
+
) -> Dict[str, Any]:
|
|
1098
|
+
"""創建 Profile 形狀識別配置(用於 MarketProfile 指標)
|
|
1099
|
+
|
|
1100
|
+
Args:
|
|
1101
|
+
early_period_ratio: 早期時段比例(前 15% 視為早期),預設 0.15
|
|
1102
|
+
late_period_ratio: 晚期時段比例(後 15% 視為晚期),預設 0.15
|
|
1103
|
+
trend_ib_max_ratio: 趨勢日 IB 最大占比,預設 0.20
|
|
1104
|
+
trend_monotonic_threshold: 趨勢日單向移動閾值,預設 0.60
|
|
1105
|
+
trend_imbalance_threshold: 趨勢日早期/晚期 TPO 不平衡度閾值,預設 0.70
|
|
1106
|
+
pshape_concentration_threshold: P型/b型 TPO 集中度閾值,預設 0.60
|
|
1107
|
+
bshape_valley_threshold: B型雙峰之間谷的深度閾值,預設 0.70
|
|
1108
|
+
normal_symmetry_threshold: Normal 型對稱性閾值,預設 0.30
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
Profile 形狀識別配置 dict
|
|
1112
|
+
|
|
1113
|
+
Note:
|
|
1114
|
+
通常使用預設值即可,僅在需要微調形狀識別邏輯時才自訂參數
|
|
1115
|
+
|
|
1116
|
+
Example:
|
|
1117
|
+
>>> # 使用預設值(推薦)
|
|
1118
|
+
>>> shape_config = create_profile_shape_config()
|
|
1119
|
+
>>> Indicator.market_profile(
|
|
1120
|
+
... anchor_config=create_daily_anchor(9, 15, 1),
|
|
1121
|
+
... tick_size=0.5,
|
|
1122
|
+
... shape_config=shape_config
|
|
1123
|
+
... )
|
|
1124
|
+
>>>
|
|
1125
|
+
>>> # 自訂閾值(進階)
|
|
1126
|
+
>>> shape_config = create_profile_shape_config(
|
|
1127
|
+
... trend_monotonic_threshold=0.65,
|
|
1128
|
+
... pshape_concentration_threshold=0.65
|
|
1129
|
+
... )
|
|
1130
|
+
"""
|
|
1131
|
+
return {
|
|
1132
|
+
"early_period_ratio": early_period_ratio,
|
|
1133
|
+
"late_period_ratio": late_period_ratio,
|
|
1134
|
+
"trend_ib_max_ratio": trend_ib_max_ratio,
|
|
1135
|
+
"trend_monotonic_threshold": trend_monotonic_threshold,
|
|
1136
|
+
"trend_imbalance_threshold": trend_imbalance_threshold,
|
|
1137
|
+
"pshape_concentration_threshold": pshape_concentration_threshold,
|
|
1138
|
+
"bshape_valley_threshold": bshape_valley_threshold,
|
|
1139
|
+
"normal_symmetry_threshold": normal_symmetry_threshold,
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
class IndicatorSpec(BaseModel):
|
|
1144
|
+
"""指標規範"""
|
|
1145
|
+
|
|
1146
|
+
instrument_id: Optional[str] = Field(
|
|
1147
|
+
None, description="交易標的 ID(可選,用於多商品場景)"
|
|
1148
|
+
)
|
|
1149
|
+
freq: Union[Freq, str] = Field(..., description="頻率 (使用 Freq enum 或自訂字串)")
|
|
1150
|
+
shift: int = Field(1, description="位移(預設 1)")
|
|
1151
|
+
indicator: Dict[str, Any] = Field(
|
|
1152
|
+
..., description="指標配置 (使用 indicator 工廠函數建立)"
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
def short_name(self) -> str:
|
|
1156
|
+
"""生成指標簡短名稱(與 Rust 的 short_name 一致)
|
|
1157
|
+
|
|
1158
|
+
格式範例:
|
|
1159
|
+
- SMA: "SMA|20.close"
|
|
1160
|
+
- EMA: "EMA|12.close"
|
|
1161
|
+
- ATR: "ATR|14"
|
|
1162
|
+
- SuperTrend: "ST|3.0x_atr"
|
|
1163
|
+
|
|
1164
|
+
Returns:
|
|
1165
|
+
指標簡稱字符串
|
|
1166
|
+
|
|
1167
|
+
Raises:
|
|
1168
|
+
ValueError: 未知的指標類型
|
|
1169
|
+
"""
|
|
1170
|
+
indicator_type = self.indicator.get("type")
|
|
1171
|
+
|
|
1172
|
+
if indicator_type == "SMA":
|
|
1173
|
+
period = self.indicator.get("period")
|
|
1174
|
+
column = self.indicator.get("column", "close")
|
|
1175
|
+
return f"SMA|{period}.{column}"
|
|
1176
|
+
|
|
1177
|
+
elif indicator_type == "EMA":
|
|
1178
|
+
period = self.indicator.get("period")
|
|
1179
|
+
column = self.indicator.get("column", "close")
|
|
1180
|
+
return f"EMA|{period}.{column}"
|
|
1181
|
+
|
|
1182
|
+
elif indicator_type == "SMMA":
|
|
1183
|
+
period = self.indicator.get("period")
|
|
1184
|
+
column = self.indicator.get("column", "close")
|
|
1185
|
+
return f"SMMA|{period}.{column}"
|
|
1186
|
+
|
|
1187
|
+
elif indicator_type == "WMA":
|
|
1188
|
+
period = self.indicator.get("period")
|
|
1189
|
+
column = self.indicator.get("column", "close")
|
|
1190
|
+
return f"WMA|{period}.{column}"
|
|
1191
|
+
|
|
1192
|
+
elif indicator_type == "ATR":
|
|
1193
|
+
period = self.indicator.get("period")
|
|
1194
|
+
# ATR 始終返回單一數值欄位,無 quantile
|
|
1195
|
+
return f"ATR|{period}"
|
|
1196
|
+
|
|
1197
|
+
elif indicator_type == "AtrQuantile":
|
|
1198
|
+
atr_column = self.indicator.get("atr_column")
|
|
1199
|
+
window = self.indicator.get("window")
|
|
1200
|
+
quantile = self.indicator.get("quantile")
|
|
1201
|
+
# 將 quantile 轉為百分比(例如 0.5 -> 50)
|
|
1202
|
+
quantile_pct = int(quantile * 100)
|
|
1203
|
+
# 格式:ATRQ|{atr_column}_Q{quantile%}_{window}
|
|
1204
|
+
return f"ATRQ|{atr_column}_Q{quantile_pct}_{window}"
|
|
1205
|
+
|
|
1206
|
+
elif indicator_type == "SuperTrend":
|
|
1207
|
+
multiplier = self.indicator.get("multiplier")
|
|
1208
|
+
volatility_column = self.indicator.get("volatility_column")
|
|
1209
|
+
return f"ST|{multiplier}x_{volatility_column}"
|
|
1210
|
+
|
|
1211
|
+
elif indicator_type == "MarketProfile":
|
|
1212
|
+
# 提取 anchor_config 資訊
|
|
1213
|
+
anchor_config = self.indicator.get("anchor_config", {})
|
|
1214
|
+
end_rule = anchor_config.get("end_rule", {})
|
|
1215
|
+
lookback_days = anchor_config.get("lookback_days", 1)
|
|
1216
|
+
tick_size = self.indicator.get("tick_size")
|
|
1217
|
+
|
|
1218
|
+
# 格式化時間字串(支持新舊兩種格式)
|
|
1219
|
+
rule_type = end_rule.get("type")
|
|
1220
|
+
|
|
1221
|
+
if rule_type == "DailyTime" or "DailyTime" in end_rule:
|
|
1222
|
+
# 新格式: {"type": "DailyTime", "hour": 9, "minute": 15}
|
|
1223
|
+
# 舊格式: {"DailyTime": {"hour": 9, "minute": 15}}
|
|
1224
|
+
if rule_type == "DailyTime":
|
|
1225
|
+
time_str = f"{end_rule['hour']:02d}{end_rule['minute']:02d}"
|
|
1226
|
+
else:
|
|
1227
|
+
daily = end_rule["DailyTime"]
|
|
1228
|
+
time_str = f"{daily['hour']:02d}{daily['minute']:02d}"
|
|
1229
|
+
|
|
1230
|
+
elif rule_type == "WeeklyTime" or "WeeklyTime" in end_rule:
|
|
1231
|
+
# 新格式: {"type": "WeeklyTime", "weekday": "Mon", "hour": 9, "minute": 15}
|
|
1232
|
+
# 舊格式: {"WeeklyTime": {"weekday": 0, "hour": 9, "minute": 15}}
|
|
1233
|
+
if rule_type == "WeeklyTime":
|
|
1234
|
+
weekday = end_rule["weekday"]
|
|
1235
|
+
# weekday 可能是字串 ("Mon") 或整數 (0)
|
|
1236
|
+
if isinstance(weekday, str):
|
|
1237
|
+
# 字串格式: "Mon" -> "Mon"
|
|
1238
|
+
weekday_str = weekday
|
|
1239
|
+
else:
|
|
1240
|
+
# 整數格式: 0 -> "Mon"
|
|
1241
|
+
weekday_map = {
|
|
1242
|
+
0: "Mon",
|
|
1243
|
+
1: "Tue",
|
|
1244
|
+
2: "Wed",
|
|
1245
|
+
3: "Thu",
|
|
1246
|
+
4: "Fri",
|
|
1247
|
+
5: "Sat",
|
|
1248
|
+
6: "Sun",
|
|
1249
|
+
}
|
|
1250
|
+
weekday_str = weekday_map.get(weekday, str(weekday))
|
|
1251
|
+
time_str = (
|
|
1252
|
+
f"W{weekday_str}_{end_rule['hour']:02d}{end_rule['minute']:02d}"
|
|
1253
|
+
)
|
|
1254
|
+
else:
|
|
1255
|
+
weekly = end_rule["WeeklyTime"]
|
|
1256
|
+
weekday = weekly["weekday"]
|
|
1257
|
+
if isinstance(weekday, str):
|
|
1258
|
+
weekday_str = weekday
|
|
1259
|
+
else:
|
|
1260
|
+
weekday_map = {
|
|
1261
|
+
0: "Mon",
|
|
1262
|
+
1: "Tue",
|
|
1263
|
+
2: "Wed",
|
|
1264
|
+
3: "Thu",
|
|
1265
|
+
4: "Fri",
|
|
1266
|
+
5: "Sat",
|
|
1267
|
+
6: "Sun",
|
|
1268
|
+
}
|
|
1269
|
+
weekday_str = weekday_map.get(weekday, str(weekday))
|
|
1270
|
+
time_str = (
|
|
1271
|
+
f"W{weekday_str}_{weekly['hour']:02d}{weekly['minute']:02d}"
|
|
1272
|
+
)
|
|
1273
|
+
else:
|
|
1274
|
+
time_str = "UNKNOWN"
|
|
1275
|
+
|
|
1276
|
+
# 格式: MP|{time}_{lookback_days}_{tick_size}
|
|
1277
|
+
# 注意:MarketProfile 不支持 indicator 層級的 shift
|
|
1278
|
+
return f"MP|{time_str}_{lookback_days}_{tick_size}"
|
|
1279
|
+
|
|
1280
|
+
elif indicator_type == "CCI":
|
|
1281
|
+
period = self.indicator.get("period")
|
|
1282
|
+
return f"CCI|{period}"
|
|
1283
|
+
|
|
1284
|
+
elif indicator_type == "RSI":
|
|
1285
|
+
period = self.indicator.get("period")
|
|
1286
|
+
return f"RSI|{period}"
|
|
1287
|
+
|
|
1288
|
+
elif indicator_type == "BollingerBands":
|
|
1289
|
+
period = self.indicator.get("period")
|
|
1290
|
+
num_std = self.indicator.get("num_std")
|
|
1291
|
+
return f"BB|{period}_{num_std}"
|
|
1292
|
+
|
|
1293
|
+
elif indicator_type == "MACD":
|
|
1294
|
+
fast_period = self.indicator.get("fast_period")
|
|
1295
|
+
slow_period = self.indicator.get("slow_period")
|
|
1296
|
+
signal_period = self.indicator.get("signal_period")
|
|
1297
|
+
return f"MACD|{fast_period}_{slow_period}_{signal_period}"
|
|
1298
|
+
|
|
1299
|
+
elif indicator_type == "Stochastic":
|
|
1300
|
+
k_period = self.indicator.get("k_period")
|
|
1301
|
+
d_period = self.indicator.get("d_period")
|
|
1302
|
+
return f"STOCH|{k_period}_{d_period}"
|
|
1303
|
+
|
|
1304
|
+
elif indicator_type == "ADX":
|
|
1305
|
+
period = self.indicator.get("period")
|
|
1306
|
+
return f"ADX|{period}"
|
|
1307
|
+
|
|
1308
|
+
elif indicator_type == "RawOhlcv":
|
|
1309
|
+
# RawOhlcv 只返回 column 名稱
|
|
1310
|
+
column = self.indicator.get("column")
|
|
1311
|
+
return column
|
|
1312
|
+
|
|
1313
|
+
else:
|
|
1314
|
+
raise ValueError(f"未知的指標類型: {indicator_type}")
|
|
1315
|
+
|
|
1316
|
+
def display_name(self) -> str:
|
|
1317
|
+
"""生成完整的 column 名稱(與 Rust 的 display_name 一致)
|
|
1318
|
+
|
|
1319
|
+
格式:
|
|
1320
|
+
- 有 instrument_id: "{instrument_id}_{freq}_{indicator_short_name}[_s{shift}]"
|
|
1321
|
+
- 無 instrument_id: "{freq}_{indicator_short_name}[_s{shift}]"
|
|
1322
|
+
|
|
1323
|
+
範例:
|
|
1324
|
+
- "ES_1min_SMA|20.close" (shift=1,不顯示)
|
|
1325
|
+
- "1D_ATR|14_s2" (shift=2)
|
|
1326
|
+
- "BTC_5min_EMA|12.close_s2" (shift=2)
|
|
1327
|
+
- "1h_ST|3.0x_atr" (無 instrument_id)
|
|
1328
|
+
|
|
1329
|
+
Returns:
|
|
1330
|
+
完整的 column 名稱字符串
|
|
1331
|
+
"""
|
|
1332
|
+
parts = []
|
|
1333
|
+
|
|
1334
|
+
# 1. instrument_id(如果有)
|
|
1335
|
+
if self.instrument_id:
|
|
1336
|
+
parts.append(self.instrument_id)
|
|
1337
|
+
|
|
1338
|
+
# 2. freq(處理 Freq enum)
|
|
1339
|
+
freq_str = self.freq.value if isinstance(self.freq, Freq) else self.freq
|
|
1340
|
+
parts.append(freq_str)
|
|
1341
|
+
|
|
1342
|
+
# 3. indicator short name
|
|
1343
|
+
parts.append(self.short_name())
|
|
1344
|
+
|
|
1345
|
+
# 4. shift(只在非預設值時加入)
|
|
1346
|
+
if self.shift != 1:
|
|
1347
|
+
parts.append(f"s{self.shift}")
|
|
1348
|
+
|
|
1349
|
+
return "_".join(parts)
|
|
1350
|
+
|
|
1351
|
+
def col(self) -> pl.Expr:
|
|
1352
|
+
"""返回 Polars column expression(最常用的便捷方法)
|
|
1353
|
+
|
|
1354
|
+
Returns:
|
|
1355
|
+
pl.col(display_name) 的 Polars 表達式
|
|
1356
|
+
|
|
1357
|
+
Example:
|
|
1358
|
+
>>> spec = IndicatorSpec(
|
|
1359
|
+
... instrument_id="ES",
|
|
1360
|
+
... freq="1min",
|
|
1361
|
+
... shift=1,
|
|
1362
|
+
... indicator={"type": "SMA", "period": 20, "column": "close"}
|
|
1363
|
+
... )
|
|
1364
|
+
>>> # 直接在條件中使用
|
|
1365
|
+
>>> condition = spec.col() > 100
|
|
1366
|
+
>>> # 或在過濾中使用
|
|
1367
|
+
>>> df.filter(spec.col() > pl.col("open"))
|
|
1368
|
+
"""
|
|
1369
|
+
return pl.col(self.display_name())
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
class Trigger(BaseModel):
|
|
1373
|
+
"""進出場觸發器
|
|
1374
|
+
|
|
1375
|
+
使用方式:
|
|
1376
|
+
- 讀取:trigger.conditions 直接得到 List[pl.Expr]
|
|
1377
|
+
- 寫入:可直接賦值 pl.col("close") > 100
|
|
1378
|
+
"""
|
|
1379
|
+
|
|
1380
|
+
name: str = Field(..., description="觸發器名稱")
|
|
1381
|
+
order_strategy: Union[OrderStrategy, str] = Field(
|
|
1382
|
+
..., description="訂單策略(OrderStrategy enum 或字串)"
|
|
1383
|
+
)
|
|
1384
|
+
priority: int = Field(..., description="優先級")
|
|
1385
|
+
note: Optional[str] = Field(None, description="備註")
|
|
1386
|
+
|
|
1387
|
+
# 直接使用 pl.Expr 類型
|
|
1388
|
+
conditions: List[pl.Expr] = Field(..., description="條件列表(Polars Expr)")
|
|
1389
|
+
price_expr: pl.Expr = Field(..., description="價格表達式(Polars Expr)")
|
|
1390
|
+
|
|
1391
|
+
@field_validator("conditions", mode="before")
|
|
1392
|
+
@classmethod
|
|
1393
|
+
def validate_conditions(cls, v: Any) -> List[pl.Expr]:
|
|
1394
|
+
"""自動轉換 conditions 為 List[pl.Expr]"""
|
|
1395
|
+
if not v:
|
|
1396
|
+
return []
|
|
1397
|
+
|
|
1398
|
+
result = []
|
|
1399
|
+
for item in v:
|
|
1400
|
+
result.append(PolarsExprField.deserialize(item))
|
|
1401
|
+
return result
|
|
1402
|
+
|
|
1403
|
+
@field_validator("price_expr", mode="before")
|
|
1404
|
+
@classmethod
|
|
1405
|
+
def validate_price_expr(cls, v: Any) -> pl.Expr:
|
|
1406
|
+
"""自動轉換 price_expr 為 pl.Expr"""
|
|
1407
|
+
return PolarsExprField.deserialize(v)
|
|
1408
|
+
|
|
1409
|
+
@field_serializer("conditions")
|
|
1410
|
+
def serialize_conditions(self, conditions: List[pl.Expr]) -> List[dict]:
|
|
1411
|
+
"""序列化 conditions 為 dict 列表(與服務器格式一致)"""
|
|
1412
|
+
return [PolarsExprField.serialize(expr) for expr in conditions]
|
|
1413
|
+
|
|
1414
|
+
@field_serializer("price_expr")
|
|
1415
|
+
def serialize_price_expr(self, price_expr: pl.Expr) -> dict:
|
|
1416
|
+
"""序列化 price_expr 為 dict(與服務器格式一致)"""
|
|
1417
|
+
return PolarsExprField.serialize(price_expr)
|
|
1418
|
+
|
|
1419
|
+
class Config:
|
|
1420
|
+
# 允許 pl.Expr 這種自定義類型
|
|
1421
|
+
arbitrary_types_allowed = True
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
class Blueprint(BaseModel):
|
|
1425
|
+
"""策略藍圖"""
|
|
1426
|
+
|
|
1427
|
+
name: str = Field(..., description="藍圖名稱")
|
|
1428
|
+
direction: Literal["Long", "Short", "Both"] = Field(..., description="方向")
|
|
1429
|
+
trend_type: Literal["Trend", "Range", "Reversal"] = Field(
|
|
1430
|
+
..., description="趨勢類型"
|
|
1431
|
+
)
|
|
1432
|
+
entry_first: bool = Field(..., description="是否優先進場")
|
|
1433
|
+
note: str = Field(..., description="備註")
|
|
1434
|
+
|
|
1435
|
+
entry_triggers: List[Trigger] = Field(..., description="進場觸發器列表")
|
|
1436
|
+
exit_triggers: List[Trigger] = Field(..., description="出場觸發器列表")
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
class StrategyConfig(BaseModel):
|
|
1440
|
+
"""完整策略配置"""
|
|
1441
|
+
|
|
1442
|
+
name: str = Field(..., description="策略名稱")
|
|
1443
|
+
base_instrument: str = Field(..., description="基礎交易標的")
|
|
1444
|
+
base_freq: Freq = Field(..., description="基礎頻率")
|
|
1445
|
+
note: str = Field(..., description="備註")
|
|
1446
|
+
|
|
1447
|
+
volatility_indicator: Optional[IndicatorSpec] = Field(
|
|
1448
|
+
None, description="波動率指標"
|
|
1449
|
+
)
|
|
1450
|
+
indicators: List[IndicatorSpec] = Field(
|
|
1451
|
+
default_factory=list, description="指標列表"
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
base_blueprint: Blueprint = Field(..., description="基礎藍圖")
|
|
1455
|
+
advanced_blueprints: List[Blueprint] = Field(
|
|
1456
|
+
default_factory=list, description="進階藍圖列表"
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
@classmethod
|
|
1460
|
+
def from_api(cls, api_response: Union[Dict[str, Any], str]) -> "StrategyConfig":
|
|
1461
|
+
"""從 API 響應創建策略配置
|
|
1462
|
+
|
|
1463
|
+
Args:
|
|
1464
|
+
api_response: API 返回的 JSON 數據(dict 或 JSON 字符串)
|
|
1465
|
+
|
|
1466
|
+
Returns:
|
|
1467
|
+
StrategyConfig 實例,conditions 和 price_expr 自動轉為 pl.Expr
|
|
1468
|
+
|
|
1469
|
+
Example:
|
|
1470
|
+
>>> # 從 JSON 字符串
|
|
1471
|
+
>>> strategy = StrategyConfig.from_api(json_str)
|
|
1472
|
+
>>> # 從 dict
|
|
1473
|
+
>>> strategy = StrategyConfig.from_api(response.json())
|
|
1474
|
+
>>> # 直接使用
|
|
1475
|
+
>>> expr = strategy.base_blueprint.entry_triggers[0].conditions[0]
|
|
1476
|
+
>>> print(type(expr)) # <class 'polars.expr.expr.Expr'>
|
|
1477
|
+
"""
|
|
1478
|
+
if isinstance(api_response, str):
|
|
1479
|
+
return cls.model_validate_json(api_response)
|
|
1480
|
+
else:
|
|
1481
|
+
return cls.model_validate(api_response)
|
|
1482
|
+
|
|
1483
|
+
def to_json(self, indent: int = 2) -> str:
|
|
1484
|
+
"""序列化為 JSON 字符串
|
|
1485
|
+
|
|
1486
|
+
自動將 pl.Expr 轉為 JSON 字符串格式
|
|
1487
|
+
|
|
1488
|
+
Returns:
|
|
1489
|
+
JSON 字符串,conditions 和 price_expr 已序列化
|
|
1490
|
+
"""
|
|
1491
|
+
return self.model_dump_json(indent=indent, exclude_none=True).replace("\n", "")
|
|
1492
|
+
|
|
1493
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1494
|
+
"""轉換為字典(用於發送到 API)
|
|
1495
|
+
|
|
1496
|
+
Returns:
|
|
1497
|
+
字典,pl.Expr 已序列化為 JSON 字符串
|
|
1498
|
+
"""
|
|
1499
|
+
return self.model_dump(exclude_none=True)
|
|
1500
|
+
|
|
1501
|
+
def save(self, filepath: str) -> None:
|
|
1502
|
+
"""保存到 JSON 文件"""
|
|
1503
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
1504
|
+
f.write(self.to_json())
|
|
1505
|
+
print(f"✅ 策略已保存到: {filepath}")
|
|
1506
|
+
|
|
1507
|
+
@classmethod
|
|
1508
|
+
def load(cls, filepath: str) -> "StrategyConfig":
|
|
1509
|
+
"""從 JSON 文件加載"""
|
|
1510
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
1511
|
+
return cls.from_api(f.read())
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
# 便捷函數
|
|
1515
|
+
def parse_strategy(data: Union[Dict[str, Any], str]) -> StrategyConfig:
|
|
1516
|
+
"""解析策略配置(最簡單的方式)
|
|
1517
|
+
|
|
1518
|
+
Args:
|
|
1519
|
+
data: API JSON 數據(dict 或 JSON 字符串)
|
|
1520
|
+
|
|
1521
|
+
Returns:
|
|
1522
|
+
StrategyConfig 實例,可直接訪問 pl.Expr
|
|
1523
|
+
|
|
1524
|
+
Example:
|
|
1525
|
+
>>> import requests
|
|
1526
|
+
>>> # 從 API
|
|
1527
|
+
>>> response = requests.get("http://localhost:8080/api/v1/strategies/txf_1d_sma20_30")
|
|
1528
|
+
>>> strategy = parse_strategy(response.json())
|
|
1529
|
+
>>>
|
|
1530
|
+
>>> # 直接訪問 Polars Expr
|
|
1531
|
+
>>> conditions = strategy.base_blueprint.entry_triggers[0].conditions
|
|
1532
|
+
>>> print(type(conditions[0])) # <class 'polars.expr.expr.Expr'>
|
|
1533
|
+
>>>
|
|
1534
|
+
>>> # 從 JSON 字符串
|
|
1535
|
+
>>> strategy = parse_strategy(json_str)
|
|
1536
|
+
"""
|
|
1537
|
+
return StrategyConfig.from_api(data)
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def create_indicator_spec(
|
|
1541
|
+
freq: Union[Freq, str],
|
|
1542
|
+
indicator: Dict[str, Any],
|
|
1543
|
+
instrument_id: Optional[str] = None,
|
|
1544
|
+
shift: int = 1,
|
|
1545
|
+
) -> IndicatorSpec:
|
|
1546
|
+
"""創建指標規範(簡化版工廠函數)
|
|
1547
|
+
|
|
1548
|
+
使用 Indicator 靜態類創建 indicator dict,然後傳入此函數
|
|
1549
|
+
|
|
1550
|
+
Args:
|
|
1551
|
+
freq: 頻率(使用 Freq enum 或字串)
|
|
1552
|
+
indicator: 指標配置(使用 Indicator 靜態類創建)
|
|
1553
|
+
instrument_id: 交易標的 ID(可選)
|
|
1554
|
+
shift: 位移(預設 1)
|
|
1555
|
+
|
|
1556
|
+
Returns:
|
|
1557
|
+
IndicatorSpec 實例,可直接調用 .col() 獲取 Polars 表達式
|
|
1558
|
+
|
|
1559
|
+
Examples:
|
|
1560
|
+
>>> from strategy_models import Freq, Indicator, create_indicator_spec
|
|
1561
|
+
>>>
|
|
1562
|
+
>>> # 方式 1: 使用 Freq enum + Indicator 靜態類
|
|
1563
|
+
>>> sma_20 = create_indicator_spec(
|
|
1564
|
+
... freq=Freq.MIN_1,
|
|
1565
|
+
... indicator=Indicator.sma(period=20),
|
|
1566
|
+
... instrument_id="ES"
|
|
1567
|
+
... )
|
|
1568
|
+
>>> print(sma_20.display_name()) # "ES_1min_SMA|20.close"
|
|
1569
|
+
>>>
|
|
1570
|
+
>>> # 方式 2: 使用字串(向後兼容)
|
|
1571
|
+
>>> atr_14 = create_indicator_spec(
|
|
1572
|
+
... freq="1D",
|
|
1573
|
+
... indicator=Indicator.atr(period=14),
|
|
1574
|
+
... shift=2
|
|
1575
|
+
... )
|
|
1576
|
+
>>> print(atr_14.display_name()) # "1D_ATR|14_s2"
|
|
1577
|
+
>>>
|
|
1578
|
+
>>> # 方式 3: SuperTrend
|
|
1579
|
+
>>> st = create_indicator_spec(
|
|
1580
|
+
... freq=Freq.MIN_5,
|
|
1581
|
+
... indicator=Indicator.supertrend(multiplier=3.0, volatility_column="atr"),
|
|
1582
|
+
... instrument_id="BTC"
|
|
1583
|
+
... )
|
|
1584
|
+
>>> print(st.display_name()) # "BTC_5min_ST|3.0x_atr"
|
|
1585
|
+
>>>
|
|
1586
|
+
>>> # 在策略中使用(最簡潔的寫法)
|
|
1587
|
+
>>> sma_50 = create_indicator_spec(Freq.MIN_1, Indicator.sma(50), "ES")
|
|
1588
|
+
>>> conditions = [
|
|
1589
|
+
... sma_20.col() > sma_50.col(),
|
|
1590
|
+
... pl.col("volume") > 1000
|
|
1591
|
+
... ]
|
|
1592
|
+
"""
|
|
1593
|
+
return IndicatorSpec(
|
|
1594
|
+
instrument_id=instrument_id,
|
|
1595
|
+
freq=freq,
|
|
1596
|
+
shift=shift,
|
|
1597
|
+
indicator=indicator,
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def create_trigger(
|
|
1602
|
+
name: str,
|
|
1603
|
+
conditions: List[pl.Expr],
|
|
1604
|
+
price_expr: pl.Expr,
|
|
1605
|
+
order_strategy: Union[OrderStrategy, str] = OrderStrategy.IMMEDIATE_ENTRY,
|
|
1606
|
+
priority: int = 1,
|
|
1607
|
+
note: Optional[str] = None,
|
|
1608
|
+
) -> Trigger:
|
|
1609
|
+
"""創建觸發器(用於開發時)
|
|
1610
|
+
|
|
1611
|
+
Args:
|
|
1612
|
+
name: 觸發器名稱
|
|
1613
|
+
conditions: 條件列表,可直接使用 pl.col() 等
|
|
1614
|
+
price_expr: 價格表達式
|
|
1615
|
+
order_strategy: 訂單策略(可使用 OrderStrategy enum 或字串,預設 OrderStrategy.IMMEDIATE_ENTRY)
|
|
1616
|
+
priority: 優先級
|
|
1617
|
+
note: 備註
|
|
1618
|
+
|
|
1619
|
+
Returns:
|
|
1620
|
+
Trigger 實例
|
|
1621
|
+
|
|
1622
|
+
Example:
|
|
1623
|
+
>>> # 使用 OrderStrategy enum(推薦)
|
|
1624
|
+
>>> entry = create_trigger(
|
|
1625
|
+
... name="my_entry",
|
|
1626
|
+
... conditions=[
|
|
1627
|
+
... pl.col("sma_30") > pl.col("sma_50"),
|
|
1628
|
+
... pl.col("volume") > 1000
|
|
1629
|
+
... ],
|
|
1630
|
+
... price_expr=pl.col("open"),
|
|
1631
|
+
... order_strategy=OrderStrategy.IMMEDIATE_ENTRY
|
|
1632
|
+
... )
|
|
1633
|
+
>>>
|
|
1634
|
+
>>> # 或使用字串(向後兼容)
|
|
1635
|
+
>>> entry = create_trigger(
|
|
1636
|
+
... name="my_entry",
|
|
1637
|
+
... conditions=[...],
|
|
1638
|
+
... price_expr=pl.col("open"),
|
|
1639
|
+
... order_strategy="ImmediateEntry"
|
|
1640
|
+
... )
|
|
1641
|
+
>>>
|
|
1642
|
+
>>> # 序列化為 JSON
|
|
1643
|
+
>>> print(entry.model_dump_json())
|
|
1644
|
+
"""
|
|
1645
|
+
return Trigger(
|
|
1646
|
+
name=name,
|
|
1647
|
+
conditions=conditions,
|
|
1648
|
+
price_expr=price_expr,
|
|
1649
|
+
order_strategy=order_strategy,
|
|
1650
|
+
priority=priority,
|
|
1651
|
+
note=note,
|
|
1652
|
+
)
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def create_blueprint(
|
|
1656
|
+
name: str,
|
|
1657
|
+
direction: Literal["Long", "Short", "Both"],
|
|
1658
|
+
entry_triggers: List[Trigger],
|
|
1659
|
+
exit_triggers: List[Trigger],
|
|
1660
|
+
trend_type: Literal["Trend", "Range", "Reversal"] = "Trend",
|
|
1661
|
+
entry_first: bool = True,
|
|
1662
|
+
note: str = "",
|
|
1663
|
+
) -> Blueprint:
|
|
1664
|
+
"""創建藍圖(用於開發時)
|
|
1665
|
+
|
|
1666
|
+
Example:
|
|
1667
|
+
>>> entry_trigger = create_trigger(
|
|
1668
|
+
... name="entry",
|
|
1669
|
+
... conditions=[pl.col("sma_30") > pl.col("sma_50")],
|
|
1670
|
+
... price_expr=pl.col("open")
|
|
1671
|
+
... )
|
|
1672
|
+
>>>
|
|
1673
|
+
>>> exit_trigger = create_trigger(
|
|
1674
|
+
... name="exit",
|
|
1675
|
+
... conditions=[pl.col("sma_30") < pl.col("sma_50")],
|
|
1676
|
+
... price_expr=pl.col("open"),
|
|
1677
|
+
... order_strategy="ImmediateExit"
|
|
1678
|
+
... )
|
|
1679
|
+
>>>
|
|
1680
|
+
>>> blueprint = create_blueprint(
|
|
1681
|
+
... name="my_strategy",
|
|
1682
|
+
... direction="Long",
|
|
1683
|
+
... entry_triggers=[entry_trigger],
|
|
1684
|
+
... exit_triggers=[exit_trigger]
|
|
1685
|
+
... )
|
|
1686
|
+
"""
|
|
1687
|
+
return Blueprint(
|
|
1688
|
+
name=name,
|
|
1689
|
+
direction=direction,
|
|
1690
|
+
trend_type=trend_type,
|
|
1691
|
+
entry_first=entry_first,
|
|
1692
|
+
note=note,
|
|
1693
|
+
entry_triggers=entry_triggers,
|
|
1694
|
+
exit_triggers=exit_triggers,
|
|
1695
|
+
)
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
# ============================================================================
|
|
1699
|
+
# Export API 數據模型(對應 Rust 的 Export API 響應)
|
|
1700
|
+
# ============================================================================
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
class TaskStatus(str, Enum):
|
|
1704
|
+
"""任務狀態枚舉(與 Rust TaskStatus enum 一致)
|
|
1705
|
+
|
|
1706
|
+
對應 Rust 的 TaskStatus enum,用於追蹤 export 任務的執行狀態
|
|
1707
|
+
|
|
1708
|
+
Rust 對應關係:
|
|
1709
|
+
- Rust: TaskStatus::Pending → Python: TaskStatus.PENDING (serde: "pending")
|
|
1710
|
+
- Rust: TaskStatus::Running → Python: TaskStatus.RUNNING (serde: "running")
|
|
1711
|
+
- Rust: TaskStatus::Completed → Python: TaskStatus.COMPLETED (serde: "completed")
|
|
1712
|
+
- Rust: TaskStatus::Failed → Python: TaskStatus.FAILED (serde: "failed")
|
|
1713
|
+
"""
|
|
1714
|
+
|
|
1715
|
+
PENDING = "pending"
|
|
1716
|
+
RUNNING = "running"
|
|
1717
|
+
COMPLETED = "completed"
|
|
1718
|
+
FAILED = "failed"
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
class ExportType(str, Enum):
|
|
1722
|
+
"""導出類型枚舉(與 Rust ExportType enum 一致)
|
|
1723
|
+
|
|
1724
|
+
對應 Rust 的 ExportType enum,定義支援的導出格式
|
|
1725
|
+
|
|
1726
|
+
Rust 對應關係:
|
|
1727
|
+
- Rust: ExportType::BacktestResults → Python: ExportType.BACKTEST_RESULTS (serde: "backtest_results")
|
|
1728
|
+
- Rust: ExportType::LatestTrades → Python: ExportType.LATEST_TRADES (serde: "latest_trades")
|
|
1729
|
+
- Rust: ExportType::EnhancedOhlcv → Python: ExportType.ENHANCED_OHLCV (serde: "enhanced_ohlcv")
|
|
1730
|
+
- Rust: ExportType::OnDemandOhlcv → Python: ExportType.ON_DEMAND_OHLCV (serde: "on_demand_ohlcv")
|
|
1731
|
+
"""
|
|
1732
|
+
|
|
1733
|
+
BACKTEST_RESULTS = "backtest_results"
|
|
1734
|
+
LATEST_TRADES = "latest_trades"
|
|
1735
|
+
ENHANCED_OHLCV = "enhanced_ohlcv"
|
|
1736
|
+
ON_DEMAND_OHLCV = "on_demand_ohlcv"
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
class ResultSummary(BaseModel):
|
|
1740
|
+
"""結果摘要(與 Rust ResultSummary struct 一致)
|
|
1741
|
+
|
|
1742
|
+
對應 Rust 的 ResultSummary struct,提供任務完成後的統計摘要
|
|
1743
|
+
"""
|
|
1744
|
+
|
|
1745
|
+
total_trades: int = Field(..., description="交易記錄總數")
|
|
1746
|
+
total_strategies: int = Field(..., description="執行的策略數量")
|
|
1747
|
+
data_rows_count: Optional[int] = Field(
|
|
1748
|
+
None, description="數據行數(enhanced_ohlcv 使用)"
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
class ExportTaskResponse(BaseModel):
|
|
1753
|
+
"""導出任務響應(與 Rust ExportTaskResponse struct 一致)
|
|
1754
|
+
|
|
1755
|
+
對應 Rust 的 ExportTaskResponse struct,包含完整的任務狀態和執行結果
|
|
1756
|
+
|
|
1757
|
+
這是從 GET /api/v1/backtest/export/{task_id}/status 返回的主要響應對象
|
|
1758
|
+
|
|
1759
|
+
Example:
|
|
1760
|
+
>>> response = client.get_export_status(task_id)
|
|
1761
|
+
>>> print(f"狀態: {response.status}")
|
|
1762
|
+
>>> print(f"任務 ID: {response.export_task_id}")
|
|
1763
|
+
>>> if response.status == TaskStatus.COMPLETED:
|
|
1764
|
+
... print(f"總交易數: {response.result_summary.total_trades}")
|
|
1765
|
+
"""
|
|
1766
|
+
|
|
1767
|
+
export_task_id: str = Field(..., description="任務 ID (UUID)")
|
|
1768
|
+
export_type: str = Field(..., description="導出類型")
|
|
1769
|
+
status: TaskStatus = Field(..., description="任務狀態")
|
|
1770
|
+
|
|
1771
|
+
input: Dict[str, Any] = Field(..., description="輸入參數 (ExportRequest)")
|
|
1772
|
+
|
|
1773
|
+
executed_strategies: List[str] = Field(
|
|
1774
|
+
default_factory=list, description="實際執行的策略列表"
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
result_summary: Optional[ResultSummary] = Field(
|
|
1778
|
+
None, description="結果摘要(完成後可用)"
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
redis_keys: Dict[str, str] = Field(
|
|
1782
|
+
default_factory=dict, description="Redis 鍵(完成後可用)"
|
|
1783
|
+
)
|
|
1784
|
+
|
|
1785
|
+
created_at: str = Field(..., description="創建時間 (ISO 8601)")
|
|
1786
|
+
|
|
1787
|
+
started_at: Optional[str] = Field(None, description="開始執行時間 (ISO 8601)")
|
|
1788
|
+
|
|
1789
|
+
completed_at: Optional[str] = Field(None, description="完成時間 (ISO 8601)")
|
|
1790
|
+
|
|
1791
|
+
error: Optional[str] = Field(None, description="錯誤信息")
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
class OnDemandOhlcvRequest(BaseModel):
|
|
1795
|
+
"""On-Demand OHLCV 導出請求(與 Rust OnDemandOhlcvRequest struct 一致)
|
|
1796
|
+
|
|
1797
|
+
特點:
|
|
1798
|
+
- 無需預先註冊策略
|
|
1799
|
+
- 直接指定指標規格即可導出
|
|
1800
|
+
- 適用於快速探索指標、外部數據分析
|
|
1801
|
+
|
|
1802
|
+
Example:
|
|
1803
|
+
>>> from tradepose_client.models import create_indicator_spec, Indicator, Freq
|
|
1804
|
+
>>>
|
|
1805
|
+
>>> # 定義指標
|
|
1806
|
+
>>> sma_spec = create_indicator_spec(Freq.HOUR_1, Indicator.sma(20), shift=1)
|
|
1807
|
+
>>> atr_spec = create_indicator_spec(Freq.HOUR_1, Indicator.atr(14), shift=1)
|
|
1808
|
+
>>>
|
|
1809
|
+
>>> # 創建請求
|
|
1810
|
+
>>> request = OnDemandOhlcvRequest(
|
|
1811
|
+
... base_instrument="TXFR1",
|
|
1812
|
+
... base_freq="1min",
|
|
1813
|
+
... indicator_specs=[
|
|
1814
|
+
... sma_spec.model_dump(exclude_none=True),
|
|
1815
|
+
... atr_spec.model_dump(exclude_none=True)
|
|
1816
|
+
... ],
|
|
1817
|
+
... start_date="2025-01-01T00:00:00",
|
|
1818
|
+
... end_date="2025-01-02T23:59:59"
|
|
1819
|
+
... )
|
|
1820
|
+
"""
|
|
1821
|
+
|
|
1822
|
+
base_instrument: str = Field(
|
|
1823
|
+
..., description="基礎商品 ID (例如 'TXFR1', 'TXF_M1_SHIOAJI_FUTURE')"
|
|
1824
|
+
)
|
|
1825
|
+
base_freq: str = Field(
|
|
1826
|
+
..., description="基礎頻率 ('1min', '5min', '15min', '1h', '4h', '1D' 等)"
|
|
1827
|
+
)
|
|
1828
|
+
indicator_specs: List[Dict[str, Any]] = Field(
|
|
1829
|
+
..., description="指標規格列表 (IndicatorSpec JSON 數組)", min_length=1
|
|
1830
|
+
)
|
|
1831
|
+
start_date: Optional[str] = Field(
|
|
1832
|
+
None, description="開始時間 (ISO 8601 格式,例如 '2025-01-01T00:00:00')"
|
|
1833
|
+
)
|
|
1834
|
+
end_date: Optional[str] = Field(
|
|
1835
|
+
None, description="結束時間 (ISO 8601 格式,預設為當前時間)"
|
|
1836
|
+
)
|