docs+style: README 隆重介绍 v1.29 AI 深度辅助(认知自由叙事);ruff format 对齐 CI

This commit is contained in:
GitHub
2026-09-02 20:23:27 +08:00
parent 4bd5b5d833
commit a9dabe4682
6 changed files with 63 additions and 26 deletions
+12 -1
View File
@@ -17,10 +17,21 @@ easy-tdx 要做的事很简单:**把机构的数据锁砸开,扔到每个普
**34个技术指标**MACD、KDJ、RSI、BOLL……连”捉妖大师”和”30日乖离率信号”都给你算好)开箱即用。 **34个技术指标**MACD、KDJ、RSI、BOLL……连”捉妖大师”和”30日乖离率信号”都给你算好)开箱即用。
**缠论分析**(笔、中枢、买卖点、背驰)一键出结果——你不再需要手画分型、猜线段。 **缠论分析**(笔、中枢、买卖点、背驰)一键出结果——你不再需要手画分型、猜线段。
**内置回测引擎**——写个策略文件,一行命令跑回测,18 个经典策略自带,多因子组合、策略选股扫描,批量对比哪个最赚钱一目了然。**防过拟合验证链**(v1.25 新增)——Walk-Forward 七窗样本外验证(每窗独立开仓)、训练/验证/测试三段适配性体检(8 项可解释检查 + 「高适配」标记)、0-100 综合评分、多 seed 晋级门槛、买入持有基准对比,回测页勾选即出报告——「回测好」升级为「样本外也好」。**深度风险报告**(v1.28 新增)——绩效指标扩到 25 项(新增 Ulcer 指数、95% 日 VaR/CVaR、SQN 系统质量数、最大连胜/连亏);基准对比从超额收益一个数升级为 α/β/信息比率/跟踪误差四件套(「涨的时候跟不跟得上大盘、跌的时候抗不抗跌」一眼可读);策略一行带移动止损与百分比止损止盈(`self.buy(trail_stop=0.08)` 涨得越高止损线跟得越高,OCO 任一触发即失效);引擎行为由黄金测试锁定——19 个内置策略的指标基线进 CI,撮合/费率逻辑任何静默漂移当场爆红。 **内置回测引擎**——写个策略文件,一行命令跑回测,19 个经典策略自带,多因子组合、策略选股扫描,批量对比哪个最赚钱一目了然。**防过拟合验证链**(v1.25 新增)——Walk-Forward 七窗样本外验证(每窗独立开仓)、训练/验证/测试三段适配性体检(8 项可解释检查 + 「高适配」标记)、0-100 综合评分、多 seed 晋级门槛、买入持有基准对比,回测页勾选即出报告——「回测好」升级为「样本外也好」。**深度风险报告**(v1.28 新增)——绩效指标扩到 25 项(新增 Ulcer 指数、95% 日 VaR/CVaR、SQN 系统质量数、最大连胜/连亏);基准对比从超额收益一个数升级为 α/β/信息比率/跟踪误差四件套(「涨的时候跟不跟得上大盘、跌的时候抗不抗跌」一眼可读);策略一行带移动止损与百分比止损止盈(`self.buy(trail_stop=0.08)` 涨得越高止损线跟得越高,OCO 任一触发即失效);引擎行为由黄金测试锁定——19 个内置策略的指标基线进 CI,撮合/费率逻辑任何静默漂移当场爆红。
**行情终端 Web UI**v1.23 重大升级)——`easy-tdx serve` 一条命令,浏览器秒变专业看盘终端:**市场看板**(五大指数实时推送 + 迷你分时、涨跌统计、四维情绪雷达、全市场涨跌分布直方图、涨停雷达、行业/概念热冷榜、涨幅/跌幅/成交额/换手四联排行榜、两市异动雷达)、**自选行情**(输入 6 位代码即加,全表 SSE 实时刷新、行内迷你分时)、**个股详情弹窗**(五档盘口 + 1/3/5 日分时 + 带 MA/BOLL/MACD/KDJ/RSI 指标切换的日 K,一键加自选、一键全策略寻优)、**板块下钻**(行业/概念弹窗看板块走势 + 成分股涨跌榜直达个股)。实时推送采用 SSE 单循环轮询 fan-out 架构(盘中 8 秒一拍,无人订阅自动休眠),自选持久化 SQLite。展示层设计对标专业终端(暗色、红涨绿跌、高信息密度),数据全部来自通达信协议直连——不花一分钱。 **行情终端 Web UI**v1.23 重大升级)——`easy-tdx serve` 一条命令,浏览器秒变专业看盘终端:**市场看板**(五大指数实时推送 + 迷你分时、涨跌统计、四维情绪雷达、全市场涨跌分布直方图、涨停雷达、行业/概念热冷榜、涨幅/跌幅/成交额/换手四联排行榜、两市异动雷达)、**自选行情**(输入 6 位代码即加,全表 SSE 实时刷新、行内迷你分时)、**个股详情弹窗**(五档盘口 + 1/3/5 日分时 + 带 MA/BOLL/MACD/KDJ/RSI 指标切换的日 K,一键加自选、一键全策略寻优)、**板块下钻**(行业/概念弹窗看板块走势 + 成分股涨跌榜直达个股)。实时推送采用 SSE 单循环轮询 fan-out 架构(盘中 8 秒一拍,无人订阅自动休眠),自选持久化 SQLite。展示层设计对标专业终端(暗色、红涨绿跌、高信息密度),数据全部来自通达信协议直连——不花一分钱。
**🤖 AI 深度辅助:让 AI 读报告、陪你复盘(v1.29 重大升级)**——量化工具最大的门槛从来不是取数,而是**看不懂报告**:25 项绩效指标、Walk-Forward 逐窗、α/β/信息比率、S-D 评级……对普通人就是天书。v1.28.1 的「AI 解读 Prompt 导出」要把提示词复制出去手动粘贴;v1.29 起升级为**配置你自己的模型、一键直接解读**:
- **AI 设置页**——9 家 Provider 开箱即用(DeepSeek / 通义千问 / **智谱 GLM** / Kimi / MiniMax / OpenAI / Claude / Ollama 本地免 Key / 自定义任意兼容网关),API 地址与模型可改,连通性一键测试。配置落盘 `~/.easy_tdx/llm.json`,环境变量可兜底——**你的 Key 只属于你**,本工具不代管、不代理、不经手第三方。
- **✨ 一键直接解读**——回测页把整份报告(配置 + 25 项指标 + WF + 评估 + 评级 + 成交摘要)组装成提示词,点击后作为**后台任务**发给你配置的模型,前端轮询进度、实时显示已耗时,不占连接、刷新页面不丢结果。模型回来的是「做了十几年量化的老手朋友」口吻的通俗解读、改进建议与 0-10 信心分——**把读报告这件事外包给 AI,你只做决策**。
- **AI 解读历史**——每次解读自动归档(提问 + 正文 + 当时的策略/参数/标的/区间),随时翻旧账对比「同一个策略上次 AI 怎么说」;每条带「→ 去回测(带参数)」一键复现当时场景,看完解读直接回去调参。
- **为 AI 时代做的工程加固**——思考型模型(GLM-5.x / DeepSeek-R1 等)思考链计入 token 预算导致正文空白?显式拦截并给出可操作提示;模型慢?读超时可调、后台任务不超时;解读内容一律标注「AI 生成,仅供参考,不构成投资建议」。
**这一步的意义**:取数自由(v1.0)→ 回测自由(v1.17)→ 防过拟合(v1.25)→ **认知自由(v1.29**。前几步解决「数据与工具的不平等」,这一步解决「解读能力的不平等」——机构有研究员写晨报,你现在有自己的 AI 分析师。仍是 MIT 开源、零注册、零平台费,唯一成本是你自己模型的调用费(用 Ollama 本地模型连这个都是零)。
**v1.29 同场加映**(借鉴社区 Fork [swimmingaaron/easy_tdx](https://github.com/swimmingaaron/easy_tdx) 的好想法,逐项甄别后移植):**ZIG 右侧突破回补策略**(MyTT 新增之字转向指标,波谷建仓带硬止损 + 见顶记前高 + 右侧突破回补,对冲未来函数的前视偏差);**交易时段感知刷新**(看板轮询休市自动暂停、三态状态栏、手动刷新不受限,不再半夜空转刷数据);**120 分钟 K 线**(MAC 原生两级降级获取)与**逐 bar 涨跌幅/振幅字段**(`pre_close`/`change_pct`/`amplitude_pct` 直接返回);**159 只核心龙头池**(东财全行业龙头名单 → `universe=core` 一键只扫龙头,配「龙头池」页面;名单仅为扫描范围,不构成任何推荐);全站**风险提示与免责声明**常驻。WebUI 工程加固:未知 API 路径返回 JSON 404 而非 HTML、入口页 `no-store` 防缓存旧版——更新即所见。
<img src="./docs/web-ui-page-4.png" alt="行情终端 Web UIv1.23):市场看板 / 自选行情 / 个股与板块详情" /> <img src="./docs/web-ui-page-4.png" alt="行情终端 Web UIv1.23):市场看板 / 自选行情 / 个股与板块详情" />
**回测可视化 Web UI**v1.17 新增)——Vue3 + ECharts 单页应用,浏览器里选标的、挑策略、调参数,K 线买卖点、净值回撤、25 项绩效指标一目了然。支持组合回测、参数网格寻优、多策略结果对比,**还能把好策略存进策略库(SQLite 持久化),勾选多个策略做资金分仓组合回测看综合表现**,全程零代码。v1.27 起新增「附加分析」开关:勾选后随回测自动跑 Walk-Forward 逐窗柱状图与一条龙评估报告(评分分项 / 高适配徽标 / 买入持有对比)。v1.28.1 起新手与 AI 辅助两连击:三个报告框内置**名词解释折叠帮助**(33 个词条讲清每项指标是什么、怎么算、怎么看,默认收起点击展开,重点粗体、阈值橙色、细节细体);回测报告一键导出 **AI 解读 Prompt**——把配置 + 25 项指标 + WF 逐窗 + 一条龙评估 + 评级 + 成交摘要组装成结构化提示词,复制发给 ChatGPT / Claude / DeepSeek / 豆包,即可获得「老手朋友」口吻的通俗解读、改进建议与 **0-10 信心分**(附「该不该执行」行动刻度)。 **回测可视化 Web UI**v1.17 新增)——Vue3 + ECharts 单页应用,浏览器里选标的、挑策略、调参数,K 线买卖点、净值回撤、25 项绩效指标一目了然。支持组合回测、参数网格寻优、多策略结果对比,**还能把好策略存进策略库(SQLite 持久化),勾选多个策略做资金分仓组合回测看综合表现**,全程零代码。v1.27 起新增「附加分析」开关:勾选后随回测自动跑 Walk-Forward 逐窗柱状图与一条龙评估报告(评分分项 / 高适配徽标 / 买入持有对比)。v1.28.1 起新手与 AI 辅助两连击:三个报告框内置**名词解释折叠帮助**(33 个词条讲清每项指标是什么、怎么算、怎么看,默认收起点击展开,重点粗体、阈值橙色、细节细体);回测报告一键导出 **AI 解读 Prompt**——把配置 + 25 项指标 + WF 逐窗 + 一条龙评估 + 评级 + 成交摘要组装成结构化提示词,复制发给 ChatGPT / Claude / DeepSeek / 豆包,即可获得「老手朋友」口吻的通俗解读、改进建议与 **0-10 信心分**(附「该不该执行」行动刻度)。
+1 -2
View File
@@ -201,8 +201,7 @@ def resolve_config(cfg: LlmConfig | None = None) -> LlmConfig:
c.model = preset.default_model c.model = preset.default_model
if not c.api_url or not c.model: if not c.api_url or not c.model:
raise ValueError( raise ValueError(
f"LLM 配置不完整:provider={c.provider} 缺少 api_url 或 model" f"LLM 配置不完整:provider={c.provider} 缺少 api_url 或 model请在 AI 设置中补全"
"请在 AI 设置中补全"
) )
return c return c
+1 -2
View File
@@ -66,8 +66,7 @@ def session_info(now: datetime | None = None, *, tz: tzinfo | None = None) -> di
return { return {
"is_trading_time": is_trading_time(t), "is_trading_time": is_trading_time(t),
"sessions": [ "sessions": [
{"start": s.strftime("%H:%M"), "end": e.strftime("%H:%M")} {"start": s.strftime("%H:%M"), "end": e.strftime("%H:%M")} for s, e in SESSION_WINDOWS
for s, e in SESSION_WINDOWS
], ],
"session_desc": SESSION_DESC, "session_desc": SESSION_DESC,
"server_time": t.isoformat(timespec="seconds"), "server_time": t.isoformat(timespec="seconds"),
+5 -1
View File
@@ -206,7 +206,11 @@ async def _fetch_120m(
_MAX_BARS_PER_FETCH // 2, _MAX_BARS_PER_FETCH // 2,
) )
df = await client.get_security_bars( df = await client.get_security_bars(
market_from_str(market), code, category_from_str("MIN_60"), start, fetch_n, market_from_str(market),
code,
category_from_str("MIN_60"),
start,
fetch_n,
bar_time=bar_time, bar_time=bar_time,
) )
return _resample_pairs(df, count) return _resample_pairs(df, count)
+13 -9
View File
@@ -65,12 +65,8 @@ class LlmChatRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=200_000) prompt: str = Field(..., min_length=1, max_length=200_000)
system_prompt: str | None = Field(None, description="None = 用配置里的默认系统提示") system_prompt: str | None = Field(None, description="None = 用配置里的默认系统提示")
override: LlmConfigUpdate | None = Field( override: LlmConfigUpdate | None = Field(None, description="临时覆盖配置(不落盘,仅本次调用)")
None, description="临时覆盖配置(不落盘,仅本次调用" context: LlmChatContext | None = Field(None, description="策略上下文(随成功解读一并落历史库")
)
context: LlmChatContext | None = Field(
None, description="策略上下文(随成功解读一并落历史库)"
)
def _merge_api_key(submitted: str, current: str) -> str: def _merge_api_key(submitted: str, current: str) -> str:
@@ -153,7 +149,11 @@ async def update_llm_config(req: LlmConfigUpdate) -> dict[str, Any]:
def _record_history( def _record_history(
provider: str, model: str, prompt: str, reply: str, elapsed: float, provider: str,
model: str,
prompt: str,
reply: str,
elapsed: float,
ctx: LlmChatContext | None, ctx: LlmChatContext | None,
) -> None: ) -> None:
"""成功解读旁路落库(llm_history.db)。失败只记日志,不影响解读结果。""" """成功解读旁路落库(llm_history.db)。失败只记日志,不影响解读结果。"""
@@ -217,8 +217,12 @@ async def llm_chat_async(req: LlmChatRequest) -> TaskSubmitResponse:
_record_history( _record_history(
client.config.provider, client.config.model, req.prompt, reply, elapsed, req.context client.config.provider, client.config.model, req.prompt, reply, elapsed, req.context
) )
return {"reply": reply, "model": client.config.model, "provider": client.config.provider, return {
"elapsed": elapsed} "reply": reply,
"model": client.config.model,
"provider": client.config.provider,
"elapsed": elapsed,
}
runner = get_runner() runner = get_runner()
task_id = runner.submit(_run, description=desc) task_id = runner.submit(_run, description=desc)
+31 -11
View File
@@ -146,9 +146,7 @@ class TestClient:
def fake_urlopen(req, timeout): def fake_urlopen(req, timeout):
body = io.BytesIO(b'{"error":"bad key"}') body = io.BytesIO(b'{"error":"bad key"}')
raise urllib.error.HTTPError( raise urllib.error.HTTPError(req.full_url, 401, "Unauthorized", hdrs=None, fp=body)
req.full_url, 401, "Unauthorized", hdrs=None, fp=body
)
monkeypatch.setattr(llm_mod.urllib.request, "urlopen", fake_urlopen) monkeypatch.setattr(llm_mod.urllib.request, "urlopen", fake_urlopen)
with pytest.raises(LlmError, match="401") as ei: with pytest.raises(LlmError, match="401") as ei:
@@ -164,8 +162,15 @@ class TestClient:
def test_provider_presets_cover_major_vendors(): def test_provider_presets_cover_major_vendors():
vendors = [ vendors = [
"deepseek", "qwen", "zhipu", "kimi", "minimax", "deepseek",
"openai", "claude", "ollama", "custom", "qwen",
"zhipu",
"kimi",
"minimax",
"openai",
"claude",
"ollama",
"custom",
] ]
for pid in vendors: for pid in vendors:
assert pid in PROVIDER_PRESETS, pid assert pid in PROVIDER_PRESETS, pid
@@ -224,6 +229,7 @@ class TestAsyncChatTask:
async def _slow(): async def _slow():
await asyncio.sleep(0.05) await asyncio.sleep(0.05)
return f"解读:{prompt[:8]}" return f"解读:{prompt[:8]}"
return _slow() return _slow()
monkeypatch.setattr(LlmClient, "chat", fake_chat) monkeypatch.setattr(LlmClient, "chat", fake_chat)
@@ -254,6 +260,7 @@ class TestAsyncChatTask:
def fake_chat(self, prompt, system_prompt=None): def fake_chat(self, prompt, system_prompt=None):
async def _boom(): async def _boom():
raise LlmError("请求超时(180s 内无响应)") raise LlmError("请求超时(180s 内无响应)")
return _boom() return _boom()
monkeypatch.setattr(LlmClient, "chat", fake_chat) monkeypatch.setattr(LlmClient, "chat", fake_chat)
@@ -304,8 +311,9 @@ class TestAsyncChatTask:
"start_date": "2024-01-01", "start_date": "2024-01-01",
"end_date": "2025-01-01", "end_date": "2025-01-01",
} }
tid = c.post("/api/v1/llm/chat/async", tid = c.post("/api/v1/llm/chat/async", json={"prompt": "报告", "context": ctx}).json()[
json={"prompt": "报告", "context": ctx}).json()["task_id"] "task_id"
]
for _ in range(50): for _ in range(50):
st = c.get(f"/api/v1/llm/chat/tasks/{tid}").json() st = c.get(f"/api/v1/llm/chat/tasks/{tid}").json()
if st["status"] in ("done", "failed"): if st["status"] in ("done", "failed"):
@@ -373,8 +381,14 @@ class TestThinkingModelBlankContent:
""" """
def _client(self, max_tokens: int = 4000) -> LlmClient: def _client(self, max_tokens: int = 4000) -> LlmClient:
return LlmClient(LlmConfig(provider="zhipu", api_key="sk-x-1234567890", return LlmClient(
model="glm-5.3-flash", max_tokens=max_tokens)) LlmConfig(
provider="zhipu",
api_key="sk-x-1234567890",
model="glm-5.3-flash",
max_tokens=max_tokens,
)
)
def test_normal_content_wins_over_reasoning(self): def test_normal_content_wins_over_reasoning(self):
msg = {"content": "正文", "reasoning_content": "思考…", "role": "assistant"} msg = {"content": "正文", "reasoning_content": "思考…", "role": "assistant"}
@@ -403,8 +417,14 @@ class TestThinkingModelBlankContent:
def fake_post(url, headers, payload, timeout): def fake_post(url, headers, payload, timeout):
blank = chr(10) + " " + chr(10) blank = chr(10) + " " + chr(10)
return {"choices": [{"message": {"content": blank, "reasoning_content": "r"}, return {
"finish_reason": "length"}]} "choices": [
{
"message": {"content": blank, "reasoning_content": "r"},
"finish_reason": "length",
}
]
}
monkeypatch.setattr(llm_mod, "_post_json", fake_post) monkeypatch.setattr(llm_mod, "_post_json", fake_post)
with pytest.raises(LlmError, match="思考链"): with pytest.raises(LlmError, match="思考链"):