tradepose-models 1.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.
- tradepose_models/__init__.py +44 -0
- tradepose_models/auth/__init__.py +13 -0
- tradepose_models/auth/api_keys.py +52 -0
- tradepose_models/auth/auth.py +20 -0
- tradepose_models/base.py +57 -0
- tradepose_models/billing/__init__.py +33 -0
- tradepose_models/billing/checkout.py +17 -0
- tradepose_models/billing/plans.py +32 -0
- tradepose_models/billing/subscriptions.py +34 -0
- tradepose_models/billing/usage.py +71 -0
- tradepose_models/broker/__init__.py +34 -0
- tradepose_models/broker/account_config.py +93 -0
- tradepose_models/broker/account_models.py +61 -0
- tradepose_models/broker/binding.py +54 -0
- tradepose_models/broker/connection_status.py +14 -0
- tradepose_models/commands/__init__.py +8 -0
- tradepose_models/commands/trader_command.py +80 -0
- tradepose_models/datafeed/__init__.py +19 -0
- tradepose_models/datafeed/events.py +132 -0
- tradepose_models/enums/__init__.py +47 -0
- tradepose_models/enums/account_source.py +42 -0
- tradepose_models/enums/broker_type.py +21 -0
- tradepose_models/enums/currency.py +17 -0
- tradepose_models/enums/engagement_phase.py +47 -0
- tradepose_models/enums/execution_mode.py +16 -0
- tradepose_models/enums/export_type.py +23 -0
- tradepose_models/enums/freq.py +32 -0
- tradepose_models/enums/indicator_type.py +46 -0
- tradepose_models/enums/operation_type.py +19 -0
- tradepose_models/enums/order_strategy.py +47 -0
- tradepose_models/enums/orderbook_event_type.py +29 -0
- tradepose_models/enums/persist_mode.py +28 -0
- tradepose_models/enums/stream.py +14 -0
- tradepose_models/enums/task_status.py +23 -0
- tradepose_models/enums/trade_direction.py +42 -0
- tradepose_models/enums/trend_type.py +22 -0
- tradepose_models/enums/weekday.py +30 -0
- tradepose_models/enums.py +32 -0
- tradepose_models/events/__init__.py +11 -0
- tradepose_models/events/order_events.py +79 -0
- tradepose_models/export/__init__.py +19 -0
- tradepose_models/export/request.py +52 -0
- tradepose_models/export/requests.py +75 -0
- tradepose_models/export/task_metadata.py +97 -0
- tradepose_models/gateway/__init__.py +19 -0
- tradepose_models/gateway/responses.py +37 -0
- tradepose_models/indicators/__init__.py +56 -0
- tradepose_models/indicators/base.py +42 -0
- tradepose_models/indicators/factory.py +254 -0
- tradepose_models/indicators/market_profile.md +60 -0
- tradepose_models/indicators/market_profile.py +333 -0
- tradepose_models/indicators/market_profile_developer.md +1782 -0
- tradepose_models/indicators/market_profile_trading.md +1060 -0
- tradepose_models/indicators/momentum.py +53 -0
- tradepose_models/indicators/moving_average.py +63 -0
- tradepose_models/indicators/other.py +40 -0
- tradepose_models/indicators/trend.py +80 -0
- tradepose_models/indicators/volatility.py +57 -0
- tradepose_models/instruments/__init__.py +13 -0
- tradepose_models/instruments/instrument.py +87 -0
- tradepose_models/scheduler/__init__.py +9 -0
- tradepose_models/scheduler/results.py +49 -0
- tradepose_models/schemas/__init__.py +15 -0
- tradepose_models/schemas/enhanced_ohlcv.py +111 -0
- tradepose_models/schemas/performance.py +40 -0
- tradepose_models/schemas/trades.py +64 -0
- tradepose_models/schemas.py +34 -0
- tradepose_models/shared.py +15 -0
- tradepose_models/strategy/__init__.py +52 -0
- tradepose_models/strategy/base.py +56 -0
- tradepose_models/strategy/blueprint.py +55 -0
- tradepose_models/strategy/config.py +142 -0
- tradepose_models/strategy/entities.py +104 -0
- tradepose_models/strategy/helpers.py +173 -0
- tradepose_models/strategy/indicator_spec.py +531 -0
- tradepose_models/strategy/performance.py +66 -0
- tradepose_models/strategy/portfolio.py +171 -0
- tradepose_models/strategy/registry.py +249 -0
- tradepose_models/strategy/requests.py +33 -0
- tradepose_models/strategy/trigger.py +77 -0
- tradepose_models/trading/__init__.py +55 -0
- tradepose_models/trading/engagement.py +160 -0
- tradepose_models/trading/orderbook.py +73 -0
- tradepose_models/trading/orders.py +137 -0
- tradepose_models/trading/positions.py +78 -0
- tradepose_models/trading/trader_commands.py +138 -0
- tradepose_models/trading/trades_execution.py +27 -0
- tradepose_models/types.py +35 -0
- tradepose_models/utils/__init__.py +13 -0
- tradepose_models/utils/rate_converter.py +112 -0
- tradepose_models/validators.py +32 -0
- tradepose_models-1.1.0.dist-info/METADATA +633 -0
- tradepose_models-1.1.0.dist-info/RECORD +94 -0
- tradepose_models-1.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helper Functions for Strategy Configuration
|
|
3
|
+
|
|
4
|
+
Provides convenient factory functions for creating IndicatorSpec, Trigger, and Blueprint instances.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import polars as pl
|
|
10
|
+
|
|
11
|
+
from ..enums import Freq, OrderStrategy, TradeDirection, TrendType
|
|
12
|
+
from .blueprint import Blueprint
|
|
13
|
+
from .indicator_spec import IndicatorSpec
|
|
14
|
+
from .trigger import Trigger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_indicator_spec(
|
|
18
|
+
freq: Union[Freq, str],
|
|
19
|
+
indicator: Dict[str, Any],
|
|
20
|
+
instrument: Optional[str] = None,
|
|
21
|
+
shift: int = 1,
|
|
22
|
+
) -> IndicatorSpec:
|
|
23
|
+
"""創建指標規範(簡化版工廠函數)
|
|
24
|
+
|
|
25
|
+
使用 Indicator 靜態類創建 indicator dict,然後傳入此函數
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
freq: 頻率(使用 Freq enum 或字串)
|
|
29
|
+
indicator: 指標配置(使用 Indicator 靜態類創建)
|
|
30
|
+
instrument: 交易標的(顯示名稱,可選)
|
|
31
|
+
shift: 位移(預設 1)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
IndicatorSpec 實例,可直接調用 .col() 獲取 Polars 表達式
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> from tradepose_models import Freq, Indicator, create_indicator_spec
|
|
38
|
+
>>>
|
|
39
|
+
>>> # 方式 1: 使用 Freq enum + Indicator 靜態類
|
|
40
|
+
>>> sma_20 = create_indicator_spec(
|
|
41
|
+
... freq=Freq.MIN_1,
|
|
42
|
+
... indicator=Indicator.sma(period=20),
|
|
43
|
+
... instrument="ES"
|
|
44
|
+
... )
|
|
45
|
+
>>> print(sma_20.display_name()) # "ES_1min_SMA|20.close"
|
|
46
|
+
>>>
|
|
47
|
+
>>> # 方式 2: 使用字串(向後兼容)
|
|
48
|
+
>>> atr_14 = create_indicator_spec(
|
|
49
|
+
... freq="1D",
|
|
50
|
+
... indicator=Indicator.atr(period=14),
|
|
51
|
+
... shift=2
|
|
52
|
+
... )
|
|
53
|
+
>>> print(atr_14.display_name()) # "1D_ATR|14_s2"
|
|
54
|
+
>>>
|
|
55
|
+
>>> # 方式 3: SuperTrend
|
|
56
|
+
>>> st = create_indicator_spec(
|
|
57
|
+
... freq=Freq.MIN_5,
|
|
58
|
+
... indicator=Indicator.supertrend(multiplier=3.0, volatility_column="atr"),
|
|
59
|
+
... instrument="BTC"
|
|
60
|
+
... )
|
|
61
|
+
>>> print(st.display_name()) # "BTC_5min_ST|3.0x_atr"
|
|
62
|
+
>>>
|
|
63
|
+
>>> # 在策略中使用(最簡潔的寫法)
|
|
64
|
+
>>> sma_50 = create_indicator_spec(Freq.MIN_1, Indicator.sma(50), "ES")
|
|
65
|
+
>>> conditions = [
|
|
66
|
+
... sma_20.col() > sma_50.col(),
|
|
67
|
+
... pl.col("volume") > 1000
|
|
68
|
+
... ]
|
|
69
|
+
"""
|
|
70
|
+
return IndicatorSpec(
|
|
71
|
+
instrument=instrument,
|
|
72
|
+
instrument_id=None, # Will be populated by Gateway
|
|
73
|
+
freq=freq,
|
|
74
|
+
shift=shift,
|
|
75
|
+
indicator=indicator,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def create_trigger(
|
|
80
|
+
name: str,
|
|
81
|
+
conditions: List[pl.Expr],
|
|
82
|
+
price_expr: pl.Expr,
|
|
83
|
+
order_strategy: OrderStrategy = OrderStrategy.IMMEDIATE_ENTRY,
|
|
84
|
+
priority: int = 1,
|
|
85
|
+
note: Optional[str] = None,
|
|
86
|
+
) -> Trigger:
|
|
87
|
+
"""創建觸發器(用於開發時)
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
name: 觸發器名稱
|
|
91
|
+
conditions: 條件列表,可直接使用 pl.col() 等
|
|
92
|
+
price_expr: 價格表達式
|
|
93
|
+
order_strategy: 訂單策略(可使用 OrderStrategy enum 或字串,預設 OrderStrategy.IMMEDIATE_ENTRY)
|
|
94
|
+
priority: 優先級
|
|
95
|
+
note: 備註
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Trigger 實例
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> # 使用 OrderStrategy enum(推薦)
|
|
102
|
+
>>> entry = create_trigger(
|
|
103
|
+
... name="my_entry",
|
|
104
|
+
... conditions=[
|
|
105
|
+
... pl.col("sma_30") > pl.col("sma_50"),
|
|
106
|
+
... pl.col("volume") > 1000
|
|
107
|
+
... ],
|
|
108
|
+
... price_expr=pl.col("open"),
|
|
109
|
+
... order_strategy=OrderStrategy.IMMEDIATE_ENTRY
|
|
110
|
+
... )
|
|
111
|
+
>>>
|
|
112
|
+
>>> # 或使用字串(向後兼容)
|
|
113
|
+
>>> entry = create_trigger(
|
|
114
|
+
... name="my_entry",
|
|
115
|
+
... conditions=[...],
|
|
116
|
+
... price_expr=pl.col("open"),
|
|
117
|
+
... order_strategy="ImmediateEntry"
|
|
118
|
+
... )
|
|
119
|
+
>>>
|
|
120
|
+
>>> # 序列化為 JSON
|
|
121
|
+
>>> print(entry.model_dump_json())
|
|
122
|
+
"""
|
|
123
|
+
return Trigger(
|
|
124
|
+
name=name,
|
|
125
|
+
conditions=conditions,
|
|
126
|
+
price_expr=price_expr,
|
|
127
|
+
order_strategy=order_strategy,
|
|
128
|
+
priority=priority,
|
|
129
|
+
note=note,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def create_blueprint(
|
|
134
|
+
name: str,
|
|
135
|
+
direction: TradeDirection,
|
|
136
|
+
entry_triggers: List[Trigger],
|
|
137
|
+
exit_triggers: List[Trigger],
|
|
138
|
+
trend_type: TrendType = TrendType.TREND,
|
|
139
|
+
entry_first: bool = True,
|
|
140
|
+
note: str = "",
|
|
141
|
+
) -> Blueprint:
|
|
142
|
+
"""創建藍圖(用於開發時)
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> entry_trigger = create_trigger(
|
|
146
|
+
... name="entry",
|
|
147
|
+
... conditions=[pl.col("sma_30") > pl.col("sma_50")],
|
|
148
|
+
... price_expr=pl.col("open")
|
|
149
|
+
... )
|
|
150
|
+
>>>
|
|
151
|
+
>>> exit_trigger = create_trigger(
|
|
152
|
+
... name="exit",
|
|
153
|
+
... conditions=[pl.col("sma_30") < pl.col("sma_50")],
|
|
154
|
+
... price_expr=pl.col("open"),
|
|
155
|
+
... order_strategy="ImmediateExit"
|
|
156
|
+
... )
|
|
157
|
+
>>>
|
|
158
|
+
>>> blueprint = create_blueprint(
|
|
159
|
+
... name="my_strategy",
|
|
160
|
+
... direction="Long",
|
|
161
|
+
... entry_triggers=[entry_trigger],
|
|
162
|
+
... exit_triggers=[exit_trigger]
|
|
163
|
+
... )
|
|
164
|
+
"""
|
|
165
|
+
return Blueprint(
|
|
166
|
+
name=name,
|
|
167
|
+
direction=direction,
|
|
168
|
+
trend_type=trend_type,
|
|
169
|
+
entry_first=entry_first,
|
|
170
|
+
note=note,
|
|
171
|
+
entry_triggers=entry_triggers,
|
|
172
|
+
exit_triggers=exit_triggers,
|
|
173
|
+
)
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IndicatorSpec Model
|
|
3
|
+
|
|
4
|
+
Provides the IndicatorSpec class for specifying indicator configurations with frequency, shift, and instrument.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Annotated, Any, Optional, Union
|
|
8
|
+
|
|
9
|
+
import polars as pl
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
11
|
+
|
|
12
|
+
from ..enums import Freq
|
|
13
|
+
from ..indicators import (
|
|
14
|
+
ADXIndicator,
|
|
15
|
+
ATRIndicator,
|
|
16
|
+
ATRQuantileIndicator,
|
|
17
|
+
BollingerBandsIndicator,
|
|
18
|
+
CCIIndicator,
|
|
19
|
+
EMAIndicator,
|
|
20
|
+
MACDIndicator,
|
|
21
|
+
MarketProfileIndicator,
|
|
22
|
+
RawOhlcvIndicator,
|
|
23
|
+
RSIIndicator,
|
|
24
|
+
SMAIndicator,
|
|
25
|
+
SMMAIndicator,
|
|
26
|
+
StochasticIndicator,
|
|
27
|
+
SuperTrendIndicator,
|
|
28
|
+
WMAIndicator,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ============================================================================
|
|
32
|
+
# Indicator Discriminated Union (强类型 Indicator 配置)
|
|
33
|
+
# ============================================================================
|
|
34
|
+
|
|
35
|
+
IndicatorConfig = Annotated[
|
|
36
|
+
Union[
|
|
37
|
+
# 移动平均类
|
|
38
|
+
SMAIndicator,
|
|
39
|
+
EMAIndicator,
|
|
40
|
+
SMMAIndicator,
|
|
41
|
+
WMAIndicator,
|
|
42
|
+
# 波动率类
|
|
43
|
+
ATRIndicator,
|
|
44
|
+
ATRQuantileIndicator,
|
|
45
|
+
# 趋势类
|
|
46
|
+
SuperTrendIndicator,
|
|
47
|
+
MACDIndicator,
|
|
48
|
+
ADXIndicator,
|
|
49
|
+
# 动量类
|
|
50
|
+
RSIIndicator,
|
|
51
|
+
CCIIndicator,
|
|
52
|
+
StochasticIndicator,
|
|
53
|
+
# 其他
|
|
54
|
+
BollingerBandsIndicator,
|
|
55
|
+
MarketProfileIndicator,
|
|
56
|
+
RawOhlcvIndicator,
|
|
57
|
+
],
|
|
58
|
+
Field(discriminator="type"),
|
|
59
|
+
]
|
|
60
|
+
"""
|
|
61
|
+
强类型 Indicator 配置
|
|
62
|
+
|
|
63
|
+
使用 Pydantic discriminated union,根据 'type' 字段自动选择正确的模型:
|
|
64
|
+
- type="SMA" → SMAIndicator
|
|
65
|
+
- type="ATR" → ATRIndicator
|
|
66
|
+
- type="SuperTrend" → SuperTrendIndicator
|
|
67
|
+
- ... etc
|
|
68
|
+
|
|
69
|
+
优点:
|
|
70
|
+
- 编译时类型检查
|
|
71
|
+
- IDE 自动补全
|
|
72
|
+
- 参数验证
|
|
73
|
+
- 自动 JSON 序列化/反序列化
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> # 方式 1: 直接创建强类型模型
|
|
77
|
+
>>> atr = ATRIndicator(period=21)
|
|
78
|
+
>>>
|
|
79
|
+
>>> # 方式 2: 从 Dict 自动转换(API 兼容)
|
|
80
|
+
>>> indicator: IndicatorConfig = {"type": "ATR", "period": 21}
|
|
81
|
+
>>> # Pydantic 自动识别为 ATRIndicator
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class IndicatorSpec(BaseModel):
|
|
86
|
+
"""指標規範(强类型版本)"""
|
|
87
|
+
|
|
88
|
+
instrument: Optional[str] = Field(
|
|
89
|
+
None, description="交易標的(顯示名稱,可選,用於多商品場景)"
|
|
90
|
+
)
|
|
91
|
+
instrument_id: Optional[int] = Field(
|
|
92
|
+
None, description="交易標的 ID(資料庫查詢用,由 Gateway 填充)"
|
|
93
|
+
)
|
|
94
|
+
freq: Freq = Field(..., description="頻率 (Freq enum)")
|
|
95
|
+
shift: int = Field(1, description="位移(預設 1)")
|
|
96
|
+
indicator: IndicatorConfig = Field(..., description="指標配置(强类型 Pydantic 模型)")
|
|
97
|
+
|
|
98
|
+
@field_validator("freq", mode="before")
|
|
99
|
+
@classmethod
|
|
100
|
+
def convert_freq(cls, v: Any) -> Freq:
|
|
101
|
+
"""自動轉換字串為 Freq enum(保持 API 兼容性)"""
|
|
102
|
+
if isinstance(v, str):
|
|
103
|
+
try:
|
|
104
|
+
return Freq(v)
|
|
105
|
+
except ValueError:
|
|
106
|
+
valid_values = [e.value for e in Freq]
|
|
107
|
+
raise ValueError(f"Invalid freq: '{v}'. Valid values: {', '.join(valid_values)}")
|
|
108
|
+
return v
|
|
109
|
+
|
|
110
|
+
@field_validator("indicator", mode="before")
|
|
111
|
+
@classmethod
|
|
112
|
+
def convert_indicator(cls, v: Any) -> IndicatorConfig:
|
|
113
|
+
"""自動轉換 Dict 為強類型 Indicator 模型(保持 API 兼容性)
|
|
114
|
+
|
|
115
|
+
接受:
|
|
116
|
+
- Dict[str, Any]: 從 API 返回或舊代碼(自動轉換)
|
|
117
|
+
- IndicatorConfig: 新代碼使用強類型模型
|
|
118
|
+
|
|
119
|
+
Pydantic 會根據 'type' 字段自動選擇正確的模型。
|
|
120
|
+
"""
|
|
121
|
+
# Dict 會被 Pydantic 自動轉換為對應的 Indicator 模型
|
|
122
|
+
# 強類型模型直接返回
|
|
123
|
+
return v
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def short_name(self) -> str:
|
|
127
|
+
"""生成指標簡短名稱(與 Rust 的 short_name 一致)
|
|
128
|
+
|
|
129
|
+
格式範例:
|
|
130
|
+
- SMA: "SMA|20.close"
|
|
131
|
+
- EMA: "EMA|12.close"
|
|
132
|
+
- ATR: "ATR|14"
|
|
133
|
+
- SuperTrend: "ST|3.0x_atr"
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
指標簡稱字符串
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: 未知的指標類型
|
|
140
|
+
"""
|
|
141
|
+
indicator_type = self.indicator.type
|
|
142
|
+
|
|
143
|
+
if indicator_type == "SMA":
|
|
144
|
+
return f"SMA|{self.indicator.period}.{self.indicator.column}"
|
|
145
|
+
|
|
146
|
+
elif indicator_type == "EMA":
|
|
147
|
+
return f"EMA|{self.indicator.period}.{self.indicator.column}"
|
|
148
|
+
|
|
149
|
+
elif indicator_type == "SMMA":
|
|
150
|
+
return f"SMMA|{self.indicator.period}.{self.indicator.column}"
|
|
151
|
+
|
|
152
|
+
elif indicator_type == "WMA":
|
|
153
|
+
return f"WMA|{self.indicator.period}.{self.indicator.column}"
|
|
154
|
+
|
|
155
|
+
elif indicator_type == "ATR":
|
|
156
|
+
# ATR 始終返回單一數值欄位,無 quantile
|
|
157
|
+
return f"ATR|{self.indicator.period}"
|
|
158
|
+
|
|
159
|
+
elif indicator_type == "AtrQuantile":
|
|
160
|
+
# 將 quantile 轉為百分比(例如 0.5 -> 50)
|
|
161
|
+
quantile_pct = int(self.indicator.quantile * 100)
|
|
162
|
+
# 格式:ATRQ|{atr_column}_Q{quantile%}_{window}
|
|
163
|
+
return f"ATRQ|{self.indicator.atr_column}_Q{quantile_pct}_{self.indicator.window}"
|
|
164
|
+
|
|
165
|
+
elif indicator_type == "SuperTrend":
|
|
166
|
+
return f"ST|{self.indicator.multiplier}x_{self.indicator.volatility_column}"
|
|
167
|
+
|
|
168
|
+
elif indicator_type == "MarketProfile":
|
|
169
|
+
# 提取 anchor_config 資訊
|
|
170
|
+
anchor_config = self.indicator.anchor_config
|
|
171
|
+
end_rule = anchor_config.get("end_rule", {})
|
|
172
|
+
lookback_days = anchor_config.get("lookback_days", 1)
|
|
173
|
+
tick_size = self.indicator.tick_size
|
|
174
|
+
|
|
175
|
+
# 格式化時間字串(支持新舊兩種格式)
|
|
176
|
+
rule_type = end_rule.get("type")
|
|
177
|
+
|
|
178
|
+
if rule_type == "DailyTime" or "DailyTime" in end_rule:
|
|
179
|
+
# 新格式: {"type": "DailyTime", "hour": 9, "minute": 15}
|
|
180
|
+
# 舊格式: {"DailyTime": {"hour": 9, "minute": 15}}
|
|
181
|
+
if rule_type == "DailyTime":
|
|
182
|
+
time_str = f"{end_rule['hour']:02d}{end_rule['minute']:02d}"
|
|
183
|
+
else:
|
|
184
|
+
daily = end_rule["DailyTime"]
|
|
185
|
+
time_str = f"{daily['hour']:02d}{daily['minute']:02d}"
|
|
186
|
+
|
|
187
|
+
elif rule_type == "WeeklyTime" or "WeeklyTime" in end_rule:
|
|
188
|
+
# 新格式: {"type": "WeeklyTime", "weekday": "Mon", "hour": 9, "minute": 15}
|
|
189
|
+
# 舊格式: {"WeeklyTime": {"weekday": 0, "hour": 9, "minute": 15}}
|
|
190
|
+
# 輸出格式: W{weekday_int}{HHMM} (與 Rust Server 一致)
|
|
191
|
+
str_to_int_map = {
|
|
192
|
+
"Mon": 0,
|
|
193
|
+
"Tue": 1,
|
|
194
|
+
"Wed": 2,
|
|
195
|
+
"Thu": 3,
|
|
196
|
+
"Fri": 4,
|
|
197
|
+
"Sat": 5,
|
|
198
|
+
"Sun": 6,
|
|
199
|
+
}
|
|
200
|
+
if rule_type == "WeeklyTime":
|
|
201
|
+
weekday = end_rule["weekday"]
|
|
202
|
+
# weekday 可能是字串 ("Mon") 或整數 (0)
|
|
203
|
+
if isinstance(weekday, str):
|
|
204
|
+
weekday_int = str_to_int_map.get(weekday, 0)
|
|
205
|
+
else:
|
|
206
|
+
weekday_int = weekday
|
|
207
|
+
time_str = f"W{weekday_int}{end_rule['hour']:02d}{end_rule['minute']:02d}"
|
|
208
|
+
else:
|
|
209
|
+
weekly = end_rule["WeeklyTime"]
|
|
210
|
+
weekday = weekly["weekday"]
|
|
211
|
+
if isinstance(weekday, str):
|
|
212
|
+
weekday_int = str_to_int_map.get(weekday, 0)
|
|
213
|
+
else:
|
|
214
|
+
weekday_int = weekday
|
|
215
|
+
time_str = f"W{weekday_int}{weekly['hour']:02d}{weekly['minute']:02d}"
|
|
216
|
+
else:
|
|
217
|
+
time_str = "UNKNOWN"
|
|
218
|
+
|
|
219
|
+
# 格式: MP|{time}_{lookback_days}_{tick_size}
|
|
220
|
+
# 注意:MarketProfile 不支持 indicator 層級的 shift
|
|
221
|
+
# TODO: 臨時方案 - 整數顯示為整數,非整數保留小數(與 Rust 行為一致)
|
|
222
|
+
# 詳見 docs/issues/tick-size-float-formatting-mismatch.md
|
|
223
|
+
tick_str = str(int(tick_size)) if tick_size == int(tick_size) else str(tick_size)
|
|
224
|
+
return f"MP|{time_str}_{lookback_days}_{tick_str}"
|
|
225
|
+
|
|
226
|
+
elif indicator_type == "CCI":
|
|
227
|
+
return f"CCI|{self.indicator.period}"
|
|
228
|
+
|
|
229
|
+
elif indicator_type == "RSI":
|
|
230
|
+
return f"RSI|{self.indicator.period}"
|
|
231
|
+
|
|
232
|
+
elif indicator_type == "BollingerBands":
|
|
233
|
+
return f"BB|{self.indicator.period}_{self.indicator.num_std}"
|
|
234
|
+
|
|
235
|
+
elif indicator_type == "MACD":
|
|
236
|
+
return f"MACD|{self.indicator.fast_period}_{self.indicator.slow_period}_{self.indicator.signal_period}"
|
|
237
|
+
|
|
238
|
+
elif indicator_type == "Stochastic":
|
|
239
|
+
return f"STOCH|{self.indicator.k_period}_{self.indicator.d_period}"
|
|
240
|
+
|
|
241
|
+
elif indicator_type == "ADX":
|
|
242
|
+
return f"ADX|{self.indicator.period}"
|
|
243
|
+
|
|
244
|
+
elif indicator_type == "RawOhlcv":
|
|
245
|
+
# RawOhlcv 只返回 column 名稱
|
|
246
|
+
return self.indicator.column
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError(f"未知的指標類型: {indicator_type}")
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def display_name(self) -> str:
|
|
253
|
+
"""生成完整的 column 名稱(與 Rust 的 display_name 一致)
|
|
254
|
+
|
|
255
|
+
格式:
|
|
256
|
+
- 有 instrument_id: "{instrument_id}_{freq}_{indicator_short_name}[_s{shift}]"
|
|
257
|
+
- 無 instrument_id: "{freq}_{indicator_short_name}[_s{shift}]"
|
|
258
|
+
|
|
259
|
+
範例:
|
|
260
|
+
- "ES_1min_SMA|20.close" (shift=1,不顯示)
|
|
261
|
+
- "1D_ATR|14_s2" (shift=2)
|
|
262
|
+
- "BTC_5min_EMA|12.close_s2" (shift=2)
|
|
263
|
+
- "1h_ST|3.0x_atr" (無 instrument_id)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
完整的 column 名稱字符串
|
|
267
|
+
"""
|
|
268
|
+
parts = []
|
|
269
|
+
|
|
270
|
+
# 1. instrument(如果有)
|
|
271
|
+
if self.instrument:
|
|
272
|
+
parts.append(self.instrument)
|
|
273
|
+
|
|
274
|
+
# 2. freq(處理 Freq enum)
|
|
275
|
+
freq_str = self.freq.value if isinstance(self.freq, Freq) else self.freq
|
|
276
|
+
parts.append(freq_str)
|
|
277
|
+
|
|
278
|
+
# 3. indicator short name
|
|
279
|
+
parts.append(self.short_name)
|
|
280
|
+
|
|
281
|
+
# 4. shift(只在非預設值時加入)
|
|
282
|
+
if self.shift != 1:
|
|
283
|
+
parts.append(f"s{self.shift}")
|
|
284
|
+
|
|
285
|
+
return "_".join(parts)
|
|
286
|
+
|
|
287
|
+
def col(self) -> pl.Expr:
|
|
288
|
+
"""返回 Polars column expression(最常用的便捷方法)
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
pl.col(display_name) 的 Polars 表達式
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
>>> spec = IndicatorSpec(
|
|
295
|
+
... instrument_id="ES",
|
|
296
|
+
... freq="1min",
|
|
297
|
+
... shift=1,
|
|
298
|
+
... indicator={"type": "SMA", "period": 20, "column": "close"}
|
|
299
|
+
... )
|
|
300
|
+
>>> # 直接在條件中使用
|
|
301
|
+
>>> condition = spec.col() > 100
|
|
302
|
+
>>> # 或在過濾中使用
|
|
303
|
+
>>> df.filter(spec.col() > pl.col("open"))
|
|
304
|
+
"""
|
|
305
|
+
return pl.col(self.display_name)
|
|
306
|
+
|
|
307
|
+
# ========================================================================
|
|
308
|
+
# Struct Field Accessors
|
|
309
|
+
# ========================================================================
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def market_profile(self) -> "MarketProfileAccessor":
|
|
313
|
+
"""MarketProfile struct 欄位存取器"""
|
|
314
|
+
return MarketProfileAccessor(self)
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def supertrend(self) -> "SuperTrendAccessor":
|
|
318
|
+
"""SuperTrend struct 欄位存取器"""
|
|
319
|
+
return SuperTrendAccessor(self)
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def macd_fields(self) -> "MACDAccessor":
|
|
323
|
+
"""MACD struct 欄位存取器"""
|
|
324
|
+
return MACDAccessor(self)
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def adx_fields(self) -> "ADXAccessor":
|
|
328
|
+
"""ADX struct 欄位存取器"""
|
|
329
|
+
return ADXAccessor(self)
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def stochastic(self) -> "StochasticAccessor":
|
|
333
|
+
"""Stochastic struct 欄位存取器"""
|
|
334
|
+
return StochasticAccessor(self)
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def bollinger_bands(self) -> "BollingerBandsAccessor":
|
|
338
|
+
"""BollingerBands struct 欄位存取器"""
|
|
339
|
+
return BollingerBandsAccessor(self)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ============================================================================
|
|
343
|
+
# Struct Field Accessors
|
|
344
|
+
# ============================================================================
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class StructFieldAccessor:
|
|
348
|
+
"""Struct 欄位存取器基類"""
|
|
349
|
+
|
|
350
|
+
def __init__(self, spec: IndicatorSpec):
|
|
351
|
+
self._spec = spec
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def struct_col(self) -> pl.Expr:
|
|
355
|
+
"""返回整個 struct 的 Polars 表達式"""
|
|
356
|
+
return self._spec.col()
|
|
357
|
+
|
|
358
|
+
def _field(self, name: str) -> pl.Expr:
|
|
359
|
+
"""內部方法:取得 struct 子欄位"""
|
|
360
|
+
return self.struct_col.struct.field(name)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class MarketProfileAccessor(StructFieldAccessor):
|
|
364
|
+
"""MarketProfile struct 欄位存取器
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
>>> mp = IndicatorSpec(freq=Freq.MIN_30, indicator=MarketProfileIndicator(...))
|
|
368
|
+
>>> mp.market_profile.poc # pl.col(...).struct.field("poc")
|
|
369
|
+
>>> mp.market_profile.vah # pl.col(...).struct.field("vah")
|
|
370
|
+
>>> mp.market_profile.profile_shape # pl.col(...).struct.field("profile_shape")
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def poc(self) -> pl.Expr:
|
|
375
|
+
"""Point of Control"""
|
|
376
|
+
return self._field("poc")
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def vah(self) -> pl.Expr:
|
|
380
|
+
"""Value Area High"""
|
|
381
|
+
return self._field("vah")
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def val(self) -> pl.Expr:
|
|
385
|
+
"""Value Area Low"""
|
|
386
|
+
return self._field("val")
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def value_area(self) -> pl.Expr:
|
|
390
|
+
"""Value Area (VAH - VAL)"""
|
|
391
|
+
return self._field("value_area")
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def segment_id(self) -> pl.Expr:
|
|
395
|
+
"""Segment ID"""
|
|
396
|
+
return self._field("segment_id")
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def profile_shape(self) -> pl.Expr:
|
|
400
|
+
"""Profile Shape (p_shape, b_shape, trend_day, normal, etc.)"""
|
|
401
|
+
return self._field("profile_shape")
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def tpo_distribution(self) -> pl.Expr:
|
|
405
|
+
"""TPO Distribution"""
|
|
406
|
+
return self._field("tpo_distribution")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class SuperTrendAccessor(StructFieldAccessor):
|
|
410
|
+
"""SuperTrend struct 欄位存取器
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
>>> st = IndicatorSpec(freq=Freq.MIN_30, indicator=SuperTrendIndicator(...))
|
|
414
|
+
>>> st.supertrend.direction # pl.col(...).struct.field("direction")
|
|
415
|
+
>>> st.supertrend.supertrend # pl.col(...).struct.field("supertrend")
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def direction(self) -> pl.Expr:
|
|
420
|
+
"""Trend direction (1 = up, -1 = down)"""
|
|
421
|
+
return self._field("direction")
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def supertrend(self) -> pl.Expr:
|
|
425
|
+
"""SuperTrend value"""
|
|
426
|
+
return self._field("supertrend")
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def long(self) -> pl.Expr:
|
|
430
|
+
"""Long stop level"""
|
|
431
|
+
return self._field("long")
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def short(self) -> pl.Expr:
|
|
435
|
+
"""Short stop level"""
|
|
436
|
+
return self._field("short")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class MACDAccessor(StructFieldAccessor):
|
|
440
|
+
"""MACD struct 欄位存取器
|
|
441
|
+
|
|
442
|
+
Example:
|
|
443
|
+
>>> macd = IndicatorSpec(freq=Freq.MIN_30, indicator=MACDIndicator(...))
|
|
444
|
+
>>> macd.macd_fields.macd # pl.col(...).struct.field("macd")
|
|
445
|
+
>>> macd.macd_fields.histogram # pl.col(...).struct.field("histogram")
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def macd(self) -> pl.Expr:
|
|
450
|
+
"""MACD line"""
|
|
451
|
+
return self._field("macd")
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def signal(self) -> pl.Expr:
|
|
455
|
+
"""Signal line"""
|
|
456
|
+
return self._field("signal")
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def histogram(self) -> pl.Expr:
|
|
460
|
+
"""MACD histogram"""
|
|
461
|
+
return self._field("histogram")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class ADXAccessor(StructFieldAccessor):
|
|
465
|
+
"""ADX struct 欄位存取器
|
|
466
|
+
|
|
467
|
+
Example:
|
|
468
|
+
>>> adx = IndicatorSpec(freq=Freq.MIN_30, indicator=ADXIndicator(...))
|
|
469
|
+
>>> adx.adx_fields.adx # pl.col(...).struct.field("adx")
|
|
470
|
+
>>> adx.adx_fields.plus_di # pl.col(...).struct.field("plus_di")
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def adx(self) -> pl.Expr:
|
|
475
|
+
"""ADX value"""
|
|
476
|
+
return self._field("adx")
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def plus_di(self) -> pl.Expr:
|
|
480
|
+
"""+DI value"""
|
|
481
|
+
return self._field("plus_di")
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def minus_di(self) -> pl.Expr:
|
|
485
|
+
"""-DI value"""
|
|
486
|
+
return self._field("minus_di")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class StochasticAccessor(StructFieldAccessor):
|
|
490
|
+
"""Stochastic struct 欄位存取器
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
>>> stoch = IndicatorSpec(freq=Freq.MIN_30, indicator=StochasticIndicator(...))
|
|
494
|
+
>>> stoch.stochastic.k # pl.col(...).struct.field("k")
|
|
495
|
+
>>> stoch.stochastic.d # pl.col(...).struct.field("d")
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def k(self) -> pl.Expr:
|
|
500
|
+
"""%K value"""
|
|
501
|
+
return self._field("k")
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def d(self) -> pl.Expr:
|
|
505
|
+
"""%D value"""
|
|
506
|
+
return self._field("d")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class BollingerBandsAccessor(StructFieldAccessor):
|
|
510
|
+
"""BollingerBands struct 欄位存取器
|
|
511
|
+
|
|
512
|
+
Example:
|
|
513
|
+
>>> bb = IndicatorSpec(freq=Freq.MIN_30, indicator=BollingerBandsIndicator(...))
|
|
514
|
+
>>> bb.bollinger_bands.upper # pl.col(...).struct.field("upper")
|
|
515
|
+
>>> bb.bollinger_bands.lower # pl.col(...).struct.field("lower")
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def upper(self) -> pl.Expr:
|
|
520
|
+
"""Upper band"""
|
|
521
|
+
return self._field("upper")
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def middle(self) -> pl.Expr:
|
|
525
|
+
"""Middle band (SMA)"""
|
|
526
|
+
return self._field("middle")
|
|
527
|
+
|
|
528
|
+
@property
|
|
529
|
+
def lower(self) -> pl.Expr:
|
|
530
|
+
"""Lower band"""
|
|
531
|
+
return self._field("lower")
|