a500 绘图成功
This commit is contained in:
parent
31d28d8683
commit
c4db32374e
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,7 @@
|
||||
*.pyc
|
||||
*.egg-info/**
|
||||
|
||||
data/*.duckdb
|
||||
data/*.duckdb.wal
|
||||
.pytest_cache/
|
||||
__pycache__/
|
||||
|
||||
12
README.md
12
README.md
@ -1,4 +1,4 @@
|
||||
# Tortoise Stock
|
||||
# Tortoise Stock
|
||||
|
||||
轻量级 A 股投研回测系统。当前已提供 Python sidecar 的统一数据获取与 DuckDB 本地缓存接口,后续策略模块可直接基于该接口读取 K 线与行情快照。
|
||||
|
||||
@ -23,3 +23,13 @@ uvicorn sidecar.api:app --host 127.0.0.1 --port 8765
|
||||
- `GET /snapshot/{symbol}?refresh=false`:获取实时快照,优先腾讯财经,失败后回退 mootdx,并写入 DuckDB TTL 缓存。
|
||||
|
||||
默认 DuckDB 文件为 `data/tortoise.duckdb`,可通过 `TORTOISE_DUCKDB_PATH` 修改。
|
||||
|
||||
## 策略1:中证 A500 收盘价图
|
||||
|
||||
运行:
|
||||
|
||||
```powershell
|
||||
python -m strategy.a500_close_chart --refresh
|
||||
```
|
||||
|
||||
默认输出:`strategy/output/a500_close.svg`。
|
||||
2
setup.py
2
setup.py
@ -4,7 +4,7 @@ setup(
|
||||
name="tortoise-stock-sidecar",
|
||||
version="0.1.0",
|
||||
description="Python sidecar for unified A-share data fetching and DuckDB cache.",
|
||||
packages=find_packages(include=["sidecar", "sidecar.*"]),
|
||||
packages=find_packages(include=["sidecar", "sidecar.*", "strategy", "strategy.*"]),
|
||||
python_requires=">=3.8",
|
||||
install_requires=[
|
||||
"duckdb>=0.10.3,<1.2",
|
||||
|
||||
@ -2,12 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from sidecar.models import KlineBar, Snapshot, parse_float
|
||||
from sidecar.models import KlineBar, Snapshot, parse_date, parse_float
|
||||
from sidecar.symbols import normalize_symbol
|
||||
|
||||
|
||||
@ -25,12 +26,23 @@ class TencentFinanceSource:
|
||||
|
||||
def fetch_kline(self, symbol: str, period: str, limit: int) -> List[KlineBar]:
|
||||
"""
|
||||
功能说明:获取 K 线数据。
|
||||
参数说明:symbol 为股票代码,period 为周期,limit 为最大条数。
|
||||
返回值说明:腾讯适配器暂不提供 K 线,固定抛出 NotImplementedError。
|
||||
注意事项:本项目 K 线优先由 mootdx 提供。
|
||||
功能说明:获取腾讯财经 K 线数据。
|
||||
参数说明:symbol 为股票或指数代码,period 为周期,limit 为最大条数。
|
||||
返回值说明:返回统一 KlineBar 列表。
|
||||
注意事项:当前仅支持 day/week/month,用于补充指数历史 K 线。
|
||||
"""
|
||||
raise NotImplementedError("腾讯财经适配器暂不提供 K 线")
|
||||
period_map = {"day": "day", "week": "week", "month": "month"}
|
||||
tencent_period = period_map.get(period)
|
||||
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)}
|
||||
)
|
||||
request = Request(url, headers={"User-Agent": "Mozilla/5.0 TortoiseSidecar/0.1"})
|
||||
with urlopen(request, timeout=self.timeout_seconds) as response:
|
||||
payload = response.read().decode("utf-8", errors="ignore")
|
||||
return kline_from_tencent_payload(normalized, period, payload)
|
||||
|
||||
def fetch_snapshot(self, symbol: str) -> Snapshot:
|
||||
"""
|
||||
@ -67,6 +79,40 @@ def parse_tencent_payload(payload: str) -> List[str]:
|
||||
return fields
|
||||
|
||||
|
||||
def kline_from_tencent_payload(symbol: str, period: str, payload: str) -> List[KlineBar]:
|
||||
"""
|
||||
功能说明:把腾讯财经 K 线 JSON 转换为统一 K 线列表。
|
||||
参数说明:symbol 为标准股票代码,period 为统一周期,payload 为腾讯 K 线 JSON 文本。
|
||||
返回值说明:返回 KlineBar 列表。
|
||||
注意事项:腾讯字段顺序为 日期、开盘、收盘、最高、最低、成交量,部分响应可能附带成交额。
|
||||
"""
|
||||
data = json.loads(payload)
|
||||
if data.get("code") != 0:
|
||||
raise ValueError("腾讯财经 K 线响应失败: %s" % data.get("msg"))
|
||||
symbol_data = data.get("data", {}).get(symbol, {})
|
||||
rows = symbol_data.get(period) or symbol_data.get("qfq%s" % period) or []
|
||||
bars = []
|
||||
for row in rows:
|
||||
trade_date = parse_date(_row_field(row, 0))
|
||||
if trade_date is None:
|
||||
continue
|
||||
bars.append(
|
||||
KlineBar(
|
||||
symbol=symbol,
|
||||
period=period,
|
||||
trade_date=trade_date,
|
||||
open=parse_float(_row_field(row, 1)),
|
||||
close=parse_float(_row_field(row, 2)),
|
||||
high=parse_float(_row_field(row, 3)),
|
||||
low=parse_float(_row_field(row, 4)),
|
||||
volume=parse_float(_row_field(row, 5)),
|
||||
amount=parse_float(_row_field(row, 6)),
|
||||
source="tencent",
|
||||
)
|
||||
)
|
||||
return bars
|
||||
|
||||
|
||||
def snapshot_from_tencent_fields(symbol: str, fields: List[str]) -> Snapshot:
|
||||
"""
|
||||
功能说明:把腾讯财经字段转换为统一快照。
|
||||
@ -109,6 +155,16 @@ def _field(fields: List[str], index: int) -> str:
|
||||
return fields[index] if index < len(fields) else ""
|
||||
|
||||
|
||||
def _row_field(row: List[Any], index: int) -> Any:
|
||||
"""
|
||||
功能说明:安全读取腾讯 K 线行字段。
|
||||
参数说明:row 为腾讯 K 线数组,index 为字段索引。
|
||||
返回值说明:字段存在返回原始值,不存在返回 None。
|
||||
注意事项:不同标的可能缺少成交额字段。
|
||||
"""
|
||||
return row[index] if index < len(row) else None
|
||||
|
||||
|
||||
def _parse_trade_time(value: str) -> Optional[datetime]:
|
||||
"""
|
||||
功能说明:解析腾讯交易时间字段。
|
||||
|
||||
@ -88,38 +88,37 @@ class DuckDBStore:
|
||||
功能说明:批量写入 K 线缓存。
|
||||
参数说明:bars 为统一 KlineBar 列表。
|
||||
返回值说明:无返回值。
|
||||
注意事项:相同 symbol、period、trade_date 的记录会被覆盖。
|
||||
注意事项:相同 symbol、period、trade_date 的记录会被覆盖;为兼容 DuckDB 0.10 主键索引限制,先删除再插入。
|
||||
"""
|
||||
if not bars:
|
||||
return
|
||||
deduped = {}
|
||||
for bar in bars:
|
||||
deduped[(bar.symbol, bar.period, bar.trade_date)] = bar
|
||||
bars = list(deduped.values())
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
self.conn.execute("BEGIN TRANSACTION")
|
||||
try:
|
||||
for bar in bars:
|
||||
self.conn.execute(
|
||||
"DELETE FROM md.kline_bars WHERE symbol = ? AND period = ? AND trade_date = ?",
|
||||
[bar.symbol, bar.period, bar.trade_date],
|
||||
)
|
||||
self.conn.execute(
|
||||
"INSERT INTO md.kline_bars VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
bar.symbol,
|
||||
bar.period,
|
||||
bar.trade_date,
|
||||
bar.open,
|
||||
bar.high,
|
||||
bar.low,
|
||||
bar.close,
|
||||
bar.volume,
|
||||
bar.amount,
|
||||
bar.source,
|
||||
now,
|
||||
],
|
||||
)
|
||||
self.conn.execute("COMMIT")
|
||||
except Exception:
|
||||
self.conn.execute("ROLLBACK")
|
||||
raise
|
||||
for bar in bars:
|
||||
self.conn.execute(
|
||||
"DELETE FROM md.kline_bars WHERE symbol = ? AND period = ? AND trade_date = ?",
|
||||
[bar.symbol, bar.period, bar.trade_date],
|
||||
)
|
||||
for bar in bars:
|
||||
self.conn.execute(
|
||||
"INSERT INTO md.kline_bars VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
bar.symbol,
|
||||
bar.period,
|
||||
bar.trade_date,
|
||||
bar.open,
|
||||
bar.high,
|
||||
bar.low,
|
||||
bar.close,
|
||||
bar.volume,
|
||||
bar.amount,
|
||||
bar.source,
|
||||
now,
|
||||
],
|
||||
)
|
||||
|
||||
def load_kline(self, symbol: str, period: str, limit: int) -> List[KlineBar]:
|
||||
"""
|
||||
|
||||
1
strategy/__init__.py
Normal file
1
strategy/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""本地策略模块。"""
|
||||
227
strategy/a500_close_chart.py
Normal file
227
strategy/a500_close_chart.py
Normal file
@ -0,0 +1,227 @@
|
||||
"""策略1:绘制中证 A500 指数成立以来收盘价。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Sequence, Tuple
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from sidecar.config import load_config
|
||||
from sidecar.models import KlineBar
|
||||
from sidecar.service import UnifiedDataService
|
||||
from sidecar.sources.tencent import TencentFinanceSource
|
||||
from sidecar.storage import DuckDBStore
|
||||
|
||||
A500_SYMBOL = "sh000510"
|
||||
A500_START_DATE = date(2024, 9, 23)
|
||||
DEFAULT_OUTPUT_PATH = Path("strategy/output/a500_close.svg")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClosePoint:
|
||||
"""收盘价绘图点。"""
|
||||
|
||||
trade_date: date
|
||||
close: float
|
||||
|
||||
|
||||
def filter_close_points(bars: Iterable[KlineBar], start_date: date) -> List[ClosePoint]:
|
||||
"""
|
||||
功能说明:从 K 线列表中筛选指定起始日之后的有效收盘价。
|
||||
参数说明:bars 为 KlineBar 可迭代对象,start_date 为保留数据的起始交易日。
|
||||
返回值说明:返回按交易日期升序排列的 ClosePoint 列表。
|
||||
注意事项:close 为空的数据会被忽略,避免绘图断线。
|
||||
"""
|
||||
points = [ClosePoint(bar.trade_date, float(bar.close)) for bar in bars if bar.trade_date >= start_date and bar.close is not None]
|
||||
return sorted(points, key=lambda point: point.trade_date)
|
||||
|
||||
|
||||
def build_svg_line_chart(points: Sequence[ClosePoint], title: str, width: int = 1200, height: int = 680) -> str:
|
||||
"""
|
||||
功能说明:把收盘价序列渲染为 SVG 折线图。
|
||||
参数说明:points 为收盘价点序列,title 为图标题,width 和 height 为画布尺寸。
|
||||
返回值说明:返回完整 SVG 文本。
|
||||
注意事项:该函数只依赖标准库,方便在无绘图库环境中运行。
|
||||
"""
|
||||
if not points:
|
||||
raise ValueError("没有可绘制的收盘价数据")
|
||||
margin_left = 82
|
||||
margin_right = 36
|
||||
margin_top = 64
|
||||
margin_bottom = 82
|
||||
chart_width = width - margin_left - margin_right
|
||||
chart_height = height - margin_top - margin_bottom
|
||||
closes = [point.close for point in points]
|
||||
min_close = min(closes)
|
||||
max_close = max(closes)
|
||||
close_range = max_close - min_close or 1.0
|
||||
coordinates = _scale_points(points, margin_left, margin_top, chart_width, chart_height, min_close, close_range)
|
||||
polyline = " ".join("%.2f,%.2f" % coordinate for coordinate in coordinates)
|
||||
y_axis = _render_y_axis(_build_y_ticks(min_close, max_close, 5), margin_left, margin_top, chart_width, chart_height, min_close, close_range)
|
||||
x_axis = _render_x_axis(_build_x_ticks(points, 6), coordinates, margin_top + chart_height)
|
||||
first_point = points[0]
|
||||
last_point = points[-1]
|
||||
subtitle = "%s 至 %s,共 %d 个交易日" % (first_point.trade_date.isoformat(), last_point.trade_date.isoformat(), len(points))
|
||||
return """<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" viewBox=\"0 0 {width} {height}\">
|
||||
<rect width=\"100%\" height=\"100%\" fill=\"#ffffff\" />
|
||||
<text x=\"{center_x}\" y=\"32\" text-anchor=\"middle\" font-size=\"22\" font-weight=\"700\" fill=\"#111827\">{title}</text>
|
||||
<text x=\"{center_x}\" y=\"54\" text-anchor=\"middle\" font-size=\"13\" fill=\"#6b7280\">{subtitle}</text>
|
||||
{y_axis}
|
||||
<line x1=\"{left}\" y1=\"{top}\" x2=\"{left}\" y2=\"{bottom}\" stroke=\"#374151\" />
|
||||
<line x1=\"{left}\" y1=\"{bottom}\" x2=\"{right}\" y2=\"{bottom}\" stroke=\"#374151\" />
|
||||
{x_axis}
|
||||
<polyline fill=\"none\" stroke=\"#2563eb\" stroke-width=\"2.4\" points=\"{polyline}\" />
|
||||
<circle cx=\"{last_x:.2f}\" cy=\"{last_y:.2f}\" r=\"4\" fill=\"#dc2626\" />
|
||||
<text x=\"{last_label_x:.2f}\" y=\"{last_label_y:.2f}\" font-size=\"13\" fill=\"#dc2626\">最新收盘 {last_close:.2f}</text>
|
||||
<text x=\"{center_x}\" y=\"{footer_y}\" text-anchor=\"middle\" font-size=\"12\" fill=\"#9ca3af\">数据来源:sidecar / 腾讯财经,指数代码 sh000510</text>
|
||||
</svg>
|
||||
""".format(
|
||||
width=width,
|
||||
height=height,
|
||||
center_x=width / 2,
|
||||
title=escape(title),
|
||||
subtitle=escape(subtitle),
|
||||
y_axis="\n ".join(y_axis),
|
||||
x_axis="\n ".join(x_axis),
|
||||
left=margin_left,
|
||||
top=margin_top,
|
||||
bottom=margin_top + chart_height,
|
||||
right=width - margin_right,
|
||||
polyline=polyline,
|
||||
last_x=coordinates[-1][0],
|
||||
last_y=coordinates[-1][1],
|
||||
last_label_x=min(coordinates[-1][0] + 10, width - margin_right - 130),
|
||||
last_label_y=max(coordinates[-1][1] - 10, margin_top + 14),
|
||||
last_close=last_point.close,
|
||||
footer_y=height - 18,
|
||||
)
|
||||
|
||||
|
||||
def fetch_a500_close_points(refresh: bool = False) -> List[ClosePoint]:
|
||||
"""
|
||||
功能说明:通过 sidecar 获取中证 A500 成立以来的日线收盘价。
|
||||
参数说明:refresh 表示是否强制刷新 sidecar 缓存。
|
||||
返回值说明:返回中证 A500 成立以来的 ClosePoint 列表。
|
||||
注意事项:指数历史 K 线使用腾讯财经源,避免 mootdx 对 sh000510 解析为非指数行情。
|
||||
"""
|
||||
config = load_config()
|
||||
tencent = TencentFinanceSource(config.request_timeout_seconds)
|
||||
service = UnifiedDataService(DuckDBStore(config.db_path), config, tencent, [tencent])
|
||||
bars = service.get_kline(A500_SYMBOL, period="day", limit=800, refresh=refresh)
|
||||
return filter_close_points(bars, A500_START_DATE)
|
||||
|
||||
|
||||
def write_a500_close_chart(output_path: Path = DEFAULT_OUTPUT_PATH, refresh: bool = False) -> Path:
|
||||
"""
|
||||
功能说明:生成中证 A500 成立以来收盘价 SVG 图。
|
||||
参数说明:output_path 为输出 SVG 路径,refresh 表示是否强制刷新 sidecar 缓存。
|
||||
返回值说明:返回实际写入的图片路径。
|
||||
注意事项:输出目录不存在时会自动创建。
|
||||
"""
|
||||
points = fetch_a500_close_points(refresh=refresh)
|
||||
svg = build_svg_line_chart(points, "中证 A500 指数成立以来收盘价")
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(svg, encoding="utf-8")
|
||||
return output_path
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""
|
||||
功能说明:解析命令行参数。
|
||||
参数说明:无。
|
||||
返回值说明:返回 argparse.Namespace 参数对象。
|
||||
注意事项:默认输出到 strategy/output/a500_close.svg。
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="绘制中证 A500 指数成立以来收盘价")
|
||||
parser.add_argument("--output", default=str(DEFAULT_OUTPUT_PATH), help="输出 SVG 文件路径")
|
||||
parser.add_argument("--refresh", action="store_true", help="强制刷新 sidecar 缓存")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
功能说明:命令行入口函数。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:执行成功后会在终端打印输出文件路径。
|
||||
"""
|
||||
args = parse_args()
|
||||
output_path = write_a500_close_chart(Path(args.output), refresh=args.refresh)
|
||||
print("已生成图表: %s" % output_path)
|
||||
|
||||
|
||||
def _scale_points(points: Sequence[ClosePoint], margin_left: int, margin_top: int, chart_width: int, chart_height: int, min_close: float, close_range: float) -> List[Tuple[float, float]]:
|
||||
"""
|
||||
功能说明:把收盘价点转换为 SVG 坐标。
|
||||
参数说明:points 为收盘价点,margin_left 和 margin_top 为边距,chart_width 和 chart_height 为绘图区尺寸,min_close 和 close_range 为价格缩放参数。
|
||||
返回值说明:返回 SVG 坐标列表。
|
||||
注意事项:单点数据会绘制在横轴起点。
|
||||
"""
|
||||
max_index = max(len(points) - 1, 1)
|
||||
return [(margin_left + index / max_index * chart_width, margin_top + chart_height - ((point.close - min_close) / close_range) * chart_height) for index, point in enumerate(points)]
|
||||
|
||||
|
||||
def _build_y_ticks(min_close: float, max_close: float, tick_count: int) -> List[float]:
|
||||
"""
|
||||
功能说明:生成纵轴价格刻度。
|
||||
参数说明:min_close 为最低收盘价,max_close 为最高收盘价,tick_count 为刻度数量。
|
||||
返回值说明:返回价格刻度列表。
|
||||
注意事项:当最高最低相等时返回单一价格刻度。
|
||||
"""
|
||||
if tick_count <= 1 or min_close == max_close:
|
||||
return [min_close]
|
||||
step = (max_close - min_close) / (tick_count - 1)
|
||||
return [min_close + step * index for index in range(tick_count)]
|
||||
|
||||
|
||||
def _build_x_ticks(points: Sequence[ClosePoint], tick_count: int) -> List[Tuple[int, date]]:
|
||||
"""
|
||||
功能说明:生成横轴日期刻度。
|
||||
参数说明:points 为收盘价点序列,tick_count 为目标刻度数量。
|
||||
返回值说明:返回二元组列表,包含点索引和日期。
|
||||
注意事项:会去重索引,避免短序列重复显示同一天。
|
||||
"""
|
||||
if not points:
|
||||
return []
|
||||
max_index = len(points) - 1
|
||||
if tick_count <= 1 or max_index == 0:
|
||||
return [(0, points[0].trade_date)]
|
||||
indexes = sorted({round(index * max_index / (tick_count - 1)) for index in range(tick_count)})
|
||||
return [(index, points[index].trade_date) for index in indexes]
|
||||
|
||||
|
||||
def _render_y_axis(ticks: Sequence[float], margin_left: int, margin_top: int, chart_width: int, chart_height: int, min_close: float, close_range: float) -> List[str]:
|
||||
"""
|
||||
功能说明:渲染纵轴网格线和文本。
|
||||
参数说明:ticks 为价格刻度,margin_left 和 margin_top 为边距,chart_width 和 chart_height 为绘图区尺寸,min_close 和 close_range 为价格缩放参数。
|
||||
返回值说明:返回 SVG 片段列表。
|
||||
注意事项:该函数仅生成轴元素,不生成折线。
|
||||
"""
|
||||
lines = []
|
||||
for tick_value in ticks:
|
||||
y_position = margin_top + chart_height - ((tick_value - min_close) / close_range) * chart_height
|
||||
lines.append('<line x1="%d" y1="%.2f" x2="%d" y2="%.2f" stroke="#e5e7eb" />' % (margin_left, y_position, margin_left + chart_width, y_position))
|
||||
lines.append('<text x="%d" y="%.2f" text-anchor="end" font-size="12" fill="#4b5563">%.2f</text>' % (margin_left - 10, y_position + 4, tick_value))
|
||||
return lines
|
||||
|
||||
|
||||
def _render_x_axis(ticks: Sequence[Tuple[int, date]], coordinates: Sequence[Tuple[float, float]], bottom: int) -> List[str]:
|
||||
"""
|
||||
功能说明:渲染横轴日期刻度和文本。
|
||||
参数说明:ticks 为日期刻度,coordinates 为已缩放坐标,bottom 为横轴纵坐标。
|
||||
返回值说明:返回 SVG 片段列表。
|
||||
注意事项:tick 索引必须存在于 coordinates 中。
|
||||
"""
|
||||
lines = []
|
||||
for index, tick_date in ticks:
|
||||
x_position = coordinates[index][0]
|
||||
lines.append('<line x1="%.2f" y1="%d" x2="%.2f" y2="%d" stroke="#d1d5db" />' % (x_position, bottom, x_position, bottom + 6))
|
||||
lines.append('<text x="%.2f" y="%d" text-anchor="middle" font-size="12" fill="#4b5563">%s</text>' % (x_position, bottom + 26, tick_date.isoformat()))
|
||||
return lines
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
50
strategy/doc/strategy1_a500_close_chart.md
Normal file
50
strategy/doc/strategy1_a500_close_chart.md
Normal file
@ -0,0 +1,50 @@
|
||||
# 策略1:中证 A500 成立以来收盘价图
|
||||
|
||||
## 目标
|
||||
|
||||
从 `sidecar` 模块获取中证 A500 指数日线数据,将指数自成立日至今日的收盘价绘制为一张 SVG 折线图。
|
||||
|
||||
## 假设
|
||||
|
||||
- 指数名称:中证 A500 指数。
|
||||
- 指数代码:`sh000510`。
|
||||
- 成立起始日:`2024-09-23`。
|
||||
- 数据周期:日线。
|
||||
- K 线数据源:`sidecar.sources.tencent.TencentFinanceSource`。
|
||||
- 输出格式:SVG 图片,默认路径为 `strategy/output/a500_close.svg`。
|
||||
|
||||
## 数据源说明
|
||||
|
||||
策略通过 `sidecar` 的统一服务读取和缓存数据,但 K 线源显式使用腾讯财经。原因是当前 mootdx 对 `sh000510` 返回的日线价格量级异常,腾讯财经快照和 K 线价格量级一致,更适合绘制中证 A500 指数历史走势。
|
||||
|
||||
## 运行方式
|
||||
|
||||
```powershell
|
||||
python -m strategy.a500_close_chart --refresh
|
||||
```
|
||||
|
||||
可指定输出路径:
|
||||
|
||||
```powershell
|
||||
python -m strategy.a500_close_chart --output strategy/output/a500_close.svg --refresh
|
||||
```
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `--output` | string | `strategy/output/a500_close.svg` | 输出 SVG 文件路径 |
|
||||
| `--refresh` | bool | `False` | 是否强制刷新 sidecar 缓存 |
|
||||
|
||||
## 输出
|
||||
|
||||
生成一张 SVG 图片:
|
||||
|
||||
- 横轴:交易日期,范围为中证 A500 成立日 `2024-09-23` 至当前可获取的最新交易日。
|
||||
- 纵轴:日收盘价。
|
||||
- 标题:`中证 A500 指数成立以来收盘价`。
|
||||
|
||||
## 测试用例
|
||||
|
||||
- `4-1`:过滤成立日之前的数据,并忽略空收盘价。
|
||||
- `4-2`:根据收盘价点生成包含标题、日期和折线的 SVG。
|
||||
33
strategy/output/a500_close.svg
Normal file
33
strategy/output/a500_close.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
45
tests/test_strategy_a500.py
Normal file
45
tests/test_strategy_a500.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""策略1中证 A500 收盘价图测试。"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_4_1_filter_a500_close_points() -> None:
|
||||
"""
|
||||
功能说明:验证策略1会过滤成立日之前和空收盘价数据。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 4-1。
|
||||
"""
|
||||
bars = [
|
||||
KlineBar("sh000510", "day", date(2024, 9, 20), 1, 1, 1, 1, 1, 1, "fake"),
|
||||
KlineBar("sh000510", "day", A500_START_DATE, 1, 1, 1, None, 1, 1, "fake"),
|
||||
KlineBar("sh000510", "day", date(2024, 9, 24), 1, 1, 1, 1000.5, 1, 1, "fake"),
|
||||
]
|
||||
|
||||
points = filter_close_points(bars, A500_START_DATE)
|
||||
|
||||
assert points == [ClosePoint(date(2024, 9, 24), 1000.5)]
|
||||
|
||||
|
||||
def test_4_2_build_svg_line_chart() -> None:
|
||||
"""
|
||||
功能说明:验证策略1可生成 SVG 折线图文本。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 4-2。
|
||||
"""
|
||||
points = [
|
||||
ClosePoint(date(2024, 9, 23), 1000.0),
|
||||
ClosePoint(date(2024, 9, 24), 1010.0),
|
||||
ClosePoint(date(2024, 9, 25), 990.0),
|
||||
]
|
||||
|
||||
svg = build_svg_line_chart(points, "中证 A500 指数成立以来收盘价")
|
||||
|
||||
assert svg.startswith("<svg")
|
||||
assert "中证 A500 指数成立以来收盘价" in svg
|
||||
assert "2024-09-23" in svg
|
||||
assert "<polyline" in svg
|
||||
@ -1,6 +1,6 @@
|
||||
"""腾讯财经适配器测试。"""
|
||||
|
||||
from sidecar.sources.tencent import parse_tencent_payload, snapshot_from_tencent_fields
|
||||
from sidecar.sources.tencent import kline_from_tencent_payload, parse_tencent_payload, snapshot_from_tencent_fields
|
||||
|
||||
|
||||
def test_1_1_tencent_snapshot_uses_correct_pb_index() -> None:
|
||||
@ -47,4 +47,21 @@ def test_1_2_parse_tencent_payload() -> None:
|
||||
parsed = parse_tencent_payload(payload)
|
||||
|
||||
assert len(parsed) == 88
|
||||
assert parsed[0] == "v"
|
||||
assert parsed[0] == "v"
|
||||
|
||||
|
||||
def test_1_3_parse_tencent_kline_payload() -> None:
|
||||
"""
|
||||
功能说明:验证腾讯财经 K 线 JSON 可转换为统一 K 线。
|
||||
参数说明:无。
|
||||
返回值说明:无返回值。
|
||||
注意事项:用例编号 1-3,字段顺序为日期、开盘、收盘、最高、最低、成交量。
|
||||
"""
|
||||
payload = '{"code":0,"msg":"","data":{"sh000510":{"day":[["2024-09-23","3392.95","3368.53","3392.95","3368.53","1"]]}}}'
|
||||
|
||||
bars = kline_from_tencent_payload("sh000510", "day", payload)
|
||||
|
||||
assert len(bars) == 1
|
||||
assert bars[0].symbol == "sh000510"
|
||||
assert bars[0].close == 3368.53
|
||||
assert bars[0].source == "tencent"
|
||||
Loading…
Reference in New Issue
Block a user