tradepose-client 0.1.0__py3-none-any.whl → 0.1.2__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.

@@ -9,79 +9,86 @@ Pydantic 模型用於策略配置 (V2 - 自動轉換版本)
9
9
  import json
10
10
  from enum import Enum
11
11
  from io import StringIO
12
- from typing import Any, Dict, List, Literal, Optional, Union
12
+ from typing import Annotated, Any, Dict, List, Optional, Union
13
13
 
14
14
  import polars as pl
15
- from pydantic import BaseModel, Field, field_serializer, field_validator
15
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
16
+
17
+ # Import enums from enums.py
18
+ from .enums import Freq, OrderStrategy, Weekday, TradeDirection, TrendType
19
+
20
+ # Import Indicator models
21
+ from .indicator_models import (
22
+ SMAIndicator,
23
+ EMAIndicator,
24
+ SMMAIndicator,
25
+ WMAIndicator,
26
+ ATRIndicator,
27
+ ATRQuantileIndicator,
28
+ SuperTrendIndicator,
29
+ MACDIndicator,
30
+ ADXIndicator,
31
+ RSIIndicator,
32
+ CCIIndicator,
33
+ StochasticIndicator,
34
+ BollingerBandsIndicator,
35
+ MarketProfileIndicator,
36
+ RawOhlcvIndicator,
37
+ )
16
38
 
17
39
 
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
- """
40
+ # ============================================================================
41
+ # Indicator Discriminated Union (强类型 Indicator 配置)
42
+ # ============================================================================
77
43
 
78
- MON = "Mon"
79
- TUE = "Tue"
80
- WED = "Wed"
81
- THU = "Thu"
82
- FRI = "Fri"
83
- SAT = "Sat"
84
- SUN = "Sun"
44
+ IndicatorConfig = Annotated[
45
+ Union[
46
+ # 移动平均类
47
+ SMAIndicator,
48
+ EMAIndicator,
49
+ SMMAIndicator,
50
+ WMAIndicator,
51
+ # 波动率类
52
+ ATRIndicator,
53
+ ATRQuantileIndicator,
54
+ # 趋势类
55
+ SuperTrendIndicator,
56
+ MACDIndicator,
57
+ ADXIndicator,
58
+ # 动量类
59
+ RSIIndicator,
60
+ CCIIndicator,
61
+ StochasticIndicator,
62
+ # 其他
63
+ BollingerBandsIndicator,
64
+ MarketProfileIndicator,
65
+ RawOhlcvIndicator,
66
+ ],
67
+ Field(discriminator='type')
68
+ ]
69
+ """
70
+ 强类型 Indicator 配置
71
+
72
+ 使用 Pydantic discriminated union,根据 'type' 字段自动选择正确的模型:
73
+ - type="SMA" → SMAIndicator
74
+ - type="ATR" → ATRIndicator
75
+ - type="SuperTrend" → SuperTrendIndicator
76
+ - ... etc
77
+
78
+ 优点:
79
+ - 编译时类型检查
80
+ - IDE 自动补全
81
+ - 参数验证
82
+ - 自动 JSON 序列化/反序列化
83
+
84
+ Example:
85
+ >>> # 方式 1: 直接创建强类型模型
86
+ >>> atr = ATRIndicator(period=21)
87
+ >>>
88
+ >>> # 方式 2: 从 Dict 自动转换(API 兼容)
89
+ >>> indicator: IndicatorConfig = {"type": "ATR", "period": 21}
90
+ >>> # Pydantic 自动识别为 ATRIndicator
91
+ """
85
92
 
86
93
 
87
94
  class PolarsExprField:
@@ -231,8 +238,8 @@ class Indicator:
231
238
  """
232
239
 
233
240
  @staticmethod
234
- def sma(period: int, column: str = "close") -> Dict[str, Any]:
235
- """創建 SMA 指標配置
241
+ def sma(period: int, column: str = "close") -> SMAIndicator:
242
+ """創建 SMA 指標配置(强类型)
236
243
 
237
244
  對應 Rust: Indicator::SMA { period, column }
238
245
 
@@ -241,72 +248,42 @@ class Indicator:
241
248
  column: 計算欄位(預設 "close")
242
249
 
243
250
  Returns:
244
- SMA 指標配置 dict
251
+ SMAIndicator 強類型模型
245
252
 
246
253
  Example:
247
- >>> Indicator.sma(period=20)
248
- >>> Indicator.sma(period=20, column="high")
254
+ >>> atr = Indicator.sma(period=20)
255
+ >>> assert isinstance(atr, SMAIndicator)
256
+ >>> atr_high = Indicator.sma(period=20, column="high")
249
257
  """
250
- return {"type": "SMA", "period": period, "column": column}
258
+ return SMAIndicator(period=period, column=column)
251
259
 
252
260
  @staticmethod
253
- def ema(period: int, column: str = "close") -> Dict[str, Any]:
254
- """創建 EMA 指標配置
261
+ def ema(period: int, column: str = "close") -> EMAIndicator:
262
+ """創建 EMA 指標配置(强类型)
255
263
 
256
264
  對應 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
265
  """
269
- return {"type": "EMA", "period": period, "column": column}
266
+ return EMAIndicator(period=period, column=column)
270
267
 
271
268
  @staticmethod
272
- def smma(period: int, column: str = "close") -> Dict[str, Any]:
273
- """創建 SMMA 指標配置
269
+ def smma(period: int, column: str = "close") -> SMMAIndicator:
270
+ """創建 SMMA 指標配置(强类型)
274
271
 
275
272
  對應 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
273
  """
287
- return {"type": "SMMA", "period": period, "column": column}
274
+ return SMMAIndicator(period=period, column=column)
288
275
 
289
276
  @staticmethod
290
- def wma(period: int, column: str = "close") -> Dict[str, Any]:
291
- """創建 WMA 指標配置
277
+ def wma(period: int, column: str = "close") -> WMAIndicator:
278
+ """創建 WMA 指標配置(强类型)
292
279
 
293
280
  對應 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
281
  """
305
- return {"type": "WMA", "period": period, "column": column}
282
+ return WMAIndicator(period=period, column=column)
306
283
 
307
284
  @staticmethod
308
- def atr(period: int) -> Dict[str, Any]:
309
- """創建 ATR 指標配置
285
+ def atr(period: int) -> ATRIndicator:
286
+ """創建 ATR 指標配置(强类型)
310
287
 
311
288
  對應 Rust: Indicator::ATR { period }
312
289
 
@@ -336,15 +313,15 @@ class Indicator:
336
313
  ... (pl.col("close") - 2 * pl.col("ATR|14")).alias("stop_loss")
337
314
  ... ])
338
315
  """
339
- return {"type": "ATR", "period": period}
316
+ return ATRIndicator(period=period)
340
317
 
341
318
  @staticmethod
342
319
  def atr_quantile(
343
320
  atr_column: str,
344
321
  window: int,
345
322
  quantile: float,
346
- ) -> Dict[str, Any]:
347
- """創建 AtrQuantile 指標配置
323
+ ) -> ATRQuantileIndicator:
324
+ """創建 AtrQuantile 指標配置(强类型)
348
325
 
349
326
  對應 Rust: Indicator::AtrQuantile { atr_column, window, quantile }
350
327
 
@@ -416,12 +393,11 @@ class Indicator:
416
393
  ... order_strategy="ImmediateEntry"
417
394
  ... )
418
395
  """
419
- return {
420
- "type": "AtrQuantile",
421
- "atr_column": atr_column,
422
- "window": window,
423
- "quantile": quantile,
424
- }
396
+ return ATRQuantileIndicator(
397
+ atr_column=atr_column,
398
+ window=window,
399
+ quantile=quantile,
400
+ )
425
401
 
426
402
  @staticmethod
427
403
  def supertrend(
@@ -431,8 +407,8 @@ class Indicator:
431
407
  low: str = "low",
432
408
  close: str = "close",
433
409
  fields: Optional[List[str]] = None,
434
- ) -> Dict[str, Any]:
435
- """創建 SuperTrend 指標配置
410
+ ) -> SuperTrendIndicator:
411
+ """創建 SuperTrend 指標配置(强类型)
436
412
 
437
413
  對應 Rust: Indicator::SuperTrend { multiplier, volatility_column, high, low, close, fields }
438
414
 
@@ -474,17 +450,14 @@ class Indicator:
474
450
  ... fields=["direction", "supertrend"]
475
451
  ... )
476
452
  """
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
453
+ return SuperTrendIndicator(
454
+ multiplier=float(multiplier),
455
+ volatility_column=volatility_column,
456
+ high=high,
457
+ low=low,
458
+ close=close,
459
+ fields=fields,
460
+ )
488
461
 
489
462
  @staticmethod
490
463
  def market_profile(
@@ -493,8 +466,8 @@ class Indicator:
493
466
  value_area_pct: float = 0.7,
494
467
  fields: Optional[List[str]] = None,
495
468
  shape_config: Optional[Dict[str, Any]] = None,
496
- ) -> Dict[str, Any]:
497
- """創建 Market Profile 指標配置
469
+ ) -> MarketProfileIndicator:
470
+ """創建 Market Profile 指標配置(强类型)
498
471
 
499
472
  對應 Rust: Indicator::MarketProfile { anchor_config, tick_size, ... }
500
473
 
@@ -552,21 +525,17 @@ class Indicator:
552
525
  ... shape_config=shape_cfg.model_dump()
553
526
  ... )
554
527
  """
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
528
+ return MarketProfileIndicator(
529
+ anchor_config=anchor_config,
530
+ tick_size=tick_size,
531
+ value_area_pct=value_area_pct,
532
+ fields=fields,
533
+ shape_config=shape_config,
534
+ )
566
535
 
567
536
  @staticmethod
568
- def cci(period: int = 20) -> Dict[str, Any]:
569
- """創建 CCI 指標配置
537
+ def cci(period: int = 20) -> CCIIndicator:
538
+ """創建 CCI 指標配置(强类型)
570
539
 
571
540
  對應 Rust: Indicator::CCI { period }
572
541
 
@@ -601,11 +570,11 @@ class Indicator:
601
570
  >>> overbought = pl.col("1h_CCI|14") > 100
602
571
  >>> oversold = pl.col("1h_CCI|14") < -100
603
572
  """
604
- return {"type": "CCI", "period": period}
573
+ return CCIIndicator(period=period)
605
574
 
606
575
  @staticmethod
607
- def rsi(period: int = 14, column: str = "close") -> Dict[str, Any]:
608
- """創建 RSI 指標配置
576
+ def rsi(period: int = 14, column: str = "close") -> RSIIndicator:
577
+ """創建 RSI 指標配置(强类型)
609
578
 
610
579
  對應 Rust: Indicator::RSI { period, column }
611
580
 
@@ -645,7 +614,7 @@ class Indicator:
645
614
  >>> entry_long = pl.col("15min_RSI|14") < 30 # RSI < 30 做多
646
615
  >>> entry_short = pl.col("15min_RSI|14") > 70 # RSI > 70 做空
647
616
  """
648
- return {"type": "RSI", "period": period, "column": column}
617
+ return RSIIndicator(period=period, column=column)
649
618
 
650
619
  @staticmethod
651
620
  def bollinger_bands(
@@ -653,8 +622,8 @@ class Indicator:
653
622
  num_std: float = 2.0,
654
623
  column: str = "close",
655
624
  fields: Optional[List[str]] = None,
656
- ) -> Dict[str, Any]:
657
- """創建 Bollinger Bands 指標配置
625
+ ) -> BollingerBandsIndicator:
626
+ """創建 Bollinger Bands 指標配置(强类型)
658
627
 
659
628
  對應 Rust: Indicator::BollingerBands { period, num_std, column, fields }
660
629
 
@@ -711,15 +680,12 @@ class Indicator:
711
680
  >>> breakout_up = pl.col("close") > upper
712
681
  >>> breakout_down = pl.col("close") < lower
713
682
  """
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
683
+ return BollingerBandsIndicator(
684
+ period=period,
685
+ num_std=float(num_std),
686
+ column=column,
687
+ fields=fields,
688
+ )
723
689
 
724
690
  @staticmethod
725
691
  def macd(
@@ -728,8 +694,8 @@ class Indicator:
728
694
  signal_period: int = 9,
729
695
  column: str = "close",
730
696
  fields: Optional[List[str]] = None,
731
- ) -> Dict[str, Any]:
732
- """創建 MACD 指標配置
697
+ ) -> MACDIndicator:
698
+ """創建 MACD 指標配置(强类型)
733
699
 
734
700
  對應 Rust: Indicator::MACD { fast_period, slow_period, signal_period, column, fields }
735
701
 
@@ -786,24 +752,21 @@ class Indicator:
786
752
  >>> golden_cross = macd_line > signal_line # 金叉(做多)
787
753
  >>> death_cross = macd_line < signal_line # 死叉(做空)
788
754
  """
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
755
+ return MACDIndicator(
756
+ fast_period=fast_period,
757
+ slow_period=slow_period,
758
+ signal_period=signal_period,
759
+ column=column,
760
+ fields=fields,
761
+ )
799
762
 
800
763
  @staticmethod
801
764
  def stochastic(
802
765
  k_period: int = 14,
803
766
  d_period: int = 3,
804
767
  fields: Optional[List[str]] = None,
805
- ) -> Dict[str, Any]:
806
- """創建 Stochastic 指標配置
768
+ ) -> StochasticIndicator:
769
+ """創建 Stochastic 指標配置(强类型)
807
770
 
808
771
  對應 Rust: Indicator::Stochastic { k_period, d_period, fields }
809
772
 
@@ -856,18 +819,15 @@ class Indicator:
856
819
  >>> golden_cross = k_line > d_line # 金叉(做多)
857
820
  >>> death_cross = k_line < d_line # 死叉(做空)
858
821
  """
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
822
+ return StochasticIndicator(
823
+ k_period=k_period,
824
+ d_period=d_period,
825
+ fields=fields,
826
+ )
867
827
 
868
828
  @staticmethod
869
- def adx(period: int = 14, fields: Optional[List[str]] = None) -> Dict[str, Any]:
870
- """創建 ADX 指標配置
829
+ def adx(period: int = 14, fields: Optional[List[str]] = None) -> ADXIndicator:
830
+ """創建 ADX 指標配置(强类型)
871
831
 
872
832
  對應 Rust: Indicator::ADX { period, fields }
873
833
 
@@ -930,17 +890,14 @@ class Indicator:
930
890
  >>> uptrend = (plus_di > minus_di) & (adx_value > 25) # 上升趨勢 + 強度足夠
931
891
  >>> downtrend = (plus_di < minus_di) & (adx_value > 25) # 下降趨勢 + 強度足夠
932
892
  """
933
- config = {
934
- "type": "ADX",
935
- "period": period,
936
- }
937
- if fields is not None:
938
- config["fields"] = fields
939
- return config
893
+ return ADXIndicator(
894
+ period=period,
895
+ fields=fields,
896
+ )
940
897
 
941
898
  @staticmethod
942
- def raw_ohlcv(column: str) -> Dict[str, Any]:
943
- """創建 RawOhlcv 指標配置
899
+ def raw_ohlcv(column: str) -> RawOhlcvIndicator:
900
+ """創建 RawOhlcv 指標配置(强类型)
944
901
 
945
902
  對應 Rust: Indicator::RawOhlcv { column }
946
903
 
@@ -979,7 +936,7 @@ class Indicator:
979
936
  >>> # 輸出列名: "TXF_M1_SHIOAJI_FUTURE_open_1D"
980
937
  >>> # 策略條件: if price > daily_open.col(): ...
981
938
  """
982
- return {"type": "RawOhlcv", "column": column}
939
+ return RawOhlcvIndicator(column=column)
983
940
 
984
941
 
985
942
  # ============================================================================
@@ -1141,17 +1098,47 @@ def create_profile_shape_config(
1141
1098
 
1142
1099
 
1143
1100
  class IndicatorSpec(BaseModel):
1144
- """指標規範"""
1101
+ """指標規範(强类型版本)"""
1145
1102
 
1146
1103
  instrument_id: Optional[str] = Field(
1147
1104
  None, description="交易標的 ID(可選,用於多商品場景)"
1148
1105
  )
1149
- freq: Union[Freq, str] = Field(..., description="頻率 (使用 Freq enum 或自訂字串)")
1106
+ freq: Freq = Field(..., description="頻率 (Freq enum)")
1150
1107
  shift: int = Field(1, description="位移(預設 1)")
1151
- indicator: Dict[str, Any] = Field(
1152
- ..., description="指標配置 (使用 indicator 工廠函數建立)"
1108
+ indicator: IndicatorConfig = Field(
1109
+ ..., description="指標配置(强类型 Pydantic 模型)"
1153
1110
  )
1154
1111
 
1112
+ @field_validator("freq", mode="before")
1113
+ @classmethod
1114
+ def convert_freq(cls, v: Any) -> Freq:
1115
+ """自動轉換字串為 Freq enum(保持 API 兼容性)"""
1116
+ if isinstance(v, str):
1117
+ try:
1118
+ return Freq(v)
1119
+ except ValueError:
1120
+ valid_values = [e.value for e in Freq]
1121
+ raise ValueError(
1122
+ f"Invalid freq: '{v}'. "
1123
+ f"Valid values: {', '.join(valid_values)}"
1124
+ )
1125
+ return v
1126
+
1127
+ @field_validator("indicator", mode="before")
1128
+ @classmethod
1129
+ def convert_indicator(cls, v: Any) -> IndicatorConfig:
1130
+ """自動轉換 Dict 為強類型 Indicator 模型(保持 API 兼容性)
1131
+
1132
+ 接受:
1133
+ - Dict[str, Any]: 從 API 返回或舊代碼(自動轉換)
1134
+ - IndicatorConfig: 新代碼使用強類型模型
1135
+
1136
+ Pydantic 會根據 'type' 字段自動選擇正確的模型。
1137
+ """
1138
+ # Dict 會被 Pydantic 自動轉換為對應的 Indicator 模型
1139
+ # 強類型模型直接返回
1140
+ return v
1141
+
1155
1142
  def short_name(self) -> str:
1156
1143
  """生成指標簡短名稱(與 Rust 的 short_name 一致)
1157
1144
 
@@ -1167,53 +1154,39 @@ class IndicatorSpec(BaseModel):
1167
1154
  Raises:
1168
1155
  ValueError: 未知的指標類型
1169
1156
  """
1170
- indicator_type = self.indicator.get("type")
1157
+ indicator_type = self.indicator.type
1171
1158
 
1172
1159
  if indicator_type == "SMA":
1173
- period = self.indicator.get("period")
1174
- column = self.indicator.get("column", "close")
1175
- return f"SMA|{period}.{column}"
1160
+ return f"SMA|{self.indicator.period}.{self.indicator.column}"
1176
1161
 
1177
1162
  elif indicator_type == "EMA":
1178
- period = self.indicator.get("period")
1179
- column = self.indicator.get("column", "close")
1180
- return f"EMA|{period}.{column}"
1163
+ return f"EMA|{self.indicator.period}.{self.indicator.column}"
1181
1164
 
1182
1165
  elif indicator_type == "SMMA":
1183
- period = self.indicator.get("period")
1184
- column = self.indicator.get("column", "close")
1185
- return f"SMMA|{period}.{column}"
1166
+ return f"SMMA|{self.indicator.period}.{self.indicator.column}"
1186
1167
 
1187
1168
  elif indicator_type == "WMA":
1188
- period = self.indicator.get("period")
1189
- column = self.indicator.get("column", "close")
1190
- return f"WMA|{period}.{column}"
1169
+ return f"WMA|{self.indicator.period}.{self.indicator.column}"
1191
1170
 
1192
1171
  elif indicator_type == "ATR":
1193
- period = self.indicator.get("period")
1194
1172
  # ATR 始終返回單一數值欄位,無 quantile
1195
- return f"ATR|{period}"
1173
+ return f"ATR|{self.indicator.period}"
1196
1174
 
1197
1175
  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
1176
  # 將 quantile 轉為百分比(例如 0.5 -> 50)
1202
- quantile_pct = int(quantile * 100)
1177
+ quantile_pct = int(self.indicator.quantile * 100)
1203
1178
  # 格式:ATRQ|{atr_column}_Q{quantile%}_{window}
1204
- return f"ATRQ|{atr_column}_Q{quantile_pct}_{window}"
1179
+ return f"ATRQ|{self.indicator.atr_column}_Q{quantile_pct}_{self.indicator.window}"
1205
1180
 
1206
1181
  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}"
1182
+ return f"ST|{self.indicator.multiplier}x_{self.indicator.volatility_column}"
1210
1183
 
1211
1184
  elif indicator_type == "MarketProfile":
1212
1185
  # 提取 anchor_config 資訊
1213
- anchor_config = self.indicator.get("anchor_config", {})
1186
+ anchor_config = self.indicator.anchor_config
1214
1187
  end_rule = anchor_config.get("end_rule", {})
1215
1188
  lookback_days = anchor_config.get("lookback_days", 1)
1216
- tick_size = self.indicator.get("tick_size")
1189
+ tick_size = self.indicator.tick_size
1217
1190
 
1218
1191
  # 格式化時間字串(支持新舊兩種格式)
1219
1192
  rule_type = end_rule.get("type")
@@ -1278,37 +1251,26 @@ class IndicatorSpec(BaseModel):
1278
1251
  return f"MP|{time_str}_{lookback_days}_{tick_size}"
1279
1252
 
1280
1253
  elif indicator_type == "CCI":
1281
- period = self.indicator.get("period")
1282
- return f"CCI|{period}"
1254
+ return f"CCI|{self.indicator.period}"
1283
1255
 
1284
1256
  elif indicator_type == "RSI":
1285
- period = self.indicator.get("period")
1286
- return f"RSI|{period}"
1257
+ return f"RSI|{self.indicator.period}"
1287
1258
 
1288
1259
  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}"
1260
+ return f"BB|{self.indicator.period}_{self.indicator.num_std}"
1292
1261
 
1293
1262
  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}"
1263
+ return f"MACD|{self.indicator.fast_period}_{self.indicator.slow_period}_{self.indicator.signal_period}"
1298
1264
 
1299
1265
  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}"
1266
+ return f"STOCH|{self.indicator.k_period}_{self.indicator.d_period}"
1303
1267
 
1304
1268
  elif indicator_type == "ADX":
1305
- period = self.indicator.get("period")
1306
- return f"ADX|{period}"
1269
+ return f"ADX|{self.indicator.period}"
1307
1270
 
1308
1271
  elif indicator_type == "RawOhlcv":
1309
1272
  # RawOhlcv 只返回 column 名稱
1310
- column = self.indicator.get("column")
1311
- return column
1273
+ return self.indicator.column
1312
1274
 
1313
1275
  else:
1314
1276
  raise ValueError(f"未知的指標類型: {indicator_type}")
@@ -1378,8 +1340,8 @@ class Trigger(BaseModel):
1378
1340
  """
1379
1341
 
1380
1342
  name: str = Field(..., description="觸發器名稱")
1381
- order_strategy: Union[OrderStrategy, str] = Field(
1382
- ..., description="訂單策略(OrderStrategy enum 或字串)"
1343
+ order_strategy: OrderStrategy = Field(
1344
+ ..., description="訂單策略(OrderStrategy enum"
1383
1345
  )
1384
1346
  priority: int = Field(..., description="優先級")
1385
1347
  note: Optional[str] = Field(None, description="備註")
@@ -1388,6 +1350,21 @@ class Trigger(BaseModel):
1388
1350
  conditions: List[pl.Expr] = Field(..., description="條件列表(Polars Expr)")
1389
1351
  price_expr: pl.Expr = Field(..., description="價格表達式(Polars Expr)")
1390
1352
 
1353
+ @field_validator("order_strategy", mode="before")
1354
+ @classmethod
1355
+ def convert_order_strategy(cls, v: Any) -> OrderStrategy:
1356
+ """自動轉換字串為 OrderStrategy enum(保持 API 兼容性)"""
1357
+ if isinstance(v, str):
1358
+ try:
1359
+ return OrderStrategy(v)
1360
+ except ValueError:
1361
+ valid_values = [e.value for e in OrderStrategy]
1362
+ raise ValueError(
1363
+ f"Invalid order_strategy: '{v}'. "
1364
+ f"Valid values: {', '.join(valid_values)}"
1365
+ )
1366
+ return v
1367
+
1391
1368
  @field_validator("conditions", mode="before")
1392
1369
  @classmethod
1393
1370
  def validate_conditions(cls, v: Any) -> List[pl.Expr]:
@@ -1416,25 +1393,53 @@ class Trigger(BaseModel):
1416
1393
  """序列化 price_expr 為 dict(與服務器格式一致)"""
1417
1394
  return PolarsExprField.serialize(price_expr)
1418
1395
 
1419
- class Config:
1420
- # 允許 pl.Expr 這種自定義類型
1421
- arbitrary_types_allowed = True
1396
+ model_config = ConfigDict(
1397
+ arbitrary_types_allowed=True # 允許 pl.Expr 這種自定義類型
1398
+ )
1422
1399
 
1423
1400
 
1424
1401
  class Blueprint(BaseModel):
1425
1402
  """策略藍圖"""
1426
1403
 
1427
1404
  name: str = Field(..., description="藍圖名稱")
1428
- direction: Literal["Long", "Short", "Both"] = Field(..., description="方向")
1429
- trend_type: Literal["Trend", "Range", "Reversal"] = Field(
1430
- ..., description="趨勢類型"
1431
- )
1405
+ direction: TradeDirection = Field(..., description="方向")
1406
+ trend_type: TrendType = Field(..., description="趨勢類型")
1432
1407
  entry_first: bool = Field(..., description="是否優先進場")
1433
1408
  note: str = Field(..., description="備註")
1434
1409
 
1435
1410
  entry_triggers: List[Trigger] = Field(..., description="進場觸發器列表")
1436
1411
  exit_triggers: List[Trigger] = Field(..., description="出場觸發器列表")
1437
1412
 
1413
+ @field_validator("direction", mode="before")
1414
+ @classmethod
1415
+ def convert_direction(cls, v: Any) -> TradeDirection:
1416
+ """自動轉換字串為 TradeDirection enum(保持 API 兼容性)"""
1417
+ if isinstance(v, str):
1418
+ try:
1419
+ return TradeDirection(v)
1420
+ except ValueError:
1421
+ valid_values = [e.value for e in TradeDirection]
1422
+ raise ValueError(
1423
+ f"Invalid direction: '{v}'. "
1424
+ f"Valid values: {', '.join(valid_values)}"
1425
+ )
1426
+ return v
1427
+
1428
+ @field_validator("trend_type", mode="before")
1429
+ @classmethod
1430
+ def convert_trend_type(cls, v: Any) -> TrendType:
1431
+ """自動轉換字串為 TrendType enum(保持 API 兼容性)"""
1432
+ if isinstance(v, str):
1433
+ try:
1434
+ return TrendType(v)
1435
+ except ValueError:
1436
+ valid_values = [e.value for e in TrendType]
1437
+ raise ValueError(
1438
+ f"Invalid trend_type: '{v}'. "
1439
+ f"Valid values: {', '.join(valid_values)}"
1440
+ )
1441
+ return v
1442
+
1438
1443
 
1439
1444
  class StrategyConfig(BaseModel):
1440
1445
  """完整策略配置"""
@@ -1602,7 +1607,7 @@ def create_trigger(
1602
1607
  name: str,
1603
1608
  conditions: List[pl.Expr],
1604
1609
  price_expr: pl.Expr,
1605
- order_strategy: Union[OrderStrategy, str] = OrderStrategy.IMMEDIATE_ENTRY,
1610
+ order_strategy: OrderStrategy = OrderStrategy.IMMEDIATE_ENTRY,
1606
1611
  priority: int = 1,
1607
1612
  note: Optional[str] = None,
1608
1613
  ) -> Trigger:
@@ -1654,10 +1659,10 @@ def create_trigger(
1654
1659
 
1655
1660
  def create_blueprint(
1656
1661
  name: str,
1657
- direction: Literal["Long", "Short", "Both"],
1662
+ direction: TradeDirection,
1658
1663
  entry_triggers: List[Trigger],
1659
1664
  exit_triggers: List[Trigger],
1660
- trend_type: Literal["Trend", "Range", "Reversal"] = "Trend",
1665
+ trend_type: TrendType = TrendType.TREND,
1661
1666
  entry_first: bool = True,
1662
1667
  note: str = "",
1663
1668
  ) -> Blueprint: