"""策略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 """
""".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('' % (margin_left, y_position, margin_left + chart_width, y_position))
lines.append('%.2f' % (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('' % (x_position, bottom, x_position, bottom + 6))
lines.append('%s' % (x_position, bottom + 26, tick_date.isoformat()))
return lines
if __name__ == "__main__":
main()