Files
Chan/web/tests/test_analyze_contract.py
T
jackyu66gitandCursor 9b72173285 feat: 第四类买卖点(B4/S4)融入缠论引擎与 web 展示
研究侧的 fast_bsp3 一直只活在 research/lib/ 里,web 端看不到,回测与目视
两条线对不上。这次把它搬进引擎,作为独立的第四类买卖点。

之所以单独立类而不是当作 B3/S3 的低滞后版:step30/31 显示引擎原生的
B3/S3 统计上呈逆势、显著亏损(胜率 27.4%、PF 0.66、t −18.76),而同一组
过滤器把 B4 从 PF 1.59 提到 2.26 却对它无效(0.66→0.71)。两者选的是
不同的交易群体,不是同一信号的早晚两版。

- chanlun/analysis/fast_bsp.py 原样搬入 find_fast_bsp3 与 build_htf_zones,
  另加 add_zone_ladder / htf_fx_timeline / attach_htf_agree
- research/lib/ 两个模块改为转发,所有 step 脚本导入不变,信号逐条比对一致
- 大级别上下文用 resample 从同一份 df 构建,不额外拉数据,因此与界面上选的
  周期和时间范围无关
- 前端三个复选框 + 过滤模式下拉;未过滤的原始信号用浅色,避免与主口径混淆

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 00:05:26 +08:00

145 lines
4.2 KiB
Python

"""加深 /api/analyze 相关契约 —— mock 行情 + analyze_chan 关键字段快照。"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from unittest.mock import patch
import pandas as pd
import pytest
ROOT = Path(__file__).resolve().parents[2]
WEB_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(WEB_ROOT))
from tests.helpers import make_ohlcv # noqa: E402
_CONTRACT_DOC = json.loads(
(Path(__file__).resolve().parent / "fixtures" / "analyze_contract_keys.json").read_text(encoding="utf-8")
)
CONTRACT_KEYS = (
_CONTRACT_DOC["required"]
if isinstance(_CONTRACT_DOC, dict) and "required" in _CONTRACT_DOC
else _CONTRACT_DOC
)
# analyze_chan 直接返回的对象字段(未序列化前)
ANALYZE_CHAN_KEYS = {
"klc_list",
"klu_list",
"bi_list",
"seg_list",
"zs_list",
"bi_zs_list",
"bsp_list",
"fast_bsp_list",
"klc_fx_info",
"chan_macd",
"ema52_dict",
}
CHAN_MACD_SERIALIZED_KEYS = {
"seg_list",
"unittf_list",
"histset_list",
"high_position_list",
"high_empty_list",
"low_position_list",
"low_empty_list",
"return_zero_list",
"cross0_up_list",
"cross0_down_list",
"klu_list",
}
def test_analyze_route_registered():
from app import app
rules = {r.rule for r in app.url_map.iter_rules()}
assert "/api/analyze" in rules
assert "/api/klines/recent" in rules
assert "/api/chart_metadata" in rules
assert "/" in rules
assert "/chan_tv" in rules
def test_klines_recent_returns_tail_only():
from app import app
df = make_ohlcv(n=30)
with patch("api.analyze.get_kl_data", return_value=df):
client = app.test_client()
resp = client.get(
"/api/klines/recent",
query_string={"symbol": "BTC/USDT:USDT", "timeframe": "5m", "limit": 2},
)
assert resp.status_code == 200
body = resp.get_json()
assert body.get("partial") is True
assert body.get("limit") == 2
assert isinstance(body.get("kline_data"), list)
assert len(body["kline_data"]) == 2
assert "bi_list" not in body
def test_contract_keys_stable():
assert "bi_list" in CONTRACT_KEYS and "seg_list" in CONTRACT_KEYS
for k in ("kline_data", "macd", "zs_list", "bsp_list", "fast_bsp_list", "chan_macd"):
assert k in CONTRACT_KEYS
def test_analyze_chan_keys_on_fixture():
from services.runtime import add_indicators, analyze_chan
df = add_indicators(make_ohlcv(400))
result = analyze_chan(df, symbol="TEST/USDT:USDT", timeframe="5m")
assert set(result.keys()) == ANALYZE_CHAN_KEYS
assert isinstance(result["bi_list"], list)
assert isinstance(result["seg_list"], list)
assert isinstance(result["chan_macd"], dict)
for k in ("seg_list", "unittf_list", "histset_list"):
assert k in result["chan_macd"]
def test_serialize_chan_macd_shape():
from pytz import timezone
from services.runtime import add_indicators, analyze_chan, serialize_chan_macd_data
df = add_indicators(make_ohlcv(200))
result = analyze_chan(df)
serialized = serialize_chan_macd_data(result["chan_macd"], timezone("Asia/Shanghai"))
assert set(serialized.keys()) == CHAN_MACD_SERIALIZED_KEYS
json.dumps(serialized)
def test_analyze_http_contract_with_mocked_kl():
"""Flask 测试客户端:mock get_kl_data,断言响应含契约关键字段。"""
from app import app
from services.runtime import add_indicators
df = add_indicators(make_ohlcv(300))
df = df.copy()
if "timestamp" not in df.columns:
df["timestamp"] = (pd.to_datetime(df["date"]).astype("int64") // 10**6).astype("int64")
with patch("api.analyze.get_kl_data", return_value=df):
client = app.test_client()
resp = client.get(
"/api/analyze",
query_string={
"symbol": "BTC/USDT:USDT",
"timeframe": "5m",
"timezone": "Asia/Shanghai",
},
)
assert resp.status_code == 200, resp.data[:500]
payload = resp.get_json()
assert payload is not None and "error" not in payload
missing = [k for k in CONTRACT_KEYS if k not in payload]
assert not missing, f"missing contract keys: {missing}"