mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 22:44:22 +08:00
- ZIG 右侧突破回补策略:MyTT 新增 ZIG 之字转向(未来函数,含前视偏差警示); 波谷启动建仓挂硬止损(OCO)→ 见顶清仓记前高 → 右侧突破回补;路径依赖不实现 entry_exit_masks(向量化守护测试白名单);含寻优预设网格与 --strategy-file 独立文件 - 交易时段感知刷新:realtime/session.py(09:15~11:30:30 / 13:00~15:05)+ GET /market/session;看板 30/60/120s 轮询休市自动暂停(三态状态栏 + 开关持久化 + 手动刷新不受限);SSE/WS 既有会话语义不动 - 120 分钟 K 线:/bars?category=MIN_120(MAC 原生 Period.MINS×120 优先, 2×60M 相邻聚合兜底,标准客户端上限 400 根);前端周期选择器同步 - 逐 bar 衍生字段:/bars 与 /bars/index 附带 pre_close/change/change_pct/ amplitude_pct(pre_close≤0.01 兜底防除零) - 159 只核心龙头池:数据资产取自 Fork(东财全行业龙头名单,四组分层); universe=core 接入 screen scan / SignalScanner / StrengthRanker / market strength; GET /market/core-leaders + WebUI「龙头池」页(搜索/个股详情) - 多 Provider LLM 直连:easy_tdx.ai + /llm/*(DeepSeek/通义/智谱/Kimi/MiniMax/ OpenAI/Claude/Ollama/自定义,openai 兼容 + anthropic 原生双协议); 配置落盘 ~/.easy_tdx/llm.json(WebUI「AI 设置」页 ⇆ 手工编辑双向兼容, 文件>环境变量>预设;key 脱敏回显/CLEAR 清除);「AI 解读」后台任务化 (复用 task_runner,提交+轮询,不占 HTTP 连接);思考型模型空白正文防御 (reasoning_content 耗尽 max_tokens → 可操作报错;默认 16000); AI 解读历史页(自动归档 Prompt/正文/策略上下文 + 去回测带参引导) - WebUI 加固:SPA fallback 对未知 /api/* 返回 JSON 404(不再 200 HTML 伪装解析错); index.html 一律 Cache-Control: no-store(防缓存旧资源引用);路由兜底重定向; 全局风险提示常驻底栏 + 龙头池/AI 解读针对性免责声明 - 测试:新增 9 个单测文件共 59 例;黄金基线仅新增 zig_breakout 条目(其余零漂移); 全量 1448 例通过
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""120 分钟 K 线重采样与逐 bar 衍生字段单元测试(bars.py 纯函数,v1.29)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from easy_tdx.web.routers.bars import (
|
|
_MIN_120_ALIASES,
|
|
_attach_derived,
|
|
_resample_pairs,
|
|
)
|
|
|
|
|
|
def _minute_df(n: int = 5) -> pd.DataFrame:
|
|
return pd.DataFrame(
|
|
{
|
|
"datetime": pd.date_range("2024-01-02 10:30", periods=n, freq="60min"),
|
|
"open": [10, 11, 12, 13, 14][:n],
|
|
"close": [10.5, 11.5, 12.5, 13.5, 14.5][:n],
|
|
"high": [10.8, 11.9, 12.9, 13.9, 14.9][:n],
|
|
"low": [9.9, 10.9, 11.9, 12.9, 13.9][:n],
|
|
"vol": [100, 200, 300, 400, 500][:n],
|
|
"amount": [1000, 2000, 3000, 4000, 5000][:n],
|
|
}
|
|
)
|
|
|
|
|
|
class TestResamplePairs:
|
|
def test_odd_count_drops_oldest(self):
|
|
"""奇数根丢最旧一根,保最新数据两两对齐。"""
|
|
r = _resample_pairs(_minute_df(5), 10)
|
|
assert len(r) == 2
|
|
row = r.iloc[0] # 原 bar1+bar2
|
|
assert row["open"] == 11 and row["close"] == 12.5
|
|
assert row["high"] == 12.9 and row["low"] == 10.9 # max/min
|
|
assert row["vol"] == 500 and row["amount"] == 5000 # sum
|
|
assert str(row["datetime"]) == "2024-01-02 12:30:00" # 后一根时间
|
|
|
|
def test_even_count_keeps_all(self):
|
|
r = _resample_pairs(_minute_df(4), 10)
|
|
assert len(r) == 2
|
|
assert r.iloc[0]["open"] == 10 # 从 bar0 起
|
|
|
|
def test_count_trims_oldest_side(self):
|
|
r = _resample_pairs(_minute_df(4), 1)
|
|
assert len(r) == 1 and r.iloc[0]["close"] == 13.5 # tail 保留
|
|
|
|
def test_empty_passthrough(self):
|
|
assert _resample_pairs(pd.DataFrame(), 10).empty
|
|
assert _resample_pairs(None, 10) is None # type: ignore[arg-type]
|
|
|
|
def test_missing_optional_columns(self):
|
|
df = _minute_df(4).drop(columns=["amount"])
|
|
r = _resample_pairs(df, 10)
|
|
assert "amount" not in r.columns and len(r) == 2
|
|
|
|
|
|
class TestAttachDerived:
|
|
def test_basic_fields(self):
|
|
d = _attach_derived(_minute_df(3))
|
|
assert {"pre_close", "change", "change_pct", "amplitude_pct"} <= set(d.columns)
|
|
assert d.iloc[0]["pre_close"] == 10 # 首根 = 本根开盘
|
|
assert d.iloc[0]["change"] == 0.5 and d.iloc[0]["change_pct"] == 5.0
|
|
assert d.iloc[1]["pre_close"] == 10.5
|
|
assert d.iloc[1]["change_pct"] == round((11.5 / 10.5 - 1) * 100, 4)
|
|
assert abs(d.iloc[0]["amplitude_pct"] - (10.8 - 9.9) / 10 * 100) < 1e-6
|
|
|
|
def test_nonpositive_preclose_floor(self):
|
|
"""QFQ 复权后前收为 0/负时按 0.01 兜底,不产生 inf。"""
|
|
df = _minute_df(3)
|
|
df.loc[0, "close"] = -5.0
|
|
d = _attach_derived(df)
|
|
assert np.isfinite(d["change_pct"]).all()
|
|
assert d.iloc[1]["change_pct"] == round((11.5 / 0.01 - 1) * 100, 4)
|
|
|
|
def test_empty_and_missing_close(self):
|
|
assert _attach_derived(pd.DataFrame()).empty
|
|
df = pd.DataFrame({"open": [1.0]})
|
|
assert "pre_close" not in _attach_derived(df).columns
|
|
|
|
|
|
def test_min_120_aliases():
|
|
assert _MIN_120_ALIASES == {"MIN_120", "120M", "120MIN"}
|