pandaai-cli 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.
- cli.py +442 -0
- pandaai/__init__.py +12 -0
- pandaai/auth.py +103 -0
- pandaai/billing.py +20 -0
- pandaai/config.py +40 -0
- pandaai/constants.py +15 -0
- pandaai/download.py +41 -0
- pandaai/factor.py +96 -0
- pandaai/factor_core.py +283 -0
- pandaai/factor_list.py +183 -0
- pandaai/formatting.py +79 -0
- pandaai/output.py +99 -0
- pandaai_cli-0.1.0.dist-info/METADATA +451 -0
- pandaai_cli-0.1.0.dist-info/RECORD +19 -0
- pandaai_cli-0.1.0.dist-info/WHEEL +5 -0
- pandaai_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pandaai_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- pandaai_cli-0.1.0.dist-info/top_level.txt +3 -0
- template.py +578 -0
pandaai/factor.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""因子分析结果格式化"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
# ─── 因子分析接口列表(前端 FactorAnalysisChartControl 全屏查看时调用) ───
|
|
6
|
+
|
|
7
|
+
FACTOR_ANALYSIS_ENDPOINTS = [
|
|
8
|
+
"query_factor_analysis_data",
|
|
9
|
+
"query_one_group_data",
|
|
10
|
+
"query_group_return_analysis",
|
|
11
|
+
"query_last_date_top_factor",
|
|
12
|
+
"query_return_chart",
|
|
13
|
+
"query_ic_decay_chart",
|
|
14
|
+
"query_ic_density_chart",
|
|
15
|
+
"query_ic_sequence_chart",
|
|
16
|
+
"query_ic_self_correlation_chart",
|
|
17
|
+
"query_rank_ic_decay_chart",
|
|
18
|
+
"query_rank_ic_density_chart",
|
|
19
|
+
"query_rank_ic_sequence_chart",
|
|
20
|
+
"query_rank_ic_self_correlation_chart",
|
|
21
|
+
"query_factor_excess_chart",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_factor_analysis_results(results: dict) -> str:
|
|
26
|
+
"""格式化因子分析结果为可读文本"""
|
|
27
|
+
lines = []
|
|
28
|
+
fa = results.get("factor_analysis", {})
|
|
29
|
+
|
|
30
|
+
# ── 核心绩效(query_one_group_data) ──
|
|
31
|
+
one_group = fa.get("query_one_group_data", {})
|
|
32
|
+
if isinstance(one_group, dict):
|
|
33
|
+
core_items = [
|
|
34
|
+
("因子收益", one_group.get("return_ratio", "")),
|
|
35
|
+
("夏普比率", one_group.get("sharpe_ratio", "")),
|
|
36
|
+
("年化收益", one_group.get("annualized_ratio", "")),
|
|
37
|
+
("最大回撤", one_group.get("maximum_drawdown", "")),
|
|
38
|
+
]
|
|
39
|
+
lines.append("━━━ 核心绩效 ━━━")
|
|
40
|
+
lines.append(f"{'指标':<22} {'数值':<15}")
|
|
41
|
+
lines.append("-" * 37)
|
|
42
|
+
for label, val in core_items:
|
|
43
|
+
lines.append(f"{label:<22} {val:<15}")
|
|
44
|
+
|
|
45
|
+
# ── 因子分析指标 ──
|
|
46
|
+
indicators = fa.get("query_factor_analysis_data", [])
|
|
47
|
+
if not isinstance(indicators, list):
|
|
48
|
+
indicators = indicators.get("factor_data_analysis", []) if isinstance(indicators, dict) else []
|
|
49
|
+
if indicators:
|
|
50
|
+
lines.append("━━━ 因子分析指标 ━━━")
|
|
51
|
+
lines.append(f"{'指标':<22} {'数值':<15}")
|
|
52
|
+
lines.append("-" * 37)
|
|
53
|
+
for item in indicators:
|
|
54
|
+
if isinstance(item, dict):
|
|
55
|
+
lines.append(f"{item['indicator']:<22} {item.get('factor1', item.get('value', '')):<15}")
|
|
56
|
+
|
|
57
|
+
# ── 分组收益 ──
|
|
58
|
+
groups = fa.get("query_group_return_analysis", [])
|
|
59
|
+
if not isinstance(groups, list):
|
|
60
|
+
groups = groups.get("group_return_analysis", []) if isinstance(groups, dict) else []
|
|
61
|
+
if groups:
|
|
62
|
+
lines.append("\n━━━ 分组收益分析 ━━━")
|
|
63
|
+
header = f"{'分组':<8} {'年化收益':<12} {'超额收益':<12} {'夏普比率':<10} {'最大回撤':<10} {'月胜率':<10}"
|
|
64
|
+
lines.append(header)
|
|
65
|
+
lines.append("-" * len(header))
|
|
66
|
+
for g in groups:
|
|
67
|
+
lines.append(
|
|
68
|
+
f"{g.get('group',''):<8} "
|
|
69
|
+
f"{g.get('annualizedReturn',''):<12} "
|
|
70
|
+
f"{g.get('excessAnnualized',''):<12} "
|
|
71
|
+
f"{g.get('sharpeRatio',''):<10} "
|
|
72
|
+
f"{g.get('maxDrawdown',''):<10} "
|
|
73
|
+
f"{g.get('monthlyWinRate',''):<10}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# ── 最新日期 Top 因子 ──
|
|
77
|
+
top_factors = fa.get("query_last_date_top_factor", [])
|
|
78
|
+
if not isinstance(top_factors, list):
|
|
79
|
+
top_factors = top_factors.get("last_date_top_factor", []) if isinstance(top_factors, dict) else []
|
|
80
|
+
if top_factors:
|
|
81
|
+
lines.append("\n━━━ 最新日期 Top 因子 ━━━")
|
|
82
|
+
lines.append(f"{'股票代码':<16} {'名称':<10} {'因子值':<10}")
|
|
83
|
+
lines.append("-" * 36)
|
|
84
|
+
for item in top_factors[:10]:
|
|
85
|
+
lines.append(f"{item.get('symbol',''):<16} {item.get('name',''):<10} {item.get('factor1',''):<10}")
|
|
86
|
+
|
|
87
|
+
# ── 图表数据 ──
|
|
88
|
+
chart_keys = [k for k in fa if k.endswith("_chart") and fa[k] is not None]
|
|
89
|
+
if chart_keys:
|
|
90
|
+
lines.append(f"\n━━━ 图表数据 ━━━")
|
|
91
|
+
for key in sorted(chart_keys):
|
|
92
|
+
chart = fa[key]
|
|
93
|
+
lines.append(f"\n [{key}]")
|
|
94
|
+
lines.append(json.dumps(chart, ensure_ascii=False, indent=2))
|
|
95
|
+
|
|
96
|
+
return "\n".join(lines) if lines else "(无分析结果)"
|
pandaai/factor_core.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""因子分析创建、执行、轮询"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .auth import api_url, make_headers
|
|
11
|
+
from .constants import STATUS_PENDING, STATUS_RUNNING, STATUS_SUCCESS, STATUS_FAILED, STATUS_BILLING_STOP
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _fail(error_type: str, message: str) -> None:
|
|
15
|
+
from .formatting import format_failure
|
|
16
|
+
print(json.dumps(format_failure(status="ERROR", error_type=error_type, message=message), ensure_ascii=False))
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_factor(config: dict, token: str, uid: str, factor_json: dict) -> str:
|
|
21
|
+
"""保存因子分析,返回 factor_id"""
|
|
22
|
+
url = api_url(config, "/quantflow/api/workflow/save")
|
|
23
|
+
try:
|
|
24
|
+
resp = requests.post(url, json=factor_json, headers=make_headers(token=token, uid=uid), timeout=15)
|
|
25
|
+
data = resp.json()
|
|
26
|
+
except Exception as e:
|
|
27
|
+
return _fail("CREATE_FAILED", f"创建因子分析请求失败: {e}")
|
|
28
|
+
|
|
29
|
+
if resp.status_code == 402:
|
|
30
|
+
detail = data.get("detail", {})
|
|
31
|
+
if isinstance(detail, dict):
|
|
32
|
+
level = detail.get("memberLevel", "?")
|
|
33
|
+
recommend = detail.get("recommendLevelCode", "?")
|
|
34
|
+
msg = (f"因子分析数量已达上限 (已用{detail.get('currentUsage','?')}/{detail.get('quotaValue','?')}) "
|
|
35
|
+
f"| 当前会员: {level} | 推荐升级: {recommend}")
|
|
36
|
+
return _fail("CREATE_FAILED", msg)
|
|
37
|
+
return _fail("CREATE_FAILED", f"创建因子分析失败: {resp.text[:300]}")
|
|
38
|
+
if resp.status_code >= 400:
|
|
39
|
+
return _fail("CREATE_FAILED", f"创建因子分析失败(HTTP{resp.status_code}): {resp.text[:300]}")
|
|
40
|
+
if data.get("code") != 0:
|
|
41
|
+
return _fail("CREATE_FAILED", f"创建因子分析失败: {data.get('message', data.get('detail', resp.text[:200]))}")
|
|
42
|
+
|
|
43
|
+
wf_id = data.get("data", {}).get("workflow_id")
|
|
44
|
+
if not wf_id:
|
|
45
|
+
return _fail("CREATE_FAILED", "响应中没有 workflow_id")
|
|
46
|
+
|
|
47
|
+
return wf_id
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_factor(config: dict, token: str, uid: str, factor_id: str):
|
|
51
|
+
"""启动因子分析运行,返回 (factor_run_id, error_message)。使用默认算力 cpu=4, mem=8, gpu=4"""
|
|
52
|
+
url = api_url(config, "/quantflow/api/workflow/run_with_store")
|
|
53
|
+
body = {
|
|
54
|
+
"workflow_id": factor_id,
|
|
55
|
+
"server_cpu": 4,
|
|
56
|
+
"server_memory": 8,
|
|
57
|
+
"server_gpu": 4,
|
|
58
|
+
}
|
|
59
|
+
try:
|
|
60
|
+
resp = requests.post(url, json=body,
|
|
61
|
+
headers=make_headers(token=token, uid=uid, extra={"quantflow-auth": "2"}), timeout=15)
|
|
62
|
+
data = resp.json()
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return None, f"启动因子分析请求失败: {e}"
|
|
65
|
+
|
|
66
|
+
if resp.status_code >= 400:
|
|
67
|
+
detail = data.get("detail", data.get("message", ""))
|
|
68
|
+
if isinstance(detail, dict):
|
|
69
|
+
detail = json.dumps(detail, ensure_ascii=False)
|
|
70
|
+
return None, f"HTTP {resp.status_code}, code={data.get('code')}: {detail or resp.text[:300]}"
|
|
71
|
+
|
|
72
|
+
run_id = data.get("data", {}).get("workflow_run_id")
|
|
73
|
+
if not run_id:
|
|
74
|
+
return None, f"响应中没有 workflow_run_id: {resp.text[:300]}"
|
|
75
|
+
|
|
76
|
+
return run_id, None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def poll_factor_run(config: dict, token: str, uid: str, run_id: str,
|
|
80
|
+
poll_interval: int = 2, timeout: int = 600) -> Dict[str, Any]:
|
|
81
|
+
"""轮询因子分析运行状态,直到完成/失败/超时"""
|
|
82
|
+
start_ts = time.time()
|
|
83
|
+
url = api_url(config, "/quantflow/api/workflow/run")
|
|
84
|
+
log_url = api_url(config, "/quantflow/api/workflow/run/log")
|
|
85
|
+
|
|
86
|
+
while True:
|
|
87
|
+
elapsed = time.time() - start_ts
|
|
88
|
+
if elapsed > timeout:
|
|
89
|
+
return {"status": "TIMEOUT", "start_time": None, "end_time": None,
|
|
90
|
+
"failed_node_ids": [], "errors": [{"message": f"轮询超时 ({timeout}s)"}]}
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
resp = requests.get(url, params={"workflow_run_id": run_id},
|
|
94
|
+
headers=make_headers(token=token, uid=uid), timeout=10)
|
|
95
|
+
data = resp.json()
|
|
96
|
+
except Exception:
|
|
97
|
+
time.sleep(poll_interval)
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
if data.get("code") != 0:
|
|
101
|
+
time.sleep(poll_interval)
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
run_data = data.get("data", {})
|
|
105
|
+
status = run_data.get("status", STATUS_RUNNING)
|
|
106
|
+
failed_node_ids = run_data.get("failed_node_ids", [])
|
|
107
|
+
start_time = run_data.get("start_time")
|
|
108
|
+
end_time = run_data.get("end_time")
|
|
109
|
+
|
|
110
|
+
if status == STATUS_SUCCESS:
|
|
111
|
+
return {"status": status, "start_time": start_time, "end_time": end_time,
|
|
112
|
+
"failed_node_ids": [], "errors": []}
|
|
113
|
+
|
|
114
|
+
if status == STATUS_FAILED:
|
|
115
|
+
errors = _fetch_factor_run_errors(config, token, uid, run_id, log_url)
|
|
116
|
+
return {"status": status, "start_time": start_time, "end_time": end_time,
|
|
117
|
+
"failed_node_ids": failed_node_ids, "errors": errors}
|
|
118
|
+
|
|
119
|
+
if status == STATUS_BILLING_STOP:
|
|
120
|
+
return {"status": status, "start_time": start_time, "end_time": end_time,
|
|
121
|
+
"failed_node_ids": [], "errors": [{"message": "余额不足,因子分析已被服务端终止"}]}
|
|
122
|
+
|
|
123
|
+
time.sleep(poll_interval)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _fetch_factor_run_errors(config: dict, token: str, uid: str, run_id: str, log_url: str) -> list:
|
|
127
|
+
"""从运行日志中提取节点错误信息"""
|
|
128
|
+
try:
|
|
129
|
+
resp = requests.get(log_url, params={"workflow_run_id": run_id},
|
|
130
|
+
headers=make_headers(token=token, uid=uid), timeout=10)
|
|
131
|
+
data = resp.json()
|
|
132
|
+
if data.get("code") != 0:
|
|
133
|
+
return [{"message": f"获取日志失败, code={data.get('code')}, raw={resp.text[:500]}"}]
|
|
134
|
+
|
|
135
|
+
log_data = data.get("data", {})
|
|
136
|
+
node_errors = []
|
|
137
|
+
nodes_log = log_data.get("nodes", log_data) if isinstance(log_data, dict) else {}
|
|
138
|
+
for node_uuid, node_info in nodes_log.items() if isinstance(nodes_log, dict) else []:
|
|
139
|
+
if isinstance(node_info, dict) and node_info.get("error"):
|
|
140
|
+
node_errors.append({
|
|
141
|
+
"node_uuid": node_uuid,
|
|
142
|
+
"node_title": node_info.get("title", ""),
|
|
143
|
+
"error_message": str(node_info.get("error", "")),
|
|
144
|
+
})
|
|
145
|
+
if not node_errors:
|
|
146
|
+
return [{"message": "因子分析执行失败(无详细错误信息)", "raw_log_data": log_data}]
|
|
147
|
+
return node_errors
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return [{"message": f"获取错误详情失败: {e}"}]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_factor(config: dict, token: str, uid: str, factor_id: str) -> Optional[dict]:
|
|
153
|
+
"""获取单个因子完整定义"""
|
|
154
|
+
url = api_url(config, "/quantflow/api/workflow/query")
|
|
155
|
+
try:
|
|
156
|
+
resp = requests.get(url, params={"workflow_id": factor_id},
|
|
157
|
+
headers=make_headers(token=token, uid=uid), timeout=10)
|
|
158
|
+
data = resp.json()
|
|
159
|
+
# 响应直接是 workflow JSON(含 nodes、litegraph 等)
|
|
160
|
+
if isinstance(data, dict) and data.get("nodes"):
|
|
161
|
+
return data
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def update_factor(config: dict, token: str, uid: str, factor_id: str, factor_json: dict) -> str:
|
|
168
|
+
"""更新因子(upsert),返回 factor_id"""
|
|
169
|
+
# 设置为更新模式
|
|
170
|
+
factor_json["id"] = factor_id
|
|
171
|
+
|
|
172
|
+
# 移除 save 接口不接受的元数据字段(这些是 query 返回的只读字段)
|
|
173
|
+
for key in ("owner", "create_at", "update_at", "feature_tag", "last_run_id",
|
|
174
|
+
"publish_status", "subscription_id", "_id"):
|
|
175
|
+
factor_json.pop(key, None)
|
|
176
|
+
|
|
177
|
+
url = api_url(config, "/quantflow/api/workflow/save")
|
|
178
|
+
try:
|
|
179
|
+
resp = requests.post(url, json=factor_json, headers=make_headers(token=token, uid=uid), timeout=15)
|
|
180
|
+
data = resp.json()
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return _fail("UPDATE_FAILED", f"更新因子分析请求失败: {e}")
|
|
183
|
+
|
|
184
|
+
if resp.status_code >= 400:
|
|
185
|
+
return _fail("UPDATE_FAILED", f"更新因子分析失败(HTTP{resp.status_code}): {resp.text[:300]}")
|
|
186
|
+
if data.get("code") != 0:
|
|
187
|
+
return _fail("UPDATE_FAILED", f"更新因子分析失败: {data.get('message', data.get('detail', resp.text[:200]))}")
|
|
188
|
+
|
|
189
|
+
wf_id = data.get("data", {}).get("workflow_id")
|
|
190
|
+
if not wf_id:
|
|
191
|
+
return _fail("UPDATE_FAILED", "响应中没有 workflow_id")
|
|
192
|
+
return wf_id
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def extract_factor_info(factor_json: dict) -> dict:
|
|
196
|
+
"""从因子完整 JSON 中提取关键信息"""
|
|
197
|
+
info = {
|
|
198
|
+
"_id": factor_json.get("_id", ""),
|
|
199
|
+
"name": factor_json.get("name", ""),
|
|
200
|
+
"create_at": factor_json.get("create_at"),
|
|
201
|
+
"last_run_id": factor_json.get("last_run_id", ""),
|
|
202
|
+
"mode": None, # "code" or "formula"
|
|
203
|
+
"content": "", # 代码或公式文本
|
|
204
|
+
"start_date": "",
|
|
205
|
+
"end_date": "",
|
|
206
|
+
"market": "",
|
|
207
|
+
"adjustment_cycle": "",
|
|
208
|
+
"group_number": "",
|
|
209
|
+
"factor_direction": "",
|
|
210
|
+
"stock_pool": "",
|
|
211
|
+
}
|
|
212
|
+
for node in factor_json.get("nodes", []):
|
|
213
|
+
sid = node.get("static_input_data", {})
|
|
214
|
+
ntype = node.get("name", "")
|
|
215
|
+
if ntype == "CodeControl":
|
|
216
|
+
info["content"] = sid.get("code", "")
|
|
217
|
+
elif ntype == "FormulaControl":
|
|
218
|
+
info["content"] = sid.get("formulas", "")
|
|
219
|
+
info["mode"] = "formula"
|
|
220
|
+
elif ntype == "FactorBuildProControl":
|
|
221
|
+
info["start_date"] = sid.get("start_date", "")
|
|
222
|
+
info["end_date"] = sid.get("end_date", "")
|
|
223
|
+
info["market"] = sid.get("market", "")
|
|
224
|
+
ftype = sid.get("type", "")
|
|
225
|
+
info["mode"] = "code" if ftype == "Python" else "formula"
|
|
226
|
+
elif ntype == "FactorAnalysisControl":
|
|
227
|
+
info["adjustment_cycle"] = sid.get("adjustment_cycle", "")
|
|
228
|
+
info["group_number"] = sid.get("group_number", "")
|
|
229
|
+
info["factor_direction"] = sid.get("factor_direction", "")
|
|
230
|
+
info["stock_pool"] = sid.get("stock_pool", "")
|
|
231
|
+
return info
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def apply_factor_updates(factor_json: dict, updates: dict) -> dict:
|
|
235
|
+
"""将更新应用到因子 JSON 的对应节点上"""
|
|
236
|
+
for node in factor_json.get("nodes", []):
|
|
237
|
+
sid = node.get("static_input_data", {})
|
|
238
|
+
ntype = node.get("name", "")
|
|
239
|
+
|
|
240
|
+
if ntype == "CodeControl":
|
|
241
|
+
if "content" in updates:
|
|
242
|
+
sid["code"] = updates["content"]
|
|
243
|
+
|
|
244
|
+
elif ntype == "FormulaControl":
|
|
245
|
+
if "content" in updates:
|
|
246
|
+
sid["formulas"] = updates["content"]
|
|
247
|
+
|
|
248
|
+
elif ntype == "FactorBuildProControl":
|
|
249
|
+
if "start_date" in updates:
|
|
250
|
+
sid["start_date"] = updates["start_date"]
|
|
251
|
+
if "end_date" in updates:
|
|
252
|
+
sid["end_date"] = updates["end_date"]
|
|
253
|
+
if "market" in updates:
|
|
254
|
+
sid["market"] = updates["market"]
|
|
255
|
+
if "mode" in updates:
|
|
256
|
+
sid["type"] = "Python" if updates["mode"] == "code" else "公式"
|
|
257
|
+
|
|
258
|
+
elif ntype == "FactorAnalysisControl":
|
|
259
|
+
if "adjustment_cycle" in updates:
|
|
260
|
+
sid["adjustment_cycle"] = updates["adjustment_cycle"]
|
|
261
|
+
if "factor_direction" in updates:
|
|
262
|
+
sid["factor_direction"] = updates["factor_direction"]
|
|
263
|
+
# group_number 和 stock_pool 强制不变
|
|
264
|
+
sid["group_number"] = "10"
|
|
265
|
+
sid["stock_pool"] = "沪深全A"
|
|
266
|
+
|
|
267
|
+
if "name" in updates:
|
|
268
|
+
factor_json["name"] = updates["name"]
|
|
269
|
+
return factor_json
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_factor_output(config: dict, token: str, uid: str, factor_id: str) -> Optional[dict]:
|
|
273
|
+
"""获取因子分析最后一次运行的输出"""
|
|
274
|
+
url = api_url(config, "/quantflow/api/workflow/run/output/by-last-run")
|
|
275
|
+
try:
|
|
276
|
+
resp = requests.get(url, params={"workflow_id": factor_id, "feature_tag": "factor"},
|
|
277
|
+
headers=make_headers(token=token, uid=uid), timeout=30)
|
|
278
|
+
data = resp.json()
|
|
279
|
+
if data.get("code") == 0:
|
|
280
|
+
return data.get("data", {})
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
return None
|
pandaai/factor_list.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""因子列表与删除"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from .auth import api_url, make_headers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def list_factors(config: dict, token: str, uid: str, limit: int = 100, offset: int = 0) -> tuple:
|
|
11
|
+
"""获取用户因子列表,返回 (factors, total_count)"""
|
|
12
|
+
url = api_url(config, "/quantflow/api/workflow/all")
|
|
13
|
+
try:
|
|
14
|
+
resp = requests.get(url, headers=make_headers(token=token, uid=uid),
|
|
15
|
+
params={"limit": limit, "offset": offset, "source": "user"}, timeout=10)
|
|
16
|
+
data = resp.json()
|
|
17
|
+
if data.get("code") == 200:
|
|
18
|
+
wf_data = data.get("data", {})
|
|
19
|
+
return wf_data.get("workflows", []), wf_data.get("total_count", 0)
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
return None, None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def delete_factors(config: dict, token: str, uid: str, factor_ids: list) -> tuple:
|
|
26
|
+
"""批量删除因子,返回 (deleted_count, error)"""
|
|
27
|
+
url = api_url(config, "/quantflow/api/workflow/delete")
|
|
28
|
+
batch_size = 50
|
|
29
|
+
deleted = 0
|
|
30
|
+
for i in range(0, len(factor_ids), batch_size):
|
|
31
|
+
batch = factor_ids[i:i + batch_size]
|
|
32
|
+
try:
|
|
33
|
+
resp = requests.post(url, json={"workflow_id_list": batch},
|
|
34
|
+
headers=make_headers(token=token, uid=uid), timeout=30)
|
|
35
|
+
if resp.status_code == 200:
|
|
36
|
+
deleted += len(batch)
|
|
37
|
+
else:
|
|
38
|
+
return deleted, f"HTTP {resp.status_code}: {resp.text[:200]}"
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return deleted, str(e)
|
|
41
|
+
return deleted, None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def enrich_factors_with_runs(config: dict, token: str, uid: str, factors: list) -> list:
|
|
45
|
+
"""为因子列表补充最后一次运行时间和 query_factor_analysis_data 摘要"""
|
|
46
|
+
from .output import get_factor_run_detail, extract_task_id
|
|
47
|
+
|
|
48
|
+
enriched = []
|
|
49
|
+
for factor in factors:
|
|
50
|
+
entry = dict(factor)
|
|
51
|
+
entry["_last_run_time"] = ""
|
|
52
|
+
entry["_analysis_summary"] = ""
|
|
53
|
+
|
|
54
|
+
run_id = factor.get("last_run_id")
|
|
55
|
+
if run_id:
|
|
56
|
+
detail = get_factor_run_detail(config, token, uid, run_id)
|
|
57
|
+
if detail:
|
|
58
|
+
# 提取最后一次运行时间
|
|
59
|
+
end_time = detail.get("end_time")
|
|
60
|
+
if end_time:
|
|
61
|
+
entry["_last_run_time"] = end_time
|
|
62
|
+
|
|
63
|
+
# 提取 query_factor_analysis_data 摘要
|
|
64
|
+
task_id = extract_task_id(config, token, uid, run_id)
|
|
65
|
+
if task_id:
|
|
66
|
+
fa_data = _fetch_analysis_summary(config, token, uid, task_id)
|
|
67
|
+
entry["_analysis_summary"] = fa_data
|
|
68
|
+
|
|
69
|
+
enriched.append(entry)
|
|
70
|
+
return enriched
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _fetch_analysis_summary(config: dict, token: str, uid: str, task_id: str) -> str:
|
|
74
|
+
"""获取 14 个指标摘要,按界面 5 行布局:
|
|
75
|
+
第1行: 因子收益 夏普比率 年化收益
|
|
76
|
+
第2行: 最大回撤 IC_mean Rank_IC
|
|
77
|
+
第3行: IC_std IC_IR IR
|
|
78
|
+
第4行: P(IC<-0.02) P(IC>0.02) t统计量
|
|
79
|
+
第5行: p-value 单调性
|
|
80
|
+
"""
|
|
81
|
+
prefix = api_url(config, "/quantflow/api/factor/")
|
|
82
|
+
indicators = {} # name → value
|
|
83
|
+
|
|
84
|
+
# 1. query_one_group_data → 因子收益、夏普比率、年化收益、最大回撤
|
|
85
|
+
try:
|
|
86
|
+
resp = requests.get(f"{prefix}query_one_group_data",
|
|
87
|
+
params={"task_id": task_id},
|
|
88
|
+
headers=make_headers(token=token, uid=uid), timeout=15)
|
|
89
|
+
data = resp.json()
|
|
90
|
+
if data.get("code") in ("200", 200):
|
|
91
|
+
g = data.get("data", {}).get("one_group_data", {})
|
|
92
|
+
if isinstance(g, dict):
|
|
93
|
+
indicators["因子收益"] = g.get("return_ratio", "")
|
|
94
|
+
indicators["夏普比率"] = g.get("sharpe_ratio", "")
|
|
95
|
+
indicators["年化收益"] = g.get("annualized_ratio", "")
|
|
96
|
+
indicators["最大回撤"] = g.get("maximum_drawdown", "")
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# 2. query_factor_analysis_data → IC 系列等 10 个指标
|
|
101
|
+
try:
|
|
102
|
+
resp = requests.get(f"{prefix}query_factor_analysis_data",
|
|
103
|
+
params={"task_id": task_id},
|
|
104
|
+
headers=make_headers(token=token, uid=uid), timeout=15)
|
|
105
|
+
data = resp.json()
|
|
106
|
+
if data.get("code") in ("200", 200):
|
|
107
|
+
ep_data = data.get("data", {})
|
|
108
|
+
items = ep_data.get("factor_data_analysis", [])
|
|
109
|
+
if not items:
|
|
110
|
+
for k, v in ep_data.items():
|
|
111
|
+
if k != "task_id" and isinstance(v, list):
|
|
112
|
+
items = v
|
|
113
|
+
break
|
|
114
|
+
if items and isinstance(items, list):
|
|
115
|
+
for item in items:
|
|
116
|
+
if isinstance(item, dict):
|
|
117
|
+
name = item.get("indicator", "")
|
|
118
|
+
val = item.get("factor1", item.get("value", ""))
|
|
119
|
+
if name:
|
|
120
|
+
indicators[name] = val
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# 按界面布局顺序输出(14 个指标一行)
|
|
125
|
+
order = [
|
|
126
|
+
"因子收益", "夏普比率", "年化收益",
|
|
127
|
+
"最大回撤", "IC_mean", "Rank_IC",
|
|
128
|
+
"IC_std", "IC_IR", "IR",
|
|
129
|
+
"P(IC<-0.02)", "P(IC>0.02)", "t统计量",
|
|
130
|
+
"p-value", "单调性",
|
|
131
|
+
]
|
|
132
|
+
parts = []
|
|
133
|
+
for name in order:
|
|
134
|
+
val = indicators.get(name, "")
|
|
135
|
+
parts.append(f"{name} {val}" if val else name)
|
|
136
|
+
return " ".join(parts)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def format_factor_list(factors: list, total_count: int = None, limit: int = None, offset: int = None) -> str:
|
|
140
|
+
"""格式化因子列表为可读文本"""
|
|
141
|
+
from datetime import datetime as dt
|
|
142
|
+
|
|
143
|
+
if total_count is None:
|
|
144
|
+
total_count = len(factors)
|
|
145
|
+
|
|
146
|
+
lines = []
|
|
147
|
+
if limit and total_count > limit:
|
|
148
|
+
lines.append(f"第 {offset + 1}-{offset + len(factors)} 条 / 共 {total_count} 个因子:\n")
|
|
149
|
+
else:
|
|
150
|
+
lines.append(f"共 {total_count} 个因子:\n")
|
|
151
|
+
|
|
152
|
+
lines.append(f"{'名称':<24} {'创建时间':<22} {'最后运行时间':<22} {'Run ID':<28} {'分析摘要'}")
|
|
153
|
+
lines.append("-" * 130)
|
|
154
|
+
|
|
155
|
+
for factor in factors:
|
|
156
|
+
name = (factor.get("name") or "")[:22]
|
|
157
|
+
factor_id = factor.get("_id", "")
|
|
158
|
+
|
|
159
|
+
created = ""
|
|
160
|
+
if factor.get("create_at"):
|
|
161
|
+
try:
|
|
162
|
+
created = dt.fromtimestamp(factor["create_at"] / 1000).strftime("%Y-%m-%d %H:%M:%S")
|
|
163
|
+
except Exception:
|
|
164
|
+
created = str(factor["create_at"])
|
|
165
|
+
|
|
166
|
+
last_run_time = factor.get("_last_run_time", "")
|
|
167
|
+
if last_run_time:
|
|
168
|
+
try:
|
|
169
|
+
for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"]:
|
|
170
|
+
try:
|
|
171
|
+
last_run_time = dt.strptime(last_run_time[:26], fmt).strftime("%Y-%m-%d %H:%M:%S")
|
|
172
|
+
break
|
|
173
|
+
except (ValueError, IndexError):
|
|
174
|
+
continue
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
run_id = (factor.get("last_run_id") or "")
|
|
179
|
+
summary = (factor.get("_analysis_summary") or "")
|
|
180
|
+
|
|
181
|
+
lines.append(f"{name:<24} {created:<22} {last_run_time:<22} {run_id:<28} {summary}")
|
|
182
|
+
|
|
183
|
+
return "\n".join(lines)
|
pandaai/formatting.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""结果格式化与计费计算"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _calc_duration(start_time: str, end_time: str) -> Optional[float]:
|
|
8
|
+
"""计算运行时长(秒)"""
|
|
9
|
+
if not start_time or not end_time:
|
|
10
|
+
return None
|
|
11
|
+
try:
|
|
12
|
+
for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f"]:
|
|
13
|
+
try:
|
|
14
|
+
st = datetime.strptime(start_time[:26], fmt)
|
|
15
|
+
et = datetime.strptime(end_time[:26], fmt)
|
|
16
|
+
return (et - st).total_seconds()
|
|
17
|
+
except (ValueError, IndexError):
|
|
18
|
+
continue
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_success(factor_id, run_id, run_result, balance_start, balance_end, output):
|
|
25
|
+
"""格式化成功结果"""
|
|
26
|
+
duration = _calc_duration(run_result.get("start_time"), run_result.get("end_time"))
|
|
27
|
+
deducted = None
|
|
28
|
+
power_start = balance_start.get("computingPower") if isinstance(balance_start, dict) else None
|
|
29
|
+
power_end = balance_end.get("computingPower") if isinstance(balance_end, dict) else None
|
|
30
|
+
if power_start is not None and power_end is not None:
|
|
31
|
+
deducted = float(power_start) - float(power_end)
|
|
32
|
+
balance_val = power_end
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"success": True, "status": "SUCCESS",
|
|
36
|
+
"factor_id": factor_id, "factor_run_id": run_id,
|
|
37
|
+
"duration_seconds": duration,
|
|
38
|
+
"start_time": run_result.get("start_time"),
|
|
39
|
+
"end_time": run_result.get("end_time"),
|
|
40
|
+
"output": output,
|
|
41
|
+
"billing": {
|
|
42
|
+
"balance": balance_val,
|
|
43
|
+
"deducted": deducted if deducted else 0,
|
|
44
|
+
"status": "ok",
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_failure(status, error_type, message, factor_id=None, run_id=None,
|
|
50
|
+
run_result=None, balance_start=None, balance_end=None):
|
|
51
|
+
"""格式化失败结果"""
|
|
52
|
+
run_result = run_result or {}
|
|
53
|
+
duration = _calc_duration(run_result.get("start_time"), run_result.get("end_time"))
|
|
54
|
+
deducted = None
|
|
55
|
+
power_start = balance_start.get("computingPower") if isinstance(balance_start, dict) else None
|
|
56
|
+
power_end = balance_end.get("computingPower") if isinstance(balance_end, dict) else None
|
|
57
|
+
if power_start is not None and power_end is not None:
|
|
58
|
+
deducted = float(power_start) - float(power_end)
|
|
59
|
+
balance_val = power_end
|
|
60
|
+
|
|
61
|
+
error_block = {"type": error_type, "message": message}
|
|
62
|
+
if run_result.get("errors"):
|
|
63
|
+
error_block["node_errors"] = run_result["errors"]
|
|
64
|
+
if run_result.get("failed_node_ids"):
|
|
65
|
+
error_block["failed_node_ids"] = run_result["failed_node_ids"]
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"success": False, "status": status,
|
|
69
|
+
"factor_id": factor_id, "factor_run_id": run_id,
|
|
70
|
+
"duration_seconds": duration,
|
|
71
|
+
"start_time": run_result.get("start_time"),
|
|
72
|
+
"end_time": run_result.get("end_time"),
|
|
73
|
+
"error": error_block,
|
|
74
|
+
"billing": {
|
|
75
|
+
"balance": balance_val if balance_end is not None else balance_start,
|
|
76
|
+
"deducted": deducted if deducted else 0,
|
|
77
|
+
"status": "insufficient" if error_type == "INSUFFICIENT_BALANCE" else "ok",
|
|
78
|
+
},
|
|
79
|
+
}
|