This commit is contained in:
cheney 2026-06-24 16:50:59 +08:00
parent db4e519cc2
commit e046c08e02
18 changed files with 386 additions and 34 deletions

View File

@ -19,7 +19,7 @@ uvicorn sidecar.api:app --host 127.0.0.1 --port 8765
常用接口:
- `GET /health`:健康检查。
- `GET /kline/{symbol}?period=day&limit=800&refresh=false`:获取 K 线,底层使用 mootdx 并写入 DuckDB。
- `GET /kline/{symbol}?period=day&limit=800&refresh=false`:获取 K 线,日/周/月优先腾讯财经,失败后回退 mootdx分钟线使用 mootdx并写入 DuckDB。
- `GET /snapshot/{symbol}?refresh=false`:获取实时快照,优先腾讯财经,失败后回退 mootdx并写入 DuckDB TTL 缓存。
默认 DuckDB 文件为 `data/tortoise.duckdb`,可通过 `TORTOISE_DUCKDB_PATH` 修改。
@ -32,7 +32,7 @@ uvicorn sidecar.api:app --host 127.0.0.1 --port 8765
python -m strategy.a500_close_chart --refresh
```
默认输出:`strategy/output/a500_close.svg`。
默认输出:`strategy/output/a500_close_chart/a500_close.svg`。
## 策略2中证 A500 与沪深 300 双轴收盘价图
运行:
@ -41,4 +41,9 @@ python -m strategy.a500_close_chart --refresh
python -m strategy.a500_hs300_close_chart --refresh
```
默认输出:`strategy/output/a500_hs300_close.svg`。
默认输出:`strategy/output/a500_hs300_close_chart/a500_hs300_close.svg`。
## 策略通用工具
- 所有策略默认输出到 `strategy/output/<策略模块名>/`,图片和 md 说明文件放在同一个策略子目录。
- `strategy.indicators` 提供 `calculate_ma`、`calculate_ema`、`calculate_macd`、`calculate_kdj`用于均线、MACD 与 KDJ 计算。

View File

@ -9,13 +9,13 @@
- `sidecar/api.py`FastAPI HTTP 接口,提供 `/health`、`/kline/{symbol}`、`/snapshot/{symbol}`。
- `sidecar/service.py`:统一数据服务,负责缓存命中、远端拉取、数据源降级。
- `sidecar/storage.py`DuckDB 仓储,维护 `md.kline_bars``md.snapshots`
- `sidecar/sources/mootdx_source.py`mootdx 适配器,负责 K 线与备用快照。
- `sidecar/sources/tencent.py`:腾讯财经适配器,负责估值字段更完整的实时快照。
- `sidecar/sources/mootdx_source.py`mootdx 适配器,负责备用 K 线、分钟线与备用快照。
- `sidecar/sources/tencent.py`:腾讯财经适配器,负责日/周/月 K 线与估值字段更完整的实时快照。
- `sidecar/models.py`:统一 K 线与快照模型。
## 数据源策略
- K 线:默认使用 mootdx支持 `day/week/month/1m/5m/15m/30m/60m`
- K 线:`day/week/month` 默认优先腾讯财经,失败后回退 mootdx`1m/5m/15m/30m/60m` 使用 mootdx腾讯财经 K 线使用未复权接口以对齐 mootdx 原始价格
- 快照:默认优先腾讯财经,失败后回退 mootdx。
- 腾讯财经字段校准:`39=PE_TTM`、`46=PB`、`52=PE 静态``43` 是振幅,不作为 PB 使用。
@ -40,8 +40,14 @@ snapshot = service.get_snapshot("600000")
- `1-1`:验证腾讯财经 PB 使用索引 46避免误用索引 43。
- `1-2`:验证腾讯财经原始响应可解析为字段列表。
- `1-3`:验证腾讯财经 K 线 JSON 可转换为统一 K 线。
- `2-1`:验证 K 线首次拉取后可从 DuckDB 缓存读取。
- `2-2`:验证快照在 TTL 内复用 DuckDB 缓存。
- `2-3`:验证日/周/月 K 线优先使用主源。
- `2-4`:验证主源异常时 K 线自动回退备用源。
- `2-5`:验证分钟周期 K 线直接使用备用源。
- `3-4`:验证 mootdx 与腾讯财经的上证指数、深证成指、创业板指 K 线价格随机 100 个 OHLC 值基本一致;成交量单位不同不参与比较。
- `3-5`:验证三大指数代表成分股贵州茅台、平安银行、宁德时代的 K 线价格随机 100 个 OHLC 值基本一致。
运行方式:

View File

@ -11,6 +11,35 @@ from sidecar.symbols import normalize_symbol
from sidecar.storage import DuckDBStore
class FallbackKlineSource:
"""主备 K 线数据源。"""
def __init__(self, primary: MarketDataSource, fallback: MarketDataSource, primary_periods: List[str]) -> None:
"""
功能说明创建按周期选择主备源的 K 线适配器
参数说明primary 为主数据源fallback 为备用数据源primary_periods 为主数据源支持的周期列表
返回值说明无返回值
注意事项主源仅在支持周期内使用主源异常时自动回退到备用源
"""
self.primary = primary
self.fallback = fallback
self.primary_periods = set(primary_periods)
def fetch_kline(self, symbol: str, period: str, limit: int) -> List[KlineBar]:
"""
功能说明获取 K 线并在主源失败时回退备用源
参数说明symbol 为股票代码period 为周期limit 为最大条数
返回值说明返回统一 KlineBar 列表
注意事项不在主源支持周期内的数据直接使用备用源
"""
if period in self.primary_periods:
try:
return self.primary.fetch_kline(symbol, period, limit)
except Exception:
pass
return self.fallback.fetch_kline(symbol, period, limit)
class UnifiedDataService:
"""统一行情获取与缓存服务。"""
@ -84,4 +113,5 @@ def create_default_service(config: SidecarConfig) -> UnifiedDataService:
store = DuckDBStore(config.db_path)
mootdx = MootdxSource()
tencent = TencentFinanceSource(config.request_timeout_seconds)
return UnifiedDataService(store, config, mootdx, [tencent, mootdx])
kline_source = FallbackKlineSource(tencent, mootdx, ["day", "week", "month"])
return UnifiedDataService(store, config, kline_source, [tencent, mootdx])

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Any, Dict, List
from sidecar.models import KlineBar, Snapshot, parse_date, parse_float
from sidecar.symbols import mootdx_market, normalize_symbol, split_symbol
from sidecar.symbols import is_index_symbol, mootdx_market, normalize_symbol, split_symbol
PERIOD_CATEGORY = {
@ -48,7 +48,7 @@ class MootdxSource:
if category is None:
raise ValueError("不支持的 K 线周期: %s" % period)
_, code = split_symbol(normalized)
raw = self._call_bars(code, mootdx_market(normalized), category, limit)
raw = self._call_bars(normalized, code, mootdx_market(normalized), category, limit)
records = self._records(raw)
bars = []
for item in records:
@ -111,17 +111,18 @@ class MootdxSource:
source="mootdx",
)
def _call_bars(self, code: str, market: int, category: int, limit: int) -> Any:
def _call_bars(self, symbol: str, code: str, market: int, category: int, limit: int) -> Any:
"""
功能说明兼容调用不同版本 mootdx bars 接口
参数说明code 为六位股票代码market 为市场编号category 为周期编号limit 为数量
功能说明兼容调用不同版本 mootdx K 线接口
参数说明symbol 为标准代码code 为六位代码market 为市场编号category 为周期编号limit 为数量
返回值说明返回 mootdx 原始结果对象
注意事项不同 mootdx 版本参数名存在差异因此保留一次兼容重试
注意事项沪深指数必须使用 index_bars普通 bars 会返回错误价格或异常日期
"""
method = self.client.index_bars if is_index_symbol(symbol) else self.client.bars
try:
return self.client.bars(symbol=code, market=market, category=category, count=limit)
return method(symbol=code, market=market, category=category, count=limit)
except TypeError:
return self.client.bars(symbol=code, market=market, frequency=category, offset=0)
return method(symbol=code, market=market, frequency=category, offset=limit)
def _records(self, raw: Any) -> List[Dict[str, Any]]:
"""

View File

@ -36,8 +36,8 @@ class TencentFinanceSource:
if tencent_period is None:
raise ValueError("腾讯财经 K 线暂不支持周期: %s" % period)
normalized = normalize_symbol(symbol)
url = "https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?" + urlencode(
{"param": "%s,%s,,,%d,qfq" % (normalized, tencent_period, limit)}
url = "https://web.ifzq.gtimg.cn/appstock/app/kline/kline?" + urlencode(
{"param": "%s,%s,,,%d" % (normalized, tencent_period, limit)}
)
request = Request(url, headers={"User-Agent": "Mozilla/5.0 TortoiseSidecar/0.1"})
with urlopen(request, timeout=self.timeout_seconds) as response:

View File

@ -44,4 +44,15 @@ def mootdx_market(symbol: str) -> int:
注意事项mootdx 对北交所支持随版本变化这里按通达信常见约定归入 0
"""
market, _ = split_symbol(symbol)
return 1 if market == "sh" else 0
return 1 if market == "sh" else 0
def is_index_symbol(symbol: str) -> bool:
"""
功能说明判断代码是否为常见沪深指数代码
参数说明symbol 为任意支持格式的股票或指数代码
返回值说明指数代码返回 True其他代码返回 False
注意事项仅按沪市 000深市 399 前缀识别避免影响普通股票 K 线
"""
market, code = split_symbol(symbol)
return (market == "sh" and code.startswith("000")) or (market == "sz" and code.startswith("399"))

View File

@ -17,7 +17,7 @@ from sidecar.storage import DuckDBStore
A500_SYMBOL = "sh000510"
A500_START_DATE = date(2024, 9, 23)
DEFAULT_OUTPUT_PATH = Path("strategy/output/a500_close.svg")
DEFAULT_OUTPUT_PATH = Path("strategy/output/a500_close_chart/a500_close.svg")
@dataclass(frozen=True)
@ -133,7 +133,7 @@ def parse_args() -> argparse.Namespace:
功能说明解析命令行参数
参数说明
返回值说明返回 argparse.Namespace 参数对象
注意事项默认输出到 strategy/output/a500_close.svg
注意事项默认输出到 strategy/output/a500_close_chart/a500_close.svg
"""
parser = argparse.ArgumentParser(description="绘制中证 A500 指数成立以来收盘价")
parser.add_argument("--output", default=str(DEFAULT_OUTPUT_PATH), help="输出 SVG 文件路径")

View File

@ -17,7 +17,7 @@ from sidecar.storage import DuckDBStore
from strategy.a500_close_chart import A500_START_DATE, A500_SYMBOL, ClosePoint, filter_close_points
HS300_SYMBOL = "sh000300"
DEFAULT_OUTPUT_PATH = Path("strategy/output/a500_hs300_close.svg")
DEFAULT_OUTPUT_PATH = Path("strategy/output/a500_hs300_close_chart/a500_hs300_close.svg")
@dataclass(frozen=True)
@ -156,7 +156,7 @@ def parse_args() -> argparse.Namespace:
功能说明解析命令行参数
参数说明
返回值说明返回 argparse.Namespace 参数对象
注意事项默认输出到 strategy/output/a500_hs300_close.svg
注意事项默认输出到 strategy/output/a500_hs300_close_chart/a500_hs300_close.svg
"""
parser = argparse.ArgumentParser(description="绘制中证 A500 与沪深 300 双轴收盘价")
parser.add_argument("--output", default=str(DEFAULT_OUTPUT_PATH), help="输出 SVG 文件路径")

96
strategy/indicators.py Normal file
View File

@ -0,0 +1,96 @@
"""策略通用技术指标计算工具。"""
from __future__ import annotations
from typing import List, Optional, Sequence, Tuple
def calculate_ma(values: Sequence[float], period: int) -> List[Optional[float]]:
"""
功能说明计算简单移动平均线
参数说明values 为按时间升序排列的数值序列period 为均线周期
返回值说明返回与输入等长的均线列表样本不足的位置为 None
注意事项period 必须大于 0
"""
if period <= 0:
raise ValueError("均线周期必须大于 0")
result: List[Optional[float]] = []
window_sum = 0.0
for index, value in enumerate(values):
window_sum += value
if index >= period:
window_sum -= values[index - period]
result.append(window_sum / period if index + 1 >= period else None)
return result
def calculate_ema(values: Sequence[float], period: int) -> List[Optional[float]]:
"""
功能说明计算指数移动平均线
参数说明values 为按时间升序排列的数值序列period EMA 周期
返回值说明返回与输入等长的 EMA 列表空输入返回空列表
注意事项首个 EMA 使用首个输入值初始化period 必须大于 0
"""
if period <= 0:
raise ValueError("EMA 周期必须大于 0")
if not values:
return []
factor = 2 / (period + 1)
result: List[Optional[float]] = [float(values[0])]
for value in values[1:]:
result.append(float(value) * factor + result[-1] * (1 - factor))
return result
def calculate_macd(values: Sequence[float], fast_period: int = 12, slow_period: int = 26, signal_period: int = 9) -> Tuple[List[Optional[float]], List[Optional[float]], List[Optional[float]]]:
"""
功能说明计算 MACD 指标的 DIFDEA 和柱值
参数说明values 为按时间升序排列的收盘价序列fast_period 为快线周期slow_period 为慢线周期signal_period 为信号线周期
返回值说明返回 DIFDEAMACD 柱值三个等长列表
注意事项周期必须大于 0MACD 柱值按 A 股常用口径计算为 2 * (DIF - DEA)
"""
if fast_period <= 0 or slow_period <= 0 or signal_period <= 0:
raise ValueError("MACD 周期必须大于 0")
fast_ema = calculate_ema(values, fast_period)
slow_ema = calculate_ema(values, slow_period)
dif = [fast_value - slow_value for fast_value, slow_value in zip(fast_ema, slow_ema)]
dea = calculate_ema(dif, signal_period)
macd = [2 * (dif_value - dea_value) for dif_value, dea_value in zip(dif, dea)]
return dif, dea, macd
def calculate_kdj(highs: Sequence[float], lows: Sequence[float], closes: Sequence[float], period: int = 9, k_period: int = 3, d_period: int = 3) -> Tuple[List[Optional[float]], List[Optional[float]], List[Optional[float]]]:
"""
功能说明计算 KDJ 指标的 KDJ
参数说明highslowscloses 分别为按时间升序排列的最高价最低价收盘价序列period RSV 周期k_period d_period 为平滑周期
返回值说明返回 KDJ 三个等长列表样本不足的位置为 None
注意事项三个价格序列长度必须一致周期必须大于 0首个有效 K D 50 开始平滑
"""
if len(highs) != len(lows) or len(highs) != len(closes):
raise ValueError("KDJ 输入序列长度必须一致")
if period <= 0 or k_period <= 0 or d_period <= 0:
raise ValueError("KDJ 周期必须大于 0")
k_values: List[Optional[float]] = []
d_values: List[Optional[float]] = []
j_values: List[Optional[float]] = []
previous_k = 50.0
previous_d = 50.0
for index, close in enumerate(closes):
if index + 1 < period:
k_values.append(None)
d_values.append(None)
j_values.append(None)
continue
start_index = index + 1 - period
highest = max(highs[start_index : index + 1])
lowest = min(lows[start_index : index + 1])
rsv = 50.0 if highest == lowest else (close - lowest) / (highest - lowest) * 100
current_k = (previous_k * (k_period - 1) + rsv) / k_period
current_d = (previous_d * (d_period - 1) + current_k) / d_period
current_j = 3 * current_k - 2 * current_d
k_values.append(current_k)
d_values.append(current_d)
j_values.append(current_j)
previous_k = current_k
previous_d = current_d
return k_values, d_values, j_values

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -11,7 +11,7 @@
- 成立起始日:`2024-09-23`。
- 数据周期:日线。
- K 线数据源:`sidecar.sources.tencent.TencentFinanceSource`。
- 输出格式SVG 图片,默认路径为 `strategy/output/a500_close.svg`。
- 输出格式SVG 图片,默认路径为 `strategy/output/a500_close_chart/a500_close.svg`。
## 数据源说明
@ -26,14 +26,14 @@ python -m strategy.a500_close_chart --refresh
可指定输出路径:
```powershell
python -m strategy.a500_close_chart --output strategy/output/a500_close.svg --refresh
python -m strategy.a500_close_chart --output strategy/output/a500_close_chart/a500_close.svg --refresh
```
## 输入
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `--output` | string | `strategy/output/a500_close.svg` | 输出 SVG 文件路径 |
| `--output` | string | `strategy/output/a500_close_chart/a500_close.svg` | 输出 SVG 文件路径 |
| `--refresh` | bool | `False` | 是否强制刷新 sidecar 缓存 |
## 输出

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -11,7 +11,7 @@
- 横轴起始日:`2024-09-23`。
- 数据周期:日线。
- K 线数据源:`sidecar.sources.tencent.TencentFinanceSource`。
- 输出格式SVG 图片,默认路径为 `strategy/output/a500_hs300_close.svg`。
- 输出格式SVG 图片,默认路径为 `strategy/output/a500_hs300_close_chart/a500_hs300_close.svg`。
## 双轴缩放规则
@ -36,14 +36,14 @@ python -m strategy.a500_hs300_close_chart --refresh
可指定输出路径:
```powershell
python -m strategy.a500_hs300_close_chart --output strategy/output/a500_hs300_close.svg --refresh
python -m strategy.a500_hs300_close_chart --output strategy/output/a500_hs300_close_chart/a500_hs300_close.svg --refresh
```
## 输入
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `--output` | string | `strategy/output/a500_hs300_close.svg` | 输出 SVG 文件路径 |
| `--output` | string | `strategy/output/a500_hs300_close_chart/a500_hs300_close.svg` | 输出 SVG 文件路径 |
| `--refresh` | bool | `False` | 是否强制刷新 sidecar 缓存 |
## 输出

View File

@ -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)

View File

@ -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-3day/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

View File

@ -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"

View File

@ -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"

View File

@ -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 返回 DIFDEA 和柱值三个等长序列
参数说明
返回值说明无返回值
注意事项用例编号 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 在样本足够后计算 KDJ
参数说明
返回值说明无返回值
注意事项用例编号 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)