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,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Strategy Performance Model
|
|
3
|
+
|
|
4
|
+
Provides StrategyPerformance for caching strategy metrics in Redis.
|
|
5
|
+
Used for position size calculation and performance monitoring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StrategyPerformance(BaseModel):
|
|
16
|
+
"""Strategy performance metrics (Redis cache).
|
|
17
|
+
|
|
18
|
+
Stores calculated performance metrics for a strategy+blueprint combination.
|
|
19
|
+
Used by TradingDecisionJob for position size calculation.
|
|
20
|
+
|
|
21
|
+
Redis Key Pattern: performance:{user_id}:{strategy_name}:{blueprint_name}
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
strategy_name: str = Field(..., description="Strategy name")
|
|
25
|
+
blueprint_name: str = Field(..., description="Blueprint name")
|
|
26
|
+
user_id: UUID = Field(..., description="User UUID")
|
|
27
|
+
instrument: str = Field(..., description="Trading instrument (e.g., XAUUSD)")
|
|
28
|
+
|
|
29
|
+
# Performance metrics
|
|
30
|
+
win_rate: float = Field(..., description="Win rate (0-1)")
|
|
31
|
+
avg_pnl_pct: float = Field(..., description="Average PnL percentage")
|
|
32
|
+
mae_q90: float = Field(..., description="Maximum Adverse Excursion 90th percentile (%)")
|
|
33
|
+
mfe_q90: float = Field(..., description="Maximum Favorable Excursion 90th percentile (%)")
|
|
34
|
+
recovery_factor: float = Field(..., description="Recovery factor")
|
|
35
|
+
|
|
36
|
+
# Position size calculation
|
|
37
|
+
expected_loss_per_contract: Decimal = Field(
|
|
38
|
+
...,
|
|
39
|
+
description="Expected loss per contract in quote currency (e.g., 40000 USD)",
|
|
40
|
+
)
|
|
41
|
+
quote_currency: str = Field(..., description="Quote currency (e.g., USD)")
|
|
42
|
+
contract_size: Decimal = Field(..., description="Contract size per lot")
|
|
43
|
+
|
|
44
|
+
# Cache metadata
|
|
45
|
+
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
46
|
+
|
|
47
|
+
def calculate_position_size(self, risk_capital: Decimal) -> Decimal:
|
|
48
|
+
"""Calculate position size based on risk capital.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
risk_capital: User's risk capital in portfolio currency
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Number of contracts/lots to trade
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> perf = StrategyPerformance(
|
|
58
|
+
... expected_loss_per_contract=Decimal("40000"),
|
|
59
|
+
... ...
|
|
60
|
+
... )
|
|
61
|
+
>>> qty = perf.calculate_position_size(Decimal("80000"))
|
|
62
|
+
>>> # qty = 80000 / 40000 = 2 contracts
|
|
63
|
+
"""
|
|
64
|
+
if self.expected_loss_per_contract <= 0:
|
|
65
|
+
return Decimal("0")
|
|
66
|
+
return risk_capital / self.expected_loss_per_contract
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Portfolio Module
|
|
3
|
+
|
|
4
|
+
Provides Portfolio class as a selection view for indexing backtest results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from tradepose_models.enums import BrokerType
|
|
13
|
+
|
|
14
|
+
from .registry import BlueprintSelection
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .config import StrategyConfig
|
|
18
|
+
from .registry import StrategyRegistry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Portfolio(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Portfolio - 選取視圖,用於索引回測結果
|
|
24
|
+
|
|
25
|
+
Portfolio 儲存 BlueprintSelection 引用(而非資料複製),
|
|
26
|
+
需要 StrategyRegistry 來解析實際的 StrategyConfig。
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> portfolio = registry.select(
|
|
30
|
+
... name="Q1_Portfolio",
|
|
31
|
+
... selections=[("VA_Breakout", "va_long"), ("VA_Breakout", "va_short")],
|
|
32
|
+
... capital=100000,
|
|
33
|
+
... )
|
|
34
|
+
>>> configs = portfolio.get_configs(registry)
|
|
35
|
+
>>> batch = tester.submit(strategies=configs, periods=[...])
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str = Field(..., description="Portfolio 名稱")
|
|
39
|
+
selections: List[BlueprintSelection] = Field(
|
|
40
|
+
default_factory=list, description="選取的 (strategy_name, blueprint_name) 列表"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 帳戶配置
|
|
44
|
+
capital: Decimal = Field(default=Decimal("0"), description="資金")
|
|
45
|
+
currency: str = Field(default="USD", description="貨幣")
|
|
46
|
+
account_source: Optional[str] = Field(None, description="帳戶來源")
|
|
47
|
+
broker_type: Optional[BrokerType] = Field(None, description="交易平台/Broker 類型")
|
|
48
|
+
instrument_mapping: Optional[Dict[str, str]] = Field(
|
|
49
|
+
None, description="商品映射 (base_instrument → target_symbol),例如 {'BTC': 'BTCUSDT'}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def get_configs(self, registry: "StrategyRegistry") -> List["StrategyConfig"]:
|
|
53
|
+
"""
|
|
54
|
+
取得選取的 configs(用於傳給 BatchTester)
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
registry: StrategyRegistry 實例
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
單一 blueprint 的 StrategyConfig 列表
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
KeyError: 如果任何選取不存在於 registry
|
|
64
|
+
"""
|
|
65
|
+
configs = []
|
|
66
|
+
for sel in self.selections:
|
|
67
|
+
entry = registry.get(sel.strategy_name, sel.blueprint_name)
|
|
68
|
+
if entry is None:
|
|
69
|
+
raise KeyError(
|
|
70
|
+
f"Selection not found in registry: "
|
|
71
|
+
f"strategy='{sel.strategy_name}', blueprint='{sel.blueprint_name}'"
|
|
72
|
+
)
|
|
73
|
+
configs.append(entry.to_single_blueprint_config())
|
|
74
|
+
return configs
|
|
75
|
+
|
|
76
|
+
def add_selection(
|
|
77
|
+
self,
|
|
78
|
+
strategy_name: str,
|
|
79
|
+
blueprint_name: str,
|
|
80
|
+
) -> "Portfolio":
|
|
81
|
+
"""
|
|
82
|
+
新增選取(返回新實例)
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
strategy_name: 策略名稱
|
|
86
|
+
blueprint_name: Blueprint 名稱
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
新的 Portfolio 實例
|
|
90
|
+
"""
|
|
91
|
+
selection = BlueprintSelection(strategy_name=strategy_name, blueprint_name=blueprint_name)
|
|
92
|
+
if selection in self.selections:
|
|
93
|
+
return self
|
|
94
|
+
return self.model_copy(update={"selections": [*self.selections, selection]})
|
|
95
|
+
|
|
96
|
+
def remove_selection(
|
|
97
|
+
self,
|
|
98
|
+
strategy_name: str,
|
|
99
|
+
blueprint_name: str,
|
|
100
|
+
) -> "Portfolio":
|
|
101
|
+
"""
|
|
102
|
+
移除選取(返回新實例)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
strategy_name: 策略名稱
|
|
106
|
+
blueprint_name: Blueprint 名稱
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
新的 Portfolio 實例
|
|
110
|
+
"""
|
|
111
|
+
selection = BlueprintSelection(strategy_name=strategy_name, blueprint_name=blueprint_name)
|
|
112
|
+
if selection not in self.selections:
|
|
113
|
+
return self
|
|
114
|
+
return self.model_copy(
|
|
115
|
+
update={"selections": [s for s in self.selections if s != selection]}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def strategy_names(self) -> List[str]:
|
|
120
|
+
"""取得所有策略名稱(去重)"""
|
|
121
|
+
return list(set(sel.strategy_name for sel in self.selections))
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def selection_count(self) -> int:
|
|
125
|
+
"""選取數量"""
|
|
126
|
+
return len(self.selections)
|
|
127
|
+
|
|
128
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
129
|
+
"""轉換為字典"""
|
|
130
|
+
return self.model_dump(mode="json", exclude_none=True)
|
|
131
|
+
|
|
132
|
+
def to_json(self, indent: int = 2) -> str:
|
|
133
|
+
"""序列化為 JSON 字串"""
|
|
134
|
+
return self.model_dump_json(indent=indent, exclude_none=True)
|
|
135
|
+
|
|
136
|
+
def save(self, filepath: str) -> None:
|
|
137
|
+
"""儲存至 JSON 檔案"""
|
|
138
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
139
|
+
f.write(self.to_json())
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def load(cls, filepath: str) -> "Portfolio":
|
|
143
|
+
"""從 JSON 檔案載入"""
|
|
144
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
145
|
+
return cls.model_validate_json(f.read())
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def from_api(cls, api_response: Union[Dict[str, Any], str]) -> "Portfolio":
|
|
149
|
+
"""從 API 響應建立"""
|
|
150
|
+
if isinstance(api_response, str):
|
|
151
|
+
return cls.model_validate_json(api_response)
|
|
152
|
+
return cls.model_validate(api_response)
|
|
153
|
+
|
|
154
|
+
def __len__(self) -> int:
|
|
155
|
+
"""選取數量"""
|
|
156
|
+
return len(self.selections)
|
|
157
|
+
|
|
158
|
+
def __contains__(self, item: Tuple[str, str]) -> bool:
|
|
159
|
+
"""檢查選取是否存在"""
|
|
160
|
+
selection = BlueprintSelection(strategy_name=item[0], blueprint_name=item[1])
|
|
161
|
+
return selection in self.selections
|
|
162
|
+
|
|
163
|
+
def __iter__(self):
|
|
164
|
+
"""迭代選取"""
|
|
165
|
+
return iter(self.selections)
|
|
166
|
+
|
|
167
|
+
def __repr__(self) -> str:
|
|
168
|
+
return (
|
|
169
|
+
f"Portfolio(name={self.name!r}, selections={len(self.selections)}, "
|
|
170
|
+
f"capital={self.capital}, currency={self.currency!r})"
|
|
171
|
+
)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Strategy Registry Module
|
|
3
|
+
|
|
4
|
+
Provides StrategyRegistry for managing strategies with (strategy_name, blueprint_name)
|
|
5
|
+
as unique keys. Low-coupling design - does not call external objects directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, PrivateAttr
|
|
11
|
+
|
|
12
|
+
from .blueprint import Blueprint
|
|
13
|
+
from .config import StrategyConfig
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .portfolio import Portfolio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BlueprintSelection(BaseModel):
|
|
20
|
+
"""選取鍵,代表 Registry 中的一個 entry"""
|
|
21
|
+
|
|
22
|
+
strategy_name: str
|
|
23
|
+
blueprint_name: str
|
|
24
|
+
|
|
25
|
+
def to_tuple(self) -> Tuple[str, str]:
|
|
26
|
+
"""轉換為 tuple"""
|
|
27
|
+
return (self.strategy_name, self.blueprint_name)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_tuple(cls, t: Tuple[str, str]) -> "BlueprintSelection":
|
|
31
|
+
"""從 tuple 建立"""
|
|
32
|
+
return cls(strategy_name=t[0], blueprint_name=t[1])
|
|
33
|
+
|
|
34
|
+
def __hash__(self) -> int:
|
|
35
|
+
return hash(self.to_tuple())
|
|
36
|
+
|
|
37
|
+
def __eq__(self, other: object) -> bool:
|
|
38
|
+
if isinstance(other, BlueprintSelection):
|
|
39
|
+
return self.to_tuple() == other.to_tuple()
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"BlueprintSelection({self.strategy_name!r}, {self.blueprint_name!r})"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RegistryEntry(BaseModel):
|
|
47
|
+
"""Registry entry,儲存對原始 StrategyConfig 的引用"""
|
|
48
|
+
|
|
49
|
+
strategy: StrategyConfig
|
|
50
|
+
blueprint_name: str
|
|
51
|
+
|
|
52
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def key(self) -> BlueprintSelection:
|
|
56
|
+
"""取得選取鍵"""
|
|
57
|
+
return BlueprintSelection(
|
|
58
|
+
strategy_name=self.strategy.name, blueprint_name=self.blueprint_name
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def blueprint(self) -> Blueprint:
|
|
63
|
+
"""取得此 entry 對應的 Blueprint"""
|
|
64
|
+
if self.strategy.base_blueprint.name == self.blueprint_name:
|
|
65
|
+
return self.strategy.base_blueprint
|
|
66
|
+
for bp in self.strategy.advanced_blueprints:
|
|
67
|
+
if bp.name == self.blueprint_name:
|
|
68
|
+
return bp
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Blueprint '{self.blueprint_name}' not found in strategy '{self.strategy.name}'"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def to_single_blueprint_config(self) -> StrategyConfig:
|
|
74
|
+
"""轉換為單一 blueprint 的 StrategyConfig"""
|
|
75
|
+
return StrategyConfig(
|
|
76
|
+
name=f"{self.strategy.name}__{self.blueprint_name}",
|
|
77
|
+
base_instrument=self.strategy.base_instrument,
|
|
78
|
+
base_freq=self.strategy.base_freq,
|
|
79
|
+
note=self.strategy.note,
|
|
80
|
+
volatility_indicator=self.strategy.volatility_indicator,
|
|
81
|
+
indicators=self.strategy.indicators,
|
|
82
|
+
base_blueprint=self.blueprint,
|
|
83
|
+
advanced_blueprints=[],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def __repr__(self) -> str:
|
|
87
|
+
return f"RegistryEntry({self.strategy.name!r}, {self.blueprint_name!r})"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class StrategyRegistry(BaseModel):
|
|
91
|
+
"""
|
|
92
|
+
策略註冊表 - 低耦合設計,只負責儲存和保證唯一性
|
|
93
|
+
|
|
94
|
+
使用 (strategy_name, blueprint_name) 作為唯一 key。
|
|
95
|
+
當加入有多個 blueprints 的 StrategyConfig 時,自動拆成多個 entries。
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> registry = StrategyRegistry()
|
|
99
|
+
>>> strategy = StrategyConfig.load("va_breakout.json")
|
|
100
|
+
>>> registry.add(strategy) # 自動拆成多個 entries
|
|
101
|
+
>>> configs = registry.get_configs() # 取得所有 configs
|
|
102
|
+
>>> # 使用者自己傳給 BatchTester
|
|
103
|
+
>>> batch = tester.submit(strategies=configs, periods=[...])
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
_entries: Dict[Tuple[str, str], RegistryEntry] = PrivateAttr(default_factory=dict)
|
|
107
|
+
_strategies: Dict[str, StrategyConfig] = PrivateAttr(default_factory=dict)
|
|
108
|
+
|
|
109
|
+
def add(self, strategy: StrategyConfig) -> List[BlueprintSelection]:
|
|
110
|
+
"""
|
|
111
|
+
加入策略,自動拆分所有 blueprints
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
strategy: 要加入的 StrategyConfig
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
建立的 BlueprintSelection 列表
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: 如果任何 (strategy_name, blueprint_name) 已存在
|
|
121
|
+
"""
|
|
122
|
+
all_bps = [strategy.base_blueprint] + list(strategy.advanced_blueprints)
|
|
123
|
+
|
|
124
|
+
# 檢查重複
|
|
125
|
+
for bp in all_bps:
|
|
126
|
+
key = (strategy.name, bp.name)
|
|
127
|
+
if key in self._entries:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Entry already exists: strategy='{strategy.name}', blueprint='{bp.name}'"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# 儲存原始策略
|
|
133
|
+
self._strategies[strategy.name] = strategy
|
|
134
|
+
|
|
135
|
+
# 建立 entries
|
|
136
|
+
created = []
|
|
137
|
+
for bp in all_bps:
|
|
138
|
+
key = (strategy.name, bp.name)
|
|
139
|
+
self._entries[key] = RegistryEntry(strategy=strategy, blueprint_name=bp.name)
|
|
140
|
+
created.append(BlueprintSelection(strategy_name=strategy.name, blueprint_name=bp.name))
|
|
141
|
+
|
|
142
|
+
return created
|
|
143
|
+
|
|
144
|
+
def add_or_replace(self, strategy: StrategyConfig) -> List[BlueprintSelection]:
|
|
145
|
+
"""加入或取代策略"""
|
|
146
|
+
if strategy.name in self._strategies:
|
|
147
|
+
self.remove(strategy.name)
|
|
148
|
+
return self.add(strategy)
|
|
149
|
+
|
|
150
|
+
def get(self, strategy_name: str, blueprint_name: str) -> Optional[RegistryEntry]:
|
|
151
|
+
"""取得特定 entry"""
|
|
152
|
+
return self._entries.get((strategy_name, blueprint_name))
|
|
153
|
+
|
|
154
|
+
def get_strategy(self, strategy_name: str) -> Optional[StrategyConfig]:
|
|
155
|
+
"""取得原始 StrategyConfig(包含所有 blueprints)"""
|
|
156
|
+
return self._strategies.get(strategy_name)
|
|
157
|
+
|
|
158
|
+
def get_configs(self) -> List[StrategyConfig]:
|
|
159
|
+
"""取得所有單一 blueprint configs(用於傳給 BatchTester)"""
|
|
160
|
+
return [entry.to_single_blueprint_config() for entry in self._entries.values()]
|
|
161
|
+
|
|
162
|
+
def remove(self, strategy_name: str) -> int:
|
|
163
|
+
"""
|
|
164
|
+
移除策略的所有 entries
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
移除的 entry 數量
|
|
168
|
+
"""
|
|
169
|
+
keys = [k for k in self._entries if k[0] == strategy_name]
|
|
170
|
+
for k in keys:
|
|
171
|
+
del self._entries[k]
|
|
172
|
+
self._strategies.pop(strategy_name, None)
|
|
173
|
+
return len(keys)
|
|
174
|
+
|
|
175
|
+
def select(
|
|
176
|
+
self,
|
|
177
|
+
name: str,
|
|
178
|
+
selections: List[Tuple[str, str]],
|
|
179
|
+
capital: float = 0,
|
|
180
|
+
currency: str = "USD",
|
|
181
|
+
account_source: Optional[str] = None,
|
|
182
|
+
platform: Optional[str] = None,
|
|
183
|
+
) -> "Portfolio":
|
|
184
|
+
"""
|
|
185
|
+
建立 Portfolio
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
name: Portfolio 名稱
|
|
189
|
+
selections: 選取的 (strategy_name, blueprint_name) 列表
|
|
190
|
+
capital: 資金
|
|
191
|
+
currency: 貨幣
|
|
192
|
+
account_source: 帳戶來源
|
|
193
|
+
platform: 交易平台
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Portfolio 實例
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
KeyError: 如果任何選取不存在
|
|
200
|
+
"""
|
|
201
|
+
for sel in selections:
|
|
202
|
+
if sel not in self._entries:
|
|
203
|
+
raise KeyError(f"Selection not found: strategy='{sel[0]}', blueprint='{sel[1]}'")
|
|
204
|
+
|
|
205
|
+
from .portfolio import Portfolio
|
|
206
|
+
|
|
207
|
+
return Portfolio(
|
|
208
|
+
name=name,
|
|
209
|
+
selections=[
|
|
210
|
+
BlueprintSelection(strategy_name=s[0], blueprint_name=s[1]) for s in selections
|
|
211
|
+
],
|
|
212
|
+
capital=capital,
|
|
213
|
+
currency=currency,
|
|
214
|
+
account_source=account_source,
|
|
215
|
+
platform=platform,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def keys(self) -> List[BlueprintSelection]:
|
|
219
|
+
"""取得所有選取鍵"""
|
|
220
|
+
return [BlueprintSelection(strategy_name=k[0], blueprint_name=k[1]) for k in self._entries]
|
|
221
|
+
|
|
222
|
+
def strategy_names(self) -> List[str]:
|
|
223
|
+
"""取得所有策略名稱"""
|
|
224
|
+
return list(self._strategies.keys())
|
|
225
|
+
|
|
226
|
+
def blueprint_names(self, strategy_name: str) -> List[str]:
|
|
227
|
+
"""取得特定策略的所有 blueprint 名稱"""
|
|
228
|
+
return [k[1] for k in self._entries if k[0] == strategy_name]
|
|
229
|
+
|
|
230
|
+
def __len__(self) -> int:
|
|
231
|
+
"""Entry 數量"""
|
|
232
|
+
return len(self._entries)
|
|
233
|
+
|
|
234
|
+
def __contains__(self, key: Tuple[str, str]) -> bool:
|
|
235
|
+
"""檢查 entry 是否存在"""
|
|
236
|
+
return key in self._entries
|
|
237
|
+
|
|
238
|
+
def __getitem__(self, key: Tuple[str, str]) -> RegistryEntry:
|
|
239
|
+
"""取得 entry: registry[('strategy', 'blueprint')]"""
|
|
240
|
+
if key not in self._entries:
|
|
241
|
+
raise KeyError(f"Entry not found: strategy='{key[0]}', blueprint='{key[1]}'")
|
|
242
|
+
return self._entries[key]
|
|
243
|
+
|
|
244
|
+
def __iter__(self) -> Iterator[RegistryEntry]:
|
|
245
|
+
"""迭代所有 entries"""
|
|
246
|
+
return iter(self._entries.values())
|
|
247
|
+
|
|
248
|
+
def __repr__(self) -> str:
|
|
249
|
+
return f"StrategyRegistry(strategies={len(self._strategies)}, entries={len(self._entries)})"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Strategy API request/response models."""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RegisterStrategyRequest(BaseModel):
|
|
9
|
+
"""Request model for registering a strategy."""
|
|
10
|
+
|
|
11
|
+
strategy_code: str = Field(..., description="Strategy configuration Python code")
|
|
12
|
+
overwrite: bool = Field(default=False, description="Overwrite if strategy exists")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RegisterStrategyResponse(BaseModel):
|
|
16
|
+
"""Response model for strategy registration."""
|
|
17
|
+
|
|
18
|
+
task_id: UUID
|
|
19
|
+
message: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ListStrategiesRequest(BaseModel):
|
|
23
|
+
"""Request model for listing strategies."""
|
|
24
|
+
|
|
25
|
+
full: bool = Field(default=False, description="Return full configs or summary")
|
|
26
|
+
instrument_id: str | None = Field(default=None, description="Filter by instrument ID")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ListStrategiesResponse(BaseModel):
|
|
30
|
+
"""Response model for listing strategies."""
|
|
31
|
+
|
|
32
|
+
task_id: UUID
|
|
33
|
+
message: str
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trigger Model
|
|
3
|
+
|
|
4
|
+
Provides the Trigger class for entry/exit triggers with conditions and price expressions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
|
|
9
|
+
import polars as pl
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
|
|
11
|
+
|
|
12
|
+
from ..enums import OrderStrategy
|
|
13
|
+
from ..indicators import PolarsExprField
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Trigger(BaseModel):
|
|
17
|
+
"""進出場觸發器
|
|
18
|
+
|
|
19
|
+
使用方式:
|
|
20
|
+
- 讀取:trigger.conditions 直接得到 List[pl.Expr]
|
|
21
|
+
- 寫入:可直接賦值 pl.col("close") > 100
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name: str = Field(..., description="觸發器名稱")
|
|
25
|
+
order_strategy: OrderStrategy = Field(..., description="訂單策略(OrderStrategy enum)")
|
|
26
|
+
priority: int = Field(..., description="優先級")
|
|
27
|
+
note: Optional[str] = Field(None, description="備註")
|
|
28
|
+
|
|
29
|
+
# 直接使用 pl.Expr 類型
|
|
30
|
+
conditions: List[pl.Expr] = Field(..., description="條件列表(Polars Expr)")
|
|
31
|
+
price_expr: pl.Expr = Field(..., description="價格表達式(Polars Expr)")
|
|
32
|
+
|
|
33
|
+
@field_validator("order_strategy", mode="before")
|
|
34
|
+
@classmethod
|
|
35
|
+
def convert_order_strategy(cls, v: Any) -> OrderStrategy:
|
|
36
|
+
"""自動轉換字串為 OrderStrategy enum(保持 API 兼容性)"""
|
|
37
|
+
if isinstance(v, str):
|
|
38
|
+
try:
|
|
39
|
+
return OrderStrategy(v)
|
|
40
|
+
except ValueError:
|
|
41
|
+
valid_values = [e.value for e in OrderStrategy]
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Invalid order_strategy: '{v}'. Valid values: {', '.join(valid_values)}"
|
|
44
|
+
)
|
|
45
|
+
return v
|
|
46
|
+
|
|
47
|
+
@field_validator("conditions", mode="before")
|
|
48
|
+
@classmethod
|
|
49
|
+
def validate_conditions(cls, v: Any) -> List[pl.Expr]:
|
|
50
|
+
"""自動轉換 conditions 為 List[pl.Expr]"""
|
|
51
|
+
if not v:
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
result = []
|
|
55
|
+
for item in v:
|
|
56
|
+
result.append(PolarsExprField.deserialize(item))
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
@field_validator("price_expr", mode="before")
|
|
60
|
+
@classmethod
|
|
61
|
+
def validate_price_expr(cls, v: Any) -> pl.Expr:
|
|
62
|
+
"""自動轉換 price_expr 為 pl.Expr"""
|
|
63
|
+
return PolarsExprField.deserialize(v)
|
|
64
|
+
|
|
65
|
+
@field_serializer("conditions")
|
|
66
|
+
def serialize_conditions(self, conditions: List[pl.Expr]) -> List[dict]:
|
|
67
|
+
"""序列化 conditions 為 dict 列表(與服務器格式一致)"""
|
|
68
|
+
return [PolarsExprField.serialize(expr) for expr in conditions]
|
|
69
|
+
|
|
70
|
+
@field_serializer("price_expr")
|
|
71
|
+
def serialize_price_expr(self, price_expr: pl.Expr) -> dict:
|
|
72
|
+
"""序列化 price_expr 為 dict(與服務器格式一致)"""
|
|
73
|
+
return PolarsExprField.serialize(price_expr)
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(
|
|
76
|
+
arbitrary_types_allowed=True # 允許 pl.Expr 這種自定義類型
|
|
77
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Trading-related models (orders, positions, executions, engagements)."""
|
|
2
|
+
|
|
3
|
+
from tradepose_models.trading.engagement import Engagement
|
|
4
|
+
from tradepose_models.trading.orderbook import OrderbookEntry
|
|
5
|
+
from tradepose_models.trading.orders import (
|
|
6
|
+
ExecutionReport,
|
|
7
|
+
Order,
|
|
8
|
+
OrderSide,
|
|
9
|
+
OrderStatus,
|
|
10
|
+
OrderStrategy,
|
|
11
|
+
OrderSubmitRequest,
|
|
12
|
+
OrderType,
|
|
13
|
+
TimeInForce,
|
|
14
|
+
)
|
|
15
|
+
from tradepose_models.trading.positions import (
|
|
16
|
+
ClosedPosition,
|
|
17
|
+
Position,
|
|
18
|
+
PositionSide,
|
|
19
|
+
)
|
|
20
|
+
from tradepose_models.trading.trader_commands import (
|
|
21
|
+
BaseTraderCommand,
|
|
22
|
+
CancelOrderCommand,
|
|
23
|
+
ExecuteOrderCommand,
|
|
24
|
+
ModifyOrderCommand,
|
|
25
|
+
SyncBrokerStatusCommand,
|
|
26
|
+
)
|
|
27
|
+
from tradepose_models.trading.trades_execution import TradeExecution
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Orders
|
|
31
|
+
"Order",
|
|
32
|
+
"OrderSubmitRequest",
|
|
33
|
+
"ExecutionReport",
|
|
34
|
+
"OrderSide",
|
|
35
|
+
"OrderType",
|
|
36
|
+
"OrderStatus",
|
|
37
|
+
"TimeInForce",
|
|
38
|
+
"OrderStrategy",
|
|
39
|
+
# Positions
|
|
40
|
+
"Position",
|
|
41
|
+
"ClosedPosition",
|
|
42
|
+
"PositionSide",
|
|
43
|
+
# Trade Executions
|
|
44
|
+
"TradeExecution",
|
|
45
|
+
# Engagements
|
|
46
|
+
"Engagement",
|
|
47
|
+
# Orderbook
|
|
48
|
+
"OrderbookEntry",
|
|
49
|
+
# Trader Commands
|
|
50
|
+
"BaseTraderCommand",
|
|
51
|
+
"ExecuteOrderCommand",
|
|
52
|
+
"CancelOrderCommand",
|
|
53
|
+
"ModifyOrderCommand",
|
|
54
|
+
"SyncBrokerStatusCommand",
|
|
55
|
+
]
|