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,828 @@
|
|
|
1
|
+
"""Export 任務管理 API"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import zipfile
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, Dict, List, Tuple, Union, Any
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from ..models import ExportTaskResponse, TaskStatus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExportAPI:
|
|
17
|
+
"""Export 任務管理 API Mixin"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_export(
|
|
21
|
+
self,
|
|
22
|
+
strategy_name: str,
|
|
23
|
+
blueprint_name: str,
|
|
24
|
+
export_type: str = "enhanced-ohlcv",
|
|
25
|
+
start_date: Optional[str] = None,
|
|
26
|
+
end_date: Optional[str] = None,
|
|
27
|
+
strategy_ids: Optional[List[str]] = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""創建 export 任務
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
strategy_name: 策略名稱
|
|
33
|
+
blueprint_name: 藍圖名稱
|
|
34
|
+
export_type: 導出類型 (enhanced-ohlcv, backtest-results, latest-trades)
|
|
35
|
+
start_date: 起始日期 (ISO 8601 格式, 例如 "2020-01-01T00:00:00")
|
|
36
|
+
end_date: 終止日期 (ISO 8601 格式)
|
|
37
|
+
strategy_ids: 策略 ID 列表 (可選)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Export task ID
|
|
41
|
+
"""
|
|
42
|
+
payload = {"strategy_name": strategy_name, "blueprint_name": blueprint_name}
|
|
43
|
+
|
|
44
|
+
if start_date:
|
|
45
|
+
payload["start_date"] = start_date
|
|
46
|
+
if end_date:
|
|
47
|
+
payload["end_date"] = end_date
|
|
48
|
+
if strategy_ids:
|
|
49
|
+
payload["strategy_ids"] = strategy_ids
|
|
50
|
+
|
|
51
|
+
response = requests.post(
|
|
52
|
+
f"{self.api_url}/api/v1/backtest/export/{export_type}",
|
|
53
|
+
json=payload,
|
|
54
|
+
headers=self._get_headers(),
|
|
55
|
+
)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
|
|
58
|
+
result = response.json()
|
|
59
|
+
return result["export_task_id"]
|
|
60
|
+
|
|
61
|
+
def get_export_status(self, task_id: str) -> ExportTaskResponse:
|
|
62
|
+
"""查詢 export 任務狀態
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
task_id: Export task ID
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ExportTaskResponse: 任務狀態信息(Pydantic 模型)
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> status = client.get_export_status(task_id)
|
|
72
|
+
>>> print(f"狀態: {status.status}")
|
|
73
|
+
>>> if status.status == TaskStatus.COMPLETED:
|
|
74
|
+
... print(f"完成時間: {status.completed_at}")
|
|
75
|
+
"""
|
|
76
|
+
response = requests.get(
|
|
77
|
+
f"{self.api_url}/api/v1/backtest/export/{task_id}/status",
|
|
78
|
+
headers=self._get_headers()
|
|
79
|
+
)
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
return ExportTaskResponse.model_validate(response.json())
|
|
82
|
+
|
|
83
|
+
def wait_for_export(
|
|
84
|
+
self,
|
|
85
|
+
task_id: str,
|
|
86
|
+
timeout: int = 300,
|
|
87
|
+
poll_interval: int = 2,
|
|
88
|
+
verbose: bool = True,
|
|
89
|
+
) -> ExportTaskResponse:
|
|
90
|
+
"""等待 export 任務完成
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
task_id: Export task ID
|
|
94
|
+
timeout: 超時時間(秒)
|
|
95
|
+
poll_interval: 輪詢間隔(秒)
|
|
96
|
+
verbose: 是否顯示進度信息
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
ExportTaskResponse: 完成的任務狀態(Pydantic 模型)
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
TimeoutError: 等待超時
|
|
103
|
+
RuntimeError: 任務失敗
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> result = client.wait_for_export(task_id, verbose=True)
|
|
107
|
+
>>> print(f"執行策略: {result.executed_strategies}")
|
|
108
|
+
>>> if result.result_summary:
|
|
109
|
+
... print(f"總交易數: {result.result_summary.total_trades}")
|
|
110
|
+
"""
|
|
111
|
+
start_time = time.time()
|
|
112
|
+
|
|
113
|
+
while True:
|
|
114
|
+
elapsed = time.time() - start_time
|
|
115
|
+
if elapsed > timeout:
|
|
116
|
+
raise TimeoutError(f"等待任務完成超時 ({timeout}秒)")
|
|
117
|
+
|
|
118
|
+
status_info = self.get_export_status(task_id)
|
|
119
|
+
status = status_info.status
|
|
120
|
+
|
|
121
|
+
if verbose:
|
|
122
|
+
print(f"[{elapsed:.1f}s] Status: {status.value}", end="\r")
|
|
123
|
+
|
|
124
|
+
if status == TaskStatus.COMPLETED:
|
|
125
|
+
if verbose:
|
|
126
|
+
print(f"\n✅ 任務完成!耗時 {elapsed:.1f}秒")
|
|
127
|
+
return status_info
|
|
128
|
+
|
|
129
|
+
elif status == TaskStatus.FAILED:
|
|
130
|
+
error = status_info.error or "Unknown error"
|
|
131
|
+
raise RuntimeError(f"任務失敗: {error}")
|
|
132
|
+
|
|
133
|
+
time.sleep(poll_interval)
|
|
134
|
+
|
|
135
|
+
def get_metadata_from_redis(self, task_id: str) -> Dict:
|
|
136
|
+
"""從 Redis 讀取任務 metadata
|
|
137
|
+
|
|
138
|
+
注意: Redis 只存儲 metadata,實際數據需要通過 API 下載
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
task_id: Export task ID
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Metadata 字典
|
|
145
|
+
"""
|
|
146
|
+
metadata_key = f"export:{task_id}:metadata"
|
|
147
|
+
metadata_str = self.redis.get(metadata_key)
|
|
148
|
+
|
|
149
|
+
if not metadata_str:
|
|
150
|
+
raise ValueError(f"找不到任務 {task_id} 的元數據")
|
|
151
|
+
|
|
152
|
+
return json.loads(metadata_str)
|
|
153
|
+
|
|
154
|
+
def _extract_zip_to_memory(
|
|
155
|
+
self, zip_data: Union[BytesIO, bytes]
|
|
156
|
+
) -> Dict[str, BytesIO]:
|
|
157
|
+
"""在內存中解壓 ZIP 文件
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
zip_data: ZIP 文件數據 (BytesIO 或 bytes)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dict[filename, BytesIO] - 文件名到內容的映射
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(zip_data, bytes):
|
|
166
|
+
zip_data = BytesIO(zip_data)
|
|
167
|
+
|
|
168
|
+
files = {}
|
|
169
|
+
with zipfile.ZipFile(zip_data, "r") as zip_ref:
|
|
170
|
+
for filename in zip_ref.namelist():
|
|
171
|
+
files[filename] = BytesIO(zip_ref.read(filename))
|
|
172
|
+
|
|
173
|
+
return files
|
|
174
|
+
|
|
175
|
+
def wait_and_download(
|
|
176
|
+
self,
|
|
177
|
+
task_id: str,
|
|
178
|
+
output_path: Optional[str] = None,
|
|
179
|
+
timeout: int = 300,
|
|
180
|
+
verbose: bool = True,
|
|
181
|
+
) -> pl.DataFrame:
|
|
182
|
+
"""等待任務完成並下載 Parquet 文件(推薦方法)
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
task_id: Export task ID
|
|
186
|
+
output_path: 輸出文件路徑(如果為 None,自動生成)
|
|
187
|
+
timeout: 超時時間(秒)
|
|
188
|
+
verbose: 是否顯示進度
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Polars DataFrame
|
|
192
|
+
"""
|
|
193
|
+
# 等待任務完成
|
|
194
|
+
self.wait_for_export(task_id, timeout=timeout, verbose=verbose)
|
|
195
|
+
|
|
196
|
+
# 下載 Parquet 文件
|
|
197
|
+
parquet_path = self.download_parquet(task_id, output_path)
|
|
198
|
+
|
|
199
|
+
# 讀取並應用 schema
|
|
200
|
+
df = self.load_parquet_with_schema(parquet_path)
|
|
201
|
+
|
|
202
|
+
return df
|
|
203
|
+
|
|
204
|
+
def download_parquet(
|
|
205
|
+
self, task_id: str, output_path: Optional[str] = None
|
|
206
|
+
) -> Union[Path, BytesIO]:
|
|
207
|
+
"""直接從 API 下載 Parquet 文件
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
task_id: Export task ID
|
|
211
|
+
output_path: 輸出文件路徑(如果為 None,使用內存 BytesIO)
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
下載的文件路徑或 BytesIO 對象
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
RuntimeError: 下載失敗
|
|
218
|
+
"""
|
|
219
|
+
# 發送下載請求
|
|
220
|
+
url = f"{self.api_url}/api/v1/backtest/export/{task_id}/download"
|
|
221
|
+
print(f"📥 開始下載: {url}")
|
|
222
|
+
|
|
223
|
+
response = requests.get(url, stream=True, headers=self._get_headers())
|
|
224
|
+
response.raise_for_status()
|
|
225
|
+
|
|
226
|
+
# 如果未指定路徑,使用 BytesIO
|
|
227
|
+
if output_path is None:
|
|
228
|
+
buffer = BytesIO()
|
|
229
|
+
total_size = 0
|
|
230
|
+
|
|
231
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
232
|
+
if chunk:
|
|
233
|
+
buffer.write(chunk)
|
|
234
|
+
total_size += len(chunk)
|
|
235
|
+
|
|
236
|
+
buffer.seek(0) # 重置到開頭以便讀取
|
|
237
|
+
file_size_mb = len(buffer.getvalue()) / (1024 * 1024)
|
|
238
|
+
print(f"✅ 下載完成(內存)")
|
|
239
|
+
print(f"📦 數據大小: {file_size_mb:.2f} MB")
|
|
240
|
+
|
|
241
|
+
return buffer
|
|
242
|
+
|
|
243
|
+
# 保存到文件
|
|
244
|
+
output_path = Path(output_path)
|
|
245
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
|
|
247
|
+
total_size = 0
|
|
248
|
+
with open(output_path, "wb") as f:
|
|
249
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
250
|
+
if chunk:
|
|
251
|
+
f.write(chunk)
|
|
252
|
+
total_size += len(chunk)
|
|
253
|
+
|
|
254
|
+
file_size_mb = output_path.stat().st_size / (1024 * 1024)
|
|
255
|
+
print(f"✅ 下載完成: {output_path}")
|
|
256
|
+
print(f"📦 文件大小: {file_size_mb:.2f} MB")
|
|
257
|
+
|
|
258
|
+
return output_path
|
|
259
|
+
|
|
260
|
+
def load_parquet_with_schema(
|
|
261
|
+
self, file_path: Union[str, Path, BytesIO]
|
|
262
|
+
) -> pl.DataFrame:
|
|
263
|
+
"""讀取 Parquet 文件並應用正確的 schema
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
file_path: Parquet 文件路徑或 BytesIO 對象
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Polars DataFrame (已應用 schema)
|
|
270
|
+
"""
|
|
271
|
+
if isinstance(file_path, BytesIO):
|
|
272
|
+
print(f"📖 讀取 Parquet(從內存)")
|
|
273
|
+
else:
|
|
274
|
+
print(f"📖 讀取 Parquet: {file_path}")
|
|
275
|
+
|
|
276
|
+
# 讀取文件
|
|
277
|
+
df = pl.read_parquet(file_path)
|
|
278
|
+
|
|
279
|
+
# print(df.head())
|
|
280
|
+
|
|
281
|
+
# df = df.cast(enhanced_ohlcv_schema)
|
|
282
|
+
|
|
283
|
+
print(f"✅ 讀取完成: {len(df):,} 行 × {len(df.columns)} 列")
|
|
284
|
+
|
|
285
|
+
return df
|
|
286
|
+
|
|
287
|
+
def save_to_parquet(
|
|
288
|
+
self, df: pl.DataFrame, output_path: str, compression: str = "zstd"
|
|
289
|
+
) -> Path:
|
|
290
|
+
"""保存 DataFrame 為 Parquet 文件
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
df: Polars DataFrame
|
|
294
|
+
output_path: 輸出文件路徑
|
|
295
|
+
compression: 壓縮算法
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
輸出文件路徑
|
|
299
|
+
"""
|
|
300
|
+
output_path = Path(output_path)
|
|
301
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
302
|
+
|
|
303
|
+
df.write_parquet(output_path, compression=compression)
|
|
304
|
+
|
|
305
|
+
file_size_mb = output_path.stat().st_size / (1024 * 1024)
|
|
306
|
+
print(f"✅ 已保存: {output_path}")
|
|
307
|
+
print(f"📦 文件大小: {file_size_mb:.2f} MB")
|
|
308
|
+
print(f"📊 行數: {len(df):,} | 列數: {len(df.columns)}")
|
|
309
|
+
|
|
310
|
+
return output_path
|
|
311
|
+
|
|
312
|
+
def download_and_read_backtest_results(
|
|
313
|
+
self,
|
|
314
|
+
task_id: str,
|
|
315
|
+
save_zip_path: Optional[str] = None,
|
|
316
|
+
save_trades_path: Optional[str] = None,
|
|
317
|
+
save_perf_path: Optional[str] = None,
|
|
318
|
+
) -> Tuple[pl.DataFrame, pl.DataFrame]:
|
|
319
|
+
"""下載並讀取 backtest-results(ZIP 格式)
|
|
320
|
+
|
|
321
|
+
下載 ZIP 文件 → 內存解壓 → 讀取 trades + performance Parquet
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
task_id: Export task ID
|
|
325
|
+
save_zip_path: ZIP 保存路徑(可選,不指定則純內存操作)
|
|
326
|
+
save_trades_path: Trades Parquet 保存路徑(可選)
|
|
327
|
+
save_perf_path: Performance Parquet 保存路徑(可選)
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
(trades_df, performance_df): 兩個 Polars DataFrame
|
|
331
|
+
|
|
332
|
+
Example:
|
|
333
|
+
>>> # 純內存模式
|
|
334
|
+
>>> trades_df, perf_df = client.download_and_read_backtest_results(task_id)
|
|
335
|
+
>>>
|
|
336
|
+
>>> # 保存到本地
|
|
337
|
+
>>> trades_df, perf_df = client.download_and_read_backtest_results(
|
|
338
|
+
... task_id,
|
|
339
|
+
... save_zip_path="./results.zip",
|
|
340
|
+
... save_trades_path="./trades.parquet",
|
|
341
|
+
... save_perf_path="./performance.parquet"
|
|
342
|
+
... )
|
|
343
|
+
"""
|
|
344
|
+
# 下載 ZIP
|
|
345
|
+
print(f"📥 開始下載 backtest-results: {task_id}")
|
|
346
|
+
zip_data = self.download_parquet(task_id, output_path=save_zip_path)
|
|
347
|
+
|
|
348
|
+
# 解壓到內存
|
|
349
|
+
print(f"📦 解壓 ZIP 文件...")
|
|
350
|
+
if isinstance(zip_data, Path):
|
|
351
|
+
with open(zip_data, "rb") as f:
|
|
352
|
+
files = self._extract_zip_to_memory(f.read())
|
|
353
|
+
else:
|
|
354
|
+
files = self._extract_zip_to_memory(zip_data)
|
|
355
|
+
|
|
356
|
+
# 找到 trades 和 performance 文件
|
|
357
|
+
trades_file = None
|
|
358
|
+
perf_file = None
|
|
359
|
+
|
|
360
|
+
for filename, content in files.items():
|
|
361
|
+
if "trades" in filename.lower() and filename.endswith(".parquet"):
|
|
362
|
+
trades_file = content
|
|
363
|
+
elif "performance" in filename.lower() and filename.endswith(".parquet"):
|
|
364
|
+
perf_file = content
|
|
365
|
+
|
|
366
|
+
if not trades_file or not perf_file:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
f"ZIP 文件中找不到 trades 或 performance parquet 文件。文件列表: {list(files.keys())}"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# 讀取 trades
|
|
372
|
+
print(f"📖 讀取 trades.parquet...")
|
|
373
|
+
trades_df = pl.read_parquet(trades_file)
|
|
374
|
+
print(f" ✅ {len(trades_df):,} 行 × {len(trades_df.columns)} 列")
|
|
375
|
+
|
|
376
|
+
# 讀取 performance
|
|
377
|
+
print(f"📖 讀取 performance.parquet...")
|
|
378
|
+
perf_df = pl.read_parquet(perf_file)
|
|
379
|
+
print(f" ✅ {len(perf_df):,} 行 × {len(perf_df.columns)} 列")
|
|
380
|
+
|
|
381
|
+
# 可選:保存到本地
|
|
382
|
+
if save_trades_path:
|
|
383
|
+
self.save_to_parquet(trades_df, save_trades_path)
|
|
384
|
+
|
|
385
|
+
if save_perf_path:
|
|
386
|
+
self.save_to_parquet(perf_df, save_perf_path)
|
|
387
|
+
|
|
388
|
+
return trades_df, perf_df
|
|
389
|
+
|
|
390
|
+
def download_and_read_latest_trades(
|
|
391
|
+
self,
|
|
392
|
+
task_id: str,
|
|
393
|
+
save_path: Optional[str] = None,
|
|
394
|
+
) -> pl.DataFrame:
|
|
395
|
+
"""下載並讀取 latest-trades(Parquet 格式)
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
task_id: Export task ID
|
|
399
|
+
save_path: 保存路徑(可選,不指定則純內存操作)
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Polars DataFrame
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
>>> # 純內存模式
|
|
406
|
+
>>> df = client.download_and_read_latest_trades(task_id)
|
|
407
|
+
>>>
|
|
408
|
+
>>> # 保存到本地
|
|
409
|
+
>>> df = client.download_and_read_latest_trades(task_id, save_path="./trades.parquet")
|
|
410
|
+
"""
|
|
411
|
+
# 下載 Parquet
|
|
412
|
+
print(f"📥 開始下載 latest-trades: {task_id}")
|
|
413
|
+
file_or_buffer = self.download_parquet(task_id, output_path=save_path)
|
|
414
|
+
|
|
415
|
+
# 讀取
|
|
416
|
+
print(f"📖 讀取 latest_trades.parquet...")
|
|
417
|
+
df = pl.read_parquet(file_or_buffer)
|
|
418
|
+
|
|
419
|
+
print(f"✅ 讀取完成: {len(df):,} 行 × {len(df.columns)} 列")
|
|
420
|
+
|
|
421
|
+
return df
|
|
422
|
+
|
|
423
|
+
def download_and_read_enhanced_ohlcv(
|
|
424
|
+
self,
|
|
425
|
+
task_id: str,
|
|
426
|
+
save_path: Optional[str] = None,
|
|
427
|
+
) -> pl.DataFrame:
|
|
428
|
+
"""下載並讀取 enhanced-ohlcv(Parquet 格式)
|
|
429
|
+
|
|
430
|
+
這是 wait_and_download 的別名,提供統一的接口
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
task_id: Export task ID
|
|
434
|
+
save_path: 保存路徑(可選,不指定則純內存操作)
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Polars DataFrame
|
|
438
|
+
"""
|
|
439
|
+
return self.wait_and_download(task_id, output_path=save_path, verbose=True)
|
|
440
|
+
|
|
441
|
+
def quick_export(
|
|
442
|
+
self,
|
|
443
|
+
strategy_name: str,
|
|
444
|
+
blueprint_name: Optional[str] = None,
|
|
445
|
+
start_date: str = "2020-01-01T00:00:00",
|
|
446
|
+
end_date: Optional[str] = None,
|
|
447
|
+
save_path: Optional[str] = None,
|
|
448
|
+
) -> pl.DataFrame:
|
|
449
|
+
"""快速導出(一鍵完成所有步驟)
|
|
450
|
+
|
|
451
|
+
從 API 下載 Parquet 文件並讀取。不依賴 PostgreSQL。
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
strategy_name: 策略名稱
|
|
455
|
+
blueprint_name: 藍圖名稱(如果為 None,自動獲取)
|
|
456
|
+
start_date: 起始日期
|
|
457
|
+
end_date: 終止日期
|
|
458
|
+
save_path: 保存路徑(如果為 None,自動生成臨時文件)
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Polars DataFrame
|
|
462
|
+
|
|
463
|
+
Example:
|
|
464
|
+
>>> client = TradeposeClient()
|
|
465
|
+
>>> df = client.quick_export(
|
|
466
|
+
... strategy_name="txf_1h_sma30_50",
|
|
467
|
+
... save_path="./data/my_data.parquet"
|
|
468
|
+
... )
|
|
469
|
+
"""
|
|
470
|
+
# 獲取 blueprint 名稱
|
|
471
|
+
if blueprint_name is None:
|
|
472
|
+
strategy_detail = self.get_strategy_detail(strategy_name)
|
|
473
|
+
blueprint_name = strategy_detail["base_blueprint"]["name"]
|
|
474
|
+
print(f"📋 使用 blueprint: {blueprint_name}")
|
|
475
|
+
|
|
476
|
+
# 創建 export
|
|
477
|
+
print(f"🚀 創建 export 任務...")
|
|
478
|
+
task_id = self.create_export(
|
|
479
|
+
strategy_name=strategy_name,
|
|
480
|
+
blueprint_name=blueprint_name,
|
|
481
|
+
start_date=start_date,
|
|
482
|
+
end_date=end_date,
|
|
483
|
+
)
|
|
484
|
+
print(f" Task ID: {task_id}")
|
|
485
|
+
|
|
486
|
+
# 等待完成並下載
|
|
487
|
+
print(f"⏳ 等待任務完成...")
|
|
488
|
+
df = self.wait_and_download(task_id, output_path=save_path, verbose=True)
|
|
489
|
+
|
|
490
|
+
return df
|
|
491
|
+
|
|
492
|
+
def quick_backtest_results(
|
|
493
|
+
self,
|
|
494
|
+
strategy_ids: List[str],
|
|
495
|
+
start_date: Optional[str] = None,
|
|
496
|
+
end_date: Optional[str] = None,
|
|
497
|
+
save_zip_path: Optional[str] = None,
|
|
498
|
+
save_trades_path: Optional[str] = None,
|
|
499
|
+
save_perf_path: Optional[str] = None,
|
|
500
|
+
) -> Tuple[pl.DataFrame, pl.DataFrame]:
|
|
501
|
+
"""快速導出回測結果(一鍵完成所有步驟)
|
|
502
|
+
|
|
503
|
+
創建任務 → 等待完成 → 下載 ZIP → 解壓 → 讀取
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
strategy_ids: 策略 ID 列表
|
|
507
|
+
start_date: 起始日期 (ISO 8601)
|
|
508
|
+
end_date: 終止日期 (ISO 8601)
|
|
509
|
+
save_zip_path: ZIP 保存路徑(可選)
|
|
510
|
+
save_trades_path: Trades Parquet 保存路徑(可選)
|
|
511
|
+
save_perf_path: Performance Parquet 保存路徑(可選)
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
(trades_df, performance_df): 兩個 Polars DataFrame
|
|
515
|
+
|
|
516
|
+
Example:
|
|
517
|
+
>>> client = TradeposeClient()
|
|
518
|
+
>>> trades_df, perf_df = client.quick_backtest_results(
|
|
519
|
+
... strategy_ids=["txf_1h_sma30_50", "txf_1d_ema10_20"],
|
|
520
|
+
... save_zip_path="./results.zip"
|
|
521
|
+
... )
|
|
522
|
+
"""
|
|
523
|
+
# 創建任務
|
|
524
|
+
print(f"🚀 創建 backtest-results 導出任務...")
|
|
525
|
+
print(f" 策略列表: {strategy_ids}")
|
|
526
|
+
|
|
527
|
+
task_id = self.create_export(
|
|
528
|
+
strategy_name=strategy_ids[0] if strategy_ids else "",
|
|
529
|
+
blueprint_name="", # backtest-results 不需要 blueprint
|
|
530
|
+
export_type="backtest-results",
|
|
531
|
+
start_date=start_date,
|
|
532
|
+
end_date=end_date,
|
|
533
|
+
strategy_ids=strategy_ids,
|
|
534
|
+
)
|
|
535
|
+
print(f" Task ID: {task_id}")
|
|
536
|
+
|
|
537
|
+
# 等待完成
|
|
538
|
+
print(f"⏳ 等待任務完成...")
|
|
539
|
+
self.wait_for_export(task_id, verbose=True)
|
|
540
|
+
|
|
541
|
+
# 下載並讀取
|
|
542
|
+
return self.download_and_read_backtest_results(
|
|
543
|
+
task_id=task_id,
|
|
544
|
+
save_zip_path=save_zip_path,
|
|
545
|
+
save_trades_path=save_trades_path,
|
|
546
|
+
save_perf_path=save_perf_path,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
def quick_latest_trades(
|
|
550
|
+
self,
|
|
551
|
+
strategy_ids: List[str],
|
|
552
|
+
start_date: Optional[str] = None,
|
|
553
|
+
end_date: Optional[str] = None,
|
|
554
|
+
save_path: Optional[str] = None,
|
|
555
|
+
) -> pl.DataFrame:
|
|
556
|
+
"""快速導出最新交易(一鍵完成所有步驟)
|
|
557
|
+
|
|
558
|
+
創建任務 → 等待完成 → 下載 Parquet → 讀取
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
strategy_ids: 策略 ID 列表
|
|
562
|
+
start_date: 起始日期 (ISO 8601)
|
|
563
|
+
end_date: 終止日期 (ISO 8601)
|
|
564
|
+
save_path: Parquet 保存路徑(可選)
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
Polars DataFrame
|
|
568
|
+
|
|
569
|
+
Example:
|
|
570
|
+
>>> client = TradeposeClient()
|
|
571
|
+
>>> df = client.quick_latest_trades(
|
|
572
|
+
... strategy_ids=["txf_1h_sma30_50"],
|
|
573
|
+
... save_path="./latest_trades.parquet"
|
|
574
|
+
... )
|
|
575
|
+
"""
|
|
576
|
+
# 創建任務
|
|
577
|
+
print(f"🚀 創建 latest-trades 導出任務...")
|
|
578
|
+
print(f" 策略列表: {strategy_ids}")
|
|
579
|
+
|
|
580
|
+
task_id = self.create_export(
|
|
581
|
+
strategy_name=strategy_ids[0] if strategy_ids else "",
|
|
582
|
+
blueprint_name="", # latest-trades 不需要 blueprint
|
|
583
|
+
export_type="latest-trades",
|
|
584
|
+
start_date=start_date,
|
|
585
|
+
end_date=end_date,
|
|
586
|
+
strategy_ids=strategy_ids,
|
|
587
|
+
)
|
|
588
|
+
print(f" Task ID: {task_id}")
|
|
589
|
+
|
|
590
|
+
# 等待完成
|
|
591
|
+
print(f"⏳ 等待任務完成...")
|
|
592
|
+
self.wait_for_export(task_id, verbose=True)
|
|
593
|
+
|
|
594
|
+
# 下載並讀取
|
|
595
|
+
return self.download_and_read_latest_trades(
|
|
596
|
+
task_id=task_id, save_path=save_path
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
def quick_enhanced_ohlcv(
|
|
600
|
+
self,
|
|
601
|
+
strategy_name: str,
|
|
602
|
+
blueprint_name: Optional[str] = None,
|
|
603
|
+
start_date: str = "2020-01-01T00:00:00",
|
|
604
|
+
end_date: Optional[str] = None,
|
|
605
|
+
save_path: Optional[str] = None,
|
|
606
|
+
) -> pl.DataFrame:
|
|
607
|
+
"""快速導出 enhanced-ohlcv(一鍵完成所有步驟)
|
|
608
|
+
|
|
609
|
+
創建任務 → 等待完成 → 下載 Parquet → 讀取
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
strategy_name: 策略名稱
|
|
613
|
+
blueprint_name: 藍圖名稱(如果為 None,自動獲取)
|
|
614
|
+
start_date: 起始日期 (ISO 8601)
|
|
615
|
+
end_date: 終止日期 (ISO 8601)
|
|
616
|
+
save_path: Parquet 保存路徑(可選)
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Polars DataFrame
|
|
620
|
+
|
|
621
|
+
Example:
|
|
622
|
+
>>> client = TradeposeClient()
|
|
623
|
+
>>> df = client.quick_enhanced_ohlcv(
|
|
624
|
+
... strategy_name="txf_1h_sma30_50",
|
|
625
|
+
... blueprint_name="txf_base_trend",
|
|
626
|
+
... save_path="./enhanced_ohlcv.parquet"
|
|
627
|
+
... )
|
|
628
|
+
"""
|
|
629
|
+
# 獲取 blueprint 名稱
|
|
630
|
+
if blueprint_name is None:
|
|
631
|
+
strategy_detail = self.get_strategy_detail(strategy_name)
|
|
632
|
+
blueprint_name = strategy_detail["base_blueprint"]["name"]
|
|
633
|
+
print(f"📋 使用 blueprint: {blueprint_name}")
|
|
634
|
+
|
|
635
|
+
# 創建任務
|
|
636
|
+
print(f"🚀 創建 enhanced-ohlcv 導出任務...")
|
|
637
|
+
print(f" 策略名稱: {strategy_name}")
|
|
638
|
+
print(f" Blueprint: {blueprint_name}")
|
|
639
|
+
|
|
640
|
+
task_id = self.create_export(
|
|
641
|
+
strategy_name=strategy_name,
|
|
642
|
+
blueprint_name=blueprint_name,
|
|
643
|
+
export_type="enhanced-ohlcv",
|
|
644
|
+
start_date=start_date,
|
|
645
|
+
end_date=end_date,
|
|
646
|
+
)
|
|
647
|
+
print(f" Task ID: {task_id}")
|
|
648
|
+
|
|
649
|
+
# 等待完成
|
|
650
|
+
print(f"⏳ 等待任務完成...")
|
|
651
|
+
self.wait_for_export(task_id, verbose=True)
|
|
652
|
+
|
|
653
|
+
# 下載並讀取
|
|
654
|
+
return self.download_and_read_enhanced_ohlcv(
|
|
655
|
+
task_id=task_id, save_path=save_path
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
def create_on_demand_ohlcv_export(
|
|
659
|
+
self,
|
|
660
|
+
base_instrument: str,
|
|
661
|
+
base_freq: str,
|
|
662
|
+
indicator_specs: List[Dict[str, Any]],
|
|
663
|
+
start_date: Optional[str] = None,
|
|
664
|
+
end_date: Optional[str] = None,
|
|
665
|
+
) -> str:
|
|
666
|
+
"""創建 on-demand-ohlcv 導出任務
|
|
667
|
+
|
|
668
|
+
特點:無需預先註冊策略,直接指定指標規格即可導出
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
base_instrument: 基礎商品 ID (例如 "TXFR1", "TXF_M1_SHIOAJI_FUTURE")
|
|
672
|
+
base_freq: 基礎頻率 ("1min", "5min", "1h", "1D" 等)
|
|
673
|
+
indicator_specs: 指標規格列表 (IndicatorSpec JSON 數組)
|
|
674
|
+
start_date: 開始時間 (ISO 8601 格式, 例如 "2025-01-01T00:00:00")
|
|
675
|
+
end_date: 結束時間 (ISO 8601 格式)
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
Export task ID
|
|
679
|
+
|
|
680
|
+
Example:
|
|
681
|
+
>>> # 方式 1: 使用 dict
|
|
682
|
+
>>> specs = [
|
|
683
|
+
... {
|
|
684
|
+
... "freq": "1h",
|
|
685
|
+
... "shift": 1,
|
|
686
|
+
... "indicator": {"type": "SMA", "period": 20, "column": "close"}
|
|
687
|
+
... },
|
|
688
|
+
... {
|
|
689
|
+
... "freq": "1h",
|
|
690
|
+
... "shift": 1,
|
|
691
|
+
... "indicator": {"type": "ATR", "period": 14}
|
|
692
|
+
... }
|
|
693
|
+
... ]
|
|
694
|
+
>>> task_id = client.create_on_demand_ohlcv_export(
|
|
695
|
+
... base_instrument="TXFR1",
|
|
696
|
+
... base_freq="1min",
|
|
697
|
+
... indicator_specs=specs
|
|
698
|
+
... )
|
|
699
|
+
|
|
700
|
+
>>> # 方式 2: 使用 IndicatorSpec.model_dump()
|
|
701
|
+
>>> from tradepose_client.models import create_indicator_spec, Indicator, Freq
|
|
702
|
+
>>>
|
|
703
|
+
>>> sma_spec = create_indicator_spec(
|
|
704
|
+
... freq=Freq.HOUR_1,
|
|
705
|
+
... indicator=Indicator.sma(period=20),
|
|
706
|
+
... shift=1
|
|
707
|
+
... )
|
|
708
|
+
>>> atr_spec = create_indicator_spec(
|
|
709
|
+
... freq=Freq.HOUR_1,
|
|
710
|
+
... indicator=Indicator.atr(period=14),
|
|
711
|
+
... shift=1
|
|
712
|
+
... )
|
|
713
|
+
>>>
|
|
714
|
+
>>> task_id = client.create_on_demand_ohlcv_export(
|
|
715
|
+
... base_instrument="TXFR1",
|
|
716
|
+
... base_freq="1min",
|
|
717
|
+
... indicator_specs=[
|
|
718
|
+
... sma_spec.model_dump(exclude_none=True),
|
|
719
|
+
... atr_spec.model_dump(exclude_none=True)
|
|
720
|
+
... ]
|
|
721
|
+
... )
|
|
722
|
+
"""
|
|
723
|
+
payload = {
|
|
724
|
+
"base_instrument": base_instrument,
|
|
725
|
+
"base_freq": base_freq,
|
|
726
|
+
"indicator_specs": indicator_specs,
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if start_date:
|
|
730
|
+
payload["start_date"] = start_date
|
|
731
|
+
if end_date:
|
|
732
|
+
payload["end_date"] = end_date
|
|
733
|
+
|
|
734
|
+
response = requests.post(
|
|
735
|
+
f"{self.api_url}/api/v1/backtest/export/on-demand-ohlcv",
|
|
736
|
+
json=payload,
|
|
737
|
+
headers=self._get_headers(),
|
|
738
|
+
)
|
|
739
|
+
response.raise_for_status()
|
|
740
|
+
|
|
741
|
+
result = response.json()
|
|
742
|
+
return result["export_task_id"]
|
|
743
|
+
|
|
744
|
+
def quick_on_demand_ohlcv(
|
|
745
|
+
self,
|
|
746
|
+
base_instrument: str,
|
|
747
|
+
base_freq: str,
|
|
748
|
+
indicator_specs: List[Dict[str, Any]],
|
|
749
|
+
start_date: Optional[str] = None,
|
|
750
|
+
end_date: Optional[str] = None,
|
|
751
|
+
save_path: Optional[str] = None,
|
|
752
|
+
) -> pl.DataFrame:
|
|
753
|
+
"""快速導出 on-demand-ohlcv(一鍵完成所有步驟)
|
|
754
|
+
|
|
755
|
+
創建任務 → 等待完成 → 下載 Parquet → 讀取
|
|
756
|
+
|
|
757
|
+
特點:無需預先註冊策略,直接指定指標規格即可導出
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
base_instrument: 基礎商品 ID
|
|
761
|
+
base_freq: 基礎頻率
|
|
762
|
+
indicator_specs: 指標規格列表
|
|
763
|
+
start_date: 開始時間 (ISO 8601)
|
|
764
|
+
end_date: 結束時間 (ISO 8601)
|
|
765
|
+
save_path: Parquet 保存路徑(可選)
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Polars DataFrame
|
|
769
|
+
|
|
770
|
+
Example:
|
|
771
|
+
>>> from tradepose_client import TradeposeClient
|
|
772
|
+
>>> from tradepose_client.models import create_indicator_spec, Indicator, Freq
|
|
773
|
+
>>>
|
|
774
|
+
>>> client = TradeposeClient(
|
|
775
|
+
... api_url="http://localhost:8080",
|
|
776
|
+
... api_token="your_jwt_token"
|
|
777
|
+
... )
|
|
778
|
+
>>>
|
|
779
|
+
>>> # 定義指標
|
|
780
|
+
>>> sma_20 = create_indicator_spec(Freq.HOUR_1, Indicator.sma(20), shift=1)
|
|
781
|
+
>>> ema_50 = create_indicator_spec(Freq.HOUR_1, Indicator.ema(50), shift=1)
|
|
782
|
+
>>> atr_14 = create_indicator_spec(Freq.HOUR_1, Indicator.atr(14), shift=1)
|
|
783
|
+
>>>
|
|
784
|
+
>>> # 快速導出(無需註冊策略)
|
|
785
|
+
>>> df = client.quick_on_demand_ohlcv(
|
|
786
|
+
... base_instrument="TXFR1",
|
|
787
|
+
... base_freq="1min",
|
|
788
|
+
... indicator_specs=[
|
|
789
|
+
... sma_20.model_dump(exclude_none=True),
|
|
790
|
+
... ema_50.model_dump(exclude_none=True),
|
|
791
|
+
... atr_14.model_dump(exclude_none=True)
|
|
792
|
+
... ],
|
|
793
|
+
... start_date="2025-01-01T00:00:00",
|
|
794
|
+
... end_date="2025-01-02T23:59:59",
|
|
795
|
+
... save_path="./on_demand_data.parquet"
|
|
796
|
+
... )
|
|
797
|
+
>>>
|
|
798
|
+
>>> print(df.columns)
|
|
799
|
+
>>> # ['ts', 'open', 'high', 'low', 'close', 'volume',
|
|
800
|
+
>>> # '1h_SMA|20.close', '1h_EMA|50.close', '1h_ATR|14']
|
|
801
|
+
"""
|
|
802
|
+
# 創建任務
|
|
803
|
+
print(f"🚀 創建 on-demand-ohlcv 導出任務...")
|
|
804
|
+
print(f" 商品: {base_instrument}")
|
|
805
|
+
print(f" 頻率: {base_freq}")
|
|
806
|
+
print(f" 指標數: {len(indicator_specs)}")
|
|
807
|
+
|
|
808
|
+
task_id = self.create_on_demand_ohlcv_export(
|
|
809
|
+
base_instrument=base_instrument,
|
|
810
|
+
base_freq=base_freq,
|
|
811
|
+
indicator_specs=indicator_specs,
|
|
812
|
+
start_date=start_date,
|
|
813
|
+
end_date=end_date,
|
|
814
|
+
)
|
|
815
|
+
print(f" Task ID: {task_id}")
|
|
816
|
+
|
|
817
|
+
# 等待完成
|
|
818
|
+
print(f"⏳ 等待任務完成...")
|
|
819
|
+
self.wait_for_export(task_id, verbose=True)
|
|
820
|
+
|
|
821
|
+
# 下載並讀取
|
|
822
|
+
print(f"📥 下載並讀取數據...")
|
|
823
|
+
file_or_buffer = self.download_parquet(task_id, output_path=save_path)
|
|
824
|
+
df = pl.read_parquet(file_or_buffer)
|
|
825
|
+
|
|
826
|
+
print(f"✅ 完成: {len(df):,} 行 × {len(df.columns)} 列")
|
|
827
|
+
|
|
828
|
+
return df
|