refactor: ECR-002 拆分 runtime 包并加深 analyze 契约(已审)
将 web/services/runtime.py 拆为 runtime/ 子模块并保持门面兼容;补齐 ESS 文档、门面/契约/TF_DF 测试与 CODE_REVIEW Approve。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,14 +1,53 @@
|
||||
""" /api/analyze 契约冒烟:关键字段存在于契约清单。"""
|
||||
"""ECR-002:加深 /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]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "web"))
|
||||
|
||||
from tests.generate_golden import make_ohlcv # noqa: E402
|
||||
|
||||
|
||||
CONTRACT_KEYS = json.loads(
|
||||
(ROOT / "tests" / "fixtures" / "analyze_contract_keys.json").read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
# analyze_chan 直接返回的对象字段(未序列化前)
|
||||
ANALYZE_CHAN_KEYS = {
|
||||
"klc_list",
|
||||
"klu_list",
|
||||
"bi_list",
|
||||
"seg_list",
|
||||
"zs_list",
|
||||
"bi_zs_list",
|
||||
"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
|
||||
@@ -21,9 +60,60 @@ def test_analyze_route_registered():
|
||||
|
||||
|
||||
def test_contract_keys_stable():
|
||||
keys = json.loads(
|
||||
(ROOT / "tests" / "fixtures" / "analyze_contract_keys.json").read_text(
|
||||
encoding="utf-8"
|
||||
assert "bi_list" in CONTRACT_KEYS and "seg_list" in CONTRACT_KEYS
|
||||
for k in ("kline_data", "macd", "zs_list", "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 可序列化
|
||||
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")
|
||||
|
||||
# analyze 路由使用 `from services.runtime import *`,须 patch 其模块命名空间
|
||||
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 "bi_list" in keys and "seg_list" in keys
|
||||
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}"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""ECR-002:runtime 门面公开符号 + 子模块可导入。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "web"))
|
||||
|
||||
REQUIRED = [
|
||||
"get_kl_data",
|
||||
"analyze_chan",
|
||||
"add_indicators",
|
||||
"serialize_chan_macd_data",
|
||||
"clean_dataframe_for_json",
|
||||
"classify_trend_stage",
|
||||
"refresh_data_service_metadata",
|
||||
"TIMEFRAMES",
|
||||
"SYMBOLS",
|
||||
"_zone_cache",
|
||||
"macd_fast_period",
|
||||
"is_smaller_or_equal_timeframe",
|
||||
"get_uncompleted_seg_list",
|
||||
]
|
||||
|
||||
|
||||
def test_runtime_facade_exports():
|
||||
from services import runtime as R
|
||||
|
||||
for name in REQUIRED:
|
||||
assert hasattr(R, name), f"missing facade export: {name}"
|
||||
|
||||
|
||||
def test_runtime_submodules_importable():
|
||||
from services.runtime import state, timeframes, market_data, indicators, analyze, serialize
|
||||
|
||||
assert state.exchange is not None
|
||||
assert callable(timeframes.timeframe_to_minutes)
|
||||
assert callable(market_data.get_kl_data)
|
||||
assert callable(indicators.add_indicators)
|
||||
assert callable(analyze.analyze_chan)
|
||||
assert callable(serialize.convert_direction)
|
||||
|
||||
|
||||
def test_thin_shims_still_reexport():
|
||||
from services import market_data as md
|
||||
from services import chan_analyze as ca
|
||||
from services import serializers as ser
|
||||
from services import timeframes as tf
|
||||
|
||||
assert callable(md.get_kl_data)
|
||||
assert callable(ca.analyze_chan)
|
||||
assert callable(ser.serialize_chan_macd_data)
|
||||
assert callable(tf.timeframe_to_minutes)
|
||||
Reference in New Issue
Block a user