save
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""真实数据源集成测试。"""
|
||||
|
||||
import os
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -65,4 +66,58 @@ def test_3_3_real_unified_service_cache(tmp_path: Path) -> None:
|
||||
assert len(bars) > 0
|
||||
assert store.load_kline("sh600000", "day", 5)
|
||||
assert snapshot.price is not None
|
||||
assert store.load_snapshot("sh600000") is not None
|
||||
assert store.load_snapshot("sh600000") is not None
|
||||
|
||||
|
||||
def assert_random_kline_prices_match(symbols, sample_size: int, seed: int) -> None:
|
||||
"""
|
||||
功能说明:随机抽取多个代码的 K 线价格并比较 mootdx 与腾讯财经是否基本一致。
|
||||
参数说明:symbols 为待比较代码列表,sample_size 为抽样值数量,seed 为固定随机种子。
|
||||
返回值说明:无返回值。
|
||||
注意事项:仅比较 OHLC 价格,成交量和成交额因接口口径差异不参与比较。
|
||||
"""
|
||||
fields = ["open", "high", "low", "close"]
|
||||
mootdx = MootdxSource()
|
||||
tencent = TencentFinanceSource(timeout_seconds=10)
|
||||
candidates = []
|
||||
|
||||
for symbol in symbols:
|
||||
mootdx_bars = {bar.trade_date: bar for bar in mootdx.fetch_kline(symbol, "day", 60)}
|
||||
tencent_bars = {bar.trade_date: bar for bar in tencent.fetch_kline(symbol, "day", 60)}
|
||||
for trade_date in sorted(set(mootdx_bars) & set(tencent_bars)):
|
||||
for field in fields:
|
||||
mootdx_value = getattr(mootdx_bars[trade_date], field)
|
||||
tencent_value = getattr(tencent_bars[trade_date], field)
|
||||
if mootdx_value is not None and tencent_value is not None:
|
||||
candidates.append((symbol, trade_date, field, mootdx_value, tencent_value))
|
||||
|
||||
assert len(candidates) >= sample_size
|
||||
|
||||
samples = random.Random(seed).sample(candidates, sample_size)
|
||||
mismatches = [
|
||||
(symbol, trade_date, field, mootdx_value, tencent_value)
|
||||
for symbol, trade_date, field, mootdx_value, tencent_value in samples
|
||||
if abs(mootdx_value - tencent_value) > 0.05
|
||||
]
|
||||
|
||||
assert mismatches == []
|
||||
|
||||
|
||||
def test_3_4_real_mootdx_index_kline_matches_tencent() -> None:
|
||||
"""
|
||||
功能说明:验证 mootdx 指数 K 线与腾讯财经 K 线价格基本一致。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 3-4,随机抽取上证指数、深证成指、创业板指共 100 个 OHLC 值对比。
|
||||
"""
|
||||
assert_random_kline_prices_match(["sh000001", "sz399001", "sz399006"], 100, 20260623)
|
||||
|
||||
|
||||
def test_3_5_real_mootdx_component_kline_matches_tencent() -> None:
|
||||
"""
|
||||
功能说明:验证三大指数代表成分股的 mootdx K 线与腾讯财经 K 线价格基本一致。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 3-5,分别选取贵州茅台、平安银行、宁德时代并随机抽取 100 个 OHLC 值对比。
|
||||
"""
|
||||
assert_random_kline_prices_match(["sh600519", "sz000001", "sz300750"], 100, 20260624)
|
||||
|
||||
+60
-2
@@ -8,7 +8,7 @@ import pytest
|
||||
|
||||
from sidecar.config import SidecarConfig
|
||||
from sidecar.models import KlineBar, Snapshot
|
||||
from sidecar.service import UnifiedDataService
|
||||
from sidecar.service import FallbackKlineSource, UnifiedDataService
|
||||
from sidecar.storage import DuckDBStore
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class FakeSource:
|
||||
"""
|
||||
self.kline_calls = 0
|
||||
self.snapshot_calls = 0
|
||||
self.fail_kline = False
|
||||
|
||||
def fetch_kline(self, symbol: str, period: str, limit: int) -> List[KlineBar]:
|
||||
"""
|
||||
@@ -33,6 +34,8 @@ class FakeSource:
|
||||
注意事项:会记录调用次数用于验证缓存。
|
||||
"""
|
||||
self.kline_calls += 1
|
||||
if self.fail_kline:
|
||||
raise RuntimeError("K ??????")
|
||||
return [
|
||||
KlineBar(symbol, period, date(2026, 6, 22), 1, 2, 1, 2, 100, 200, "fake"),
|
||||
KlineBar(symbol, period, date(2026, 6, 23), 2, 3, 2, 3, 200, 300, "fake"),
|
||||
@@ -117,4 +120,59 @@ def test_2_2_snapshot_uses_ttl_cache(service: Tuple[UnifiedDataService, FakeSour
|
||||
|
||||
assert first.price == 10
|
||||
assert second.price == 10
|
||||
assert source.snapshot_calls == 1
|
||||
assert source.snapshot_calls == 1
|
||||
|
||||
|
||||
def test_2_3_fallback_kline_uses_primary_period() -> None:
|
||||
"""
|
||||
功能说明:验证主备 K 线源在日线周期优先使用主源。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 2-3,day/week/month 表示腾讯财经主源周期。
|
||||
"""
|
||||
primary = FakeSource()
|
||||
fallback = FakeSource()
|
||||
source = FallbackKlineSource(primary, fallback, ["day", "week", "month"])
|
||||
|
||||
bars = source.fetch_kline("sh600000", "day", 1)
|
||||
|
||||
assert bars[0].source == "fake"
|
||||
assert primary.kline_calls == 1
|
||||
assert fallback.kline_calls == 0
|
||||
|
||||
|
||||
def test_2_4_fallback_kline_uses_fallback_on_primary_error() -> None:
|
||||
"""
|
||||
功能说明:验证主备 K 线源在主源异常时自动使用备用源。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 2-4,用于覆盖腾讯财经失败时回退 mootdx 的行为。
|
||||
"""
|
||||
primary = FakeSource()
|
||||
fallback = FakeSource()
|
||||
primary.fail_kline = True
|
||||
source = FallbackKlineSource(primary, fallback, ["day", "week", "month"])
|
||||
|
||||
bars = source.fetch_kline("sh600000", "day", 1)
|
||||
|
||||
assert len(bars) == 1
|
||||
assert primary.kline_calls == 1
|
||||
assert fallback.kline_calls == 1
|
||||
|
||||
|
||||
def test_2_5_fallback_kline_uses_fallback_for_minute_period() -> None:
|
||||
"""
|
||||
功能说明:验证主备 K 线源在分钟周期直接使用备用源。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 2-5,腾讯财经适配器当前不作为分钟线主源。
|
||||
"""
|
||||
primary = FakeSource()
|
||||
fallback = FakeSource()
|
||||
source = FallbackKlineSource(primary, fallback, ["day", "week", "month"])
|
||||
|
||||
bars = source.fetch_kline("sh600000", "5m", 1)
|
||||
|
||||
assert len(bars) == 1
|
||||
assert primary.kline_calls == 0
|
||||
assert fallback.kline_calls == 1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import date
|
||||
|
||||
from sidecar.models import KlineBar
|
||||
from strategy.a500_close_chart import A500_START_DATE, ClosePoint, build_svg_line_chart, filter_close_points
|
||||
from strategy.a500_close_chart import A500_START_DATE, DEFAULT_OUTPUT_PATH, ClosePoint, build_svg_line_chart, filter_close_points
|
||||
|
||||
|
||||
def test_4_1_filter_a500_close_points() -> None:
|
||||
@@ -42,4 +42,14 @@ def test_4_2_build_svg_line_chart() -> None:
|
||||
assert svg.startswith("<svg")
|
||||
assert "中证 A500 指数成立以来收盘价" in svg
|
||||
assert "2024-09-23" in svg
|
||||
assert "<polyline" in svg
|
||||
assert "<polyline" in svg
|
||||
|
||||
|
||||
def test_4_3_default_output_path_uses_strategy_directory() -> None:
|
||||
"""
|
||||
功能说明:验证策略1默认输出到同名子目录。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 4-3。
|
||||
"""
|
||||
assert DEFAULT_OUTPUT_PATH.as_posix() == "strategy/output/a500_close_chart/a500_close.svg"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import date
|
||||
|
||||
from strategy.a500_close_chart import ClosePoint
|
||||
from strategy.a500_hs300_close_chart import DualAxisSeries, _scale_points_by_ratio, align_points_by_date, build_dual_axis_svg_chart
|
||||
from strategy.a500_hs300_close_chart import DEFAULT_OUTPUT_PATH, DualAxisSeries, _scale_points_by_ratio, align_points_by_date, build_dual_axis_svg_chart
|
||||
|
||||
|
||||
def test_5_1_align_points_by_common_date() -> None:
|
||||
@@ -66,3 +66,13 @@ def test_5_3_same_return_ratio_maps_to_same_y() -> None:
|
||||
|
||||
assert left_coordinates[0][1] == right_coordinates[0][1]
|
||||
assert left_coordinates[1][1] == right_coordinates[1][1]
|
||||
|
||||
|
||||
def test_5_4_default_output_path_uses_strategy_directory() -> None:
|
||||
"""
|
||||
功能说明:验证策略2默认输出到同名子目录。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 5-4。
|
||||
"""
|
||||
assert DEFAULT_OUTPUT_PATH.as_posix() == "strategy/output/a500_hs300_close_chart/a500_hs300_close.svg"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""策略通用技术指标工具测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from strategy.indicators import calculate_ema, calculate_kdj, calculate_ma, calculate_macd
|
||||
|
||||
|
||||
def test_6_1_calculate_ma() -> None:
|
||||
"""
|
||||
功能说明:验证简单移动平均线会在样本不足时返回 None。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 6-1。
|
||||
"""
|
||||
assert calculate_ma([1, 2, 3, 4], 3) == [None, None, 2.0, 3.0]
|
||||
|
||||
|
||||
def test_6_2_calculate_ema() -> None:
|
||||
"""
|
||||
功能说明:验证指数移动平均线按首个值初始化并递推。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 6-2。
|
||||
"""
|
||||
assert calculate_ema([1, 2, 3], 2) == pytest.approx([1.0, 1.6666666667, 2.5555555556])
|
||||
|
||||
|
||||
def test_6_3_calculate_macd() -> None:
|
||||
"""
|
||||
功能说明:验证 MACD 返回 DIF、DEA 和柱值三个等长序列。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 6-3。
|
||||
"""
|
||||
dif, dea, macd = calculate_macd([1, 2, 3, 4], fast_period=2, slow_period=3, signal_period=2)
|
||||
|
||||
assert len(dif) == 4
|
||||
assert len(dea) == 4
|
||||
assert len(macd) == 4
|
||||
assert dif == pytest.approx([0.0, 0.1666666667, 0.3055555556, 0.3935185185])
|
||||
assert dea == pytest.approx([0.0, 0.1111111111, 0.2407407407, 0.3425925926])
|
||||
assert macd == pytest.approx([0.0, 0.1111111111, 0.1296296296, 0.1018518519])
|
||||
|
||||
|
||||
def test_6_4_calculate_kdj() -> None:
|
||||
"""
|
||||
功能说明:验证 KDJ 在样本足够后计算 K、D、J 值。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 6-4。
|
||||
"""
|
||||
k_values, d_values, j_values = calculate_kdj([3, 4, 5], [1, 1, 1], [2, 3, 4], period=3)
|
||||
|
||||
assert k_values[:2] == [None, None]
|
||||
assert d_values[:2] == [None, None]
|
||||
assert j_values[:2] == [None, None]
|
||||
assert k_values[2] == pytest.approx(58.3333333333)
|
||||
assert d_values[2] == pytest.approx(52.7777777778)
|
||||
assert j_values[2] == pytest.approx(69.4444444444)
|
||||
|
||||
|
||||
def test_6_5_indicator_period_validation() -> None:
|
||||
"""
|
||||
功能说明:验证通用指标工具会拒绝非法周期参数。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 6-5。
|
||||
"""
|
||||
with pytest.raises(ValueError):
|
||||
calculate_ma([1], 0)
|
||||
Reference in New Issue
Block a user