From db4e519cc21abf90a3f3c09087cea59c6487de41 Mon Sep 17 00:00:00 2001 From: cheney Date: Tue, 23 Jun 2026 11:15:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E7=AD=96=E7=95=A5=E5=8F=AF?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +- strategy/a500_hs300_close_chart.py | 359 ++++++++++++++++++ .../doc/strategy2_a500_hs300_close_chart.md | 62 +++ strategy/output/a500_hs300_close.svg | 42 ++ tests/test_strategy_a500_hs300.py | 68 ++++ 5 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 strategy/a500_hs300_close_chart.py create mode 100644 strategy/doc/strategy2_a500_hs300_close_chart.md create mode 100644 strategy/output/a500_hs300_close.svg create mode 100644 tests/test_strategy_a500_hs300.py diff --git a/README.md b/README.md index 0fb42b6..116757b 100644 --- a/README.md +++ b/README.md @@ -32,4 +32,13 @@ uvicorn sidecar.api:app --host 127.0.0.1 --port 8765 python -m strategy.a500_close_chart --refresh ``` -默认输出:`strategy/output/a500_close.svg`。 \ No newline at end of file +默认输出:`strategy/output/a500_close.svg`。 +## 策略2:中证 A500 与沪深 300 双轴收盘价图 + +运行: + +```powershell +python -m strategy.a500_hs300_close_chart --refresh +``` + +默认输出:`strategy/output/a500_hs300_close.svg`。 \ No newline at end of file diff --git a/strategy/a500_hs300_close_chart.py b/strategy/a500_hs300_close_chart.py new file mode 100644 index 0000000..005aeef --- /dev/null +++ b/strategy/a500_hs300_close_chart.py @@ -0,0 +1,359 @@ +"""策略2:绘制中证 A500 与沪深 300 成立以来双轴收盘价。""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from datetime import date +from pathlib import Path +from typing import Dict, 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 +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") + + +@dataclass(frozen=True) +class DualAxisSeries: + """双轴图数据序列。""" + + name: str + symbol: str + color: str + points: List[ClosePoint] + + +def align_points_by_date(left_points: Sequence[ClosePoint], right_points: Sequence[ClosePoint]) -> Tuple[List[ClosePoint], List[ClosePoint]]: + """ + 功能说明:按共同交易日对齐两组收盘价点。 + 参数说明:left_points 为左轴序列,right_points 为右轴序列。 + 返回值说明:返回按日期升序排列的两组 ClosePoint。 + 注意事项:只保留双方同时存在的交易日,避免横轴错位。 + """ + left_map = {point.trade_date: point for point in left_points} + right_map = {point.trade_date: point for point in right_points} + dates = sorted(set(left_map).intersection(right_map)) + return [left_map[item] for item in dates], [right_map[item] for item in dates] + + +def build_dual_axis_svg_chart(left: DualAxisSeries, right: DualAxisSeries, title: str, width: int = 1200, height: int = 680) -> str: + """ + 功能说明:把两组收盘价序列渲染为双 Y 轴 SVG 折线图。 + 参数说明:left 为左轴序列,right 为右轴序列,title 为标题,width 和 height 为画布尺寸。 + 返回值说明:返回完整 SVG 文本。 + 注意事项:两组序列按首日价格归一化,共享涨跌比例坐标,确保相同涨跌比例对应相同纵向距离。 + """ + if not left.points or not right.points: + raise ValueError("没有可绘制的双轴收盘价数据") + if len(left.points) != len(right.points): + raise ValueError("双轴序列长度不一致,请先按交易日对齐") + + margin_left = 82 + margin_right = 82 + margin_top = 78 + margin_bottom = 86 + chart_width = width - margin_left - margin_right + chart_height = height - margin_top - margin_bottom + left_base = left.points[0].close + right_base = right.points[0].close + ratio_min, ratio_max = _shared_ratio_range(left.points, right.points) + ratio_range = ratio_max - ratio_min or 1.0 + left_coordinates = _scale_points_by_ratio(left.points, left_base, margin_left, margin_top, chart_width, chart_height, ratio_min, ratio_range) + right_coordinates = _scale_points_by_ratio(right.points, right_base, margin_left, margin_top, chart_width, chart_height, ratio_min, ratio_range) + ratio_ticks = _build_y_ticks(ratio_min, ratio_max, 5) + y_axis = _render_left_axis_by_ratio(ratio_ticks, left_base, margin_left, margin_top, chart_width, chart_height, ratio_min, ratio_range, left.color) + y_axis += _render_right_axis_by_ratio(ratio_ticks, right_base, width - margin_right, margin_top, chart_height, ratio_min, ratio_range, right.color) + x_axis = _render_x_axis(_build_x_ticks(left.points, 6), left_coordinates, margin_top + chart_height) + first_date = left.points[0].trade_date + last_date = left.points[-1].trade_date + subtitle = "%s 至 %s,共 %d 个共同交易日;纵轴按首日收盘价归一化" % (first_date.isoformat(), last_date.isoformat(), len(left.points)) + + return """ + + {title} + {subtitle} + + {left_name}(左轴) + + {right_name}(右轴) + {y_axis} + + + + {x_axis} + + + 数据来源:sidecar / 腾讯财经;双轴按首日价格归一化,等距离代表等涨跌比例 + +""".format( + width=width, + height=height, + center_x=width / 2, + title=escape(title), + subtitle=escape(subtitle), + legend_left_x=width / 2 - 140, + legend_left_text_x=width / 2 - 128, + legend_right_x=width / 2 + 40, + legend_right_text_x=width / 2 + 52, + left_color=left.color, + right_color=right.color, + left_name=escape(left.name), + right_name=escape(right.name), + y_axis="\n ".join(y_axis), + x_axis="\n ".join(x_axis), + left_axis=margin_left, + right_axis=width - margin_right, + top=margin_top, + bottom=margin_top + chart_height, + left_polyline=" ".join("%.2f,%.2f" % coordinate for coordinate in left_coordinates), + right_polyline=" ".join("%.2f,%.2f" % coordinate for coordinate in right_coordinates), + footer_y=height - 18, + ) + + +def fetch_dual_axis_series(refresh: bool = False) -> Tuple[DualAxisSeries, DualAxisSeries]: + """ + 功能说明:通过 sidecar 获取 A500 和沪深 300 的对齐收盘价序列。 + 参数说明:refresh 表示是否强制刷新 sidecar 缓存。 + 返回值说明:返回 A500 左轴序列和沪深 300 右轴序列。 + 注意事项:横轴范围以 A500 成立日为起点,并只保留共同交易日。 + """ + config = load_config() + tencent = TencentFinanceSource(config.request_timeout_seconds) + service = UnifiedDataService(DuckDBStore(config.db_path), config, tencent, [tencent]) + a500_points = filter_close_points(service.get_kline(A500_SYMBOL, period="day", limit=800, refresh=refresh), A500_START_DATE) + hs300_points = filter_close_points(service.get_kline(HS300_SYMBOL, period="day", limit=800, refresh=refresh), A500_START_DATE) + aligned_a500, aligned_hs300 = align_points_by_date(a500_points, hs300_points) + return ( + DualAxisSeries("中证 A500", A500_SYMBOL, "#2563eb", aligned_a500), + DualAxisSeries("沪深 300", HS300_SYMBOL, "#dc2626", aligned_hs300), + ) + + +def write_a500_hs300_close_chart(output_path: Path = DEFAULT_OUTPUT_PATH, refresh: bool = False) -> Path: + """ + 功能说明:生成 A500 与沪深 300 双轴收盘价 SVG 图。 + 参数说明:output_path 为输出 SVG 路径,refresh 表示是否强制刷新 sidecar 缓存。 + 返回值说明:返回实际写入的图片路径。 + 注意事项:输出目录不存在时会自动创建。 + """ + left, right = fetch_dual_axis_series(refresh=refresh) + svg = build_dual_axis_svg_chart(left, right, "中证 A500 与沪深 300 收盘价对比") + 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_hs300_close.svg。 + """ + parser = argparse.ArgumentParser(description="绘制中证 A500 与沪深 300 双轴收盘价") + 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_hs300_close_chart(Path(args.output), refresh=args.refresh) + print("已生成图表: %s" % output_path) + + +def _shared_ratio_range(left_points: Sequence[ClosePoint], right_points: Sequence[ClosePoint]) -> Tuple[float, float]: + """ + 功能说明:计算两组序列共享的涨跌比例范围。 + 参数说明:left_points 为左轴序列,right_points 为右轴序列。 + 返回值说明:返回最小涨跌比例和最大涨跌比例,例如 0.1 表示上涨 10%。 + 注意事项:涨跌比例都以各自首个收盘价为基准。 + """ + ratios = _ratios_from_base(left_points) + _ratios_from_base(right_points) + return min(ratios), max(ratios) + + +def _ratios_from_base(points: Sequence[ClosePoint]) -> List[float]: + """ + 功能说明:计算序列相对首个收盘价的涨跌比例。 + 参数说明:points 为收盘价点序列。 + 返回值说明:返回涨跌比例列表。 + 注意事项:首个点的涨跌比例固定为 0。 + """ + base = points[0].close + if base == 0: + raise ValueError("基准收盘价不能为 0") + return [(point.close / base) - 1 for point in points] + + +def _scale_points_by_ratio(points: Sequence[ClosePoint], base_close: float, margin_left: int, margin_top: int, chart_width: int, chart_height: int, ratio_min: float, ratio_range: float) -> List[Tuple[float, float]]: + """ + 功能说明:按涨跌比例把收盘价点转换为 SVG 坐标。 + 参数说明:points 为收盘价点,base_close 为首日收盘价,margin_left 和 margin_top 为边距,chart_width 和 chart_height 为绘图区尺寸,ratio_min 和 ratio_range 为共享比例缩放参数。 + 返回值说明:返回 SVG 坐标列表。 + 注意事项:不同指数相同涨跌比例会得到相同 Y 坐标。 + """ + if base_close == 0: + raise ValueError("基准收盘价不能为 0") + max_index = max(len(points) - 1, 1) + coordinates = [] + for index, point in enumerate(points): + ratio = point.close / base_close - 1 + x_position = margin_left + index / max_index * chart_width + y_position = margin_top + chart_height - ((ratio - ratio_min) / ratio_range) * chart_height + coordinates.append((x_position, y_position)) + return coordinates + + +def _price_from_ratio(base_close: float, ratio: float) -> float: + """ + 功能说明:把涨跌比例转换为某个指数自身价格。 + 参数说明:base_close 为指数首日收盘价,ratio 为涨跌比例。 + 返回值说明:返回该比例对应的价格。 + 注意事项:用于左右 Y 轴在同一比例位置显示各自价格刻度。 + """ + return base_close * (1 + ratio) + + +def _render_left_axis_by_ratio(ticks: Sequence[float], base_close: float, margin_left: int, margin_top: int, chart_width: int, chart_height: int, ratio_min: float, ratio_range: float, color: str) -> List[str]: + """ + 功能说明:按共享涨跌比例渲染左侧价格轴。 + 参数说明:ticks 为比例刻度,base_close 为左轴基准价,margin_left 和 margin_top 为边距,chart_width 和 chart_height 为绘图区尺寸,ratio_min 和 ratio_range 为比例缩放参数,color 为文本颜色。 + 返回值说明:返回 SVG 片段列表。 + 注意事项:左轴显示 A500 价格,但刻度位置由共享涨跌比例决定。 + """ + lines = [] + for tick_ratio in ticks: + y_position = margin_top + chart_height - ((tick_ratio - ratio_min) / ratio_range) * chart_height + price = _price_from_ratio(base_close, tick_ratio) + percent = tick_ratio * 100 + lines.append('' % (margin_left, y_position, margin_left + chart_width, y_position)) + lines.append('%.2f (%+.1f%%)' % (margin_left - 10, y_position + 4, color, price, percent)) + return lines + + +def _render_right_axis_by_ratio(ticks: Sequence[float], base_close: float, right_axis: int, margin_top: int, chart_height: int, ratio_min: float, ratio_range: float, color: str) -> List[str]: + """ + 功能说明:按共享涨跌比例渲染右侧价格轴。 + 参数说明:ticks 为比例刻度,base_close 为右轴基准价,right_axis 为右轴横坐标,margin_top 和 chart_height 为绘图区参数,ratio_min 和 ratio_range 为比例缩放参数,color 为文本颜色。 + 返回值说明:返回 SVG 片段列表。 + 注意事项:右轴显示沪深 300 价格,但刻度位置由共享涨跌比例决定。 + """ + lines = [] + for tick_ratio in ticks: + y_position = margin_top + chart_height - ((tick_ratio - ratio_min) / ratio_range) * chart_height + price = _price_from_ratio(base_close, tick_ratio) + percent = tick_ratio * 100 + lines.append('%.2f (%+.1f%%)' % (right_axis + 10, y_position + 4, color, price, percent)) + return lines +def _value_range(points: Sequence[ClosePoint]) -> Tuple[float, float]: + """ + 功能说明:计算收盘价范围。 + 参数说明:points 为收盘价点序列。 + 返回值说明:返回最低值和最高值。 + 注意事项:调用方需保证 points 非空。 + """ + values = [point.close for point in points] + return min(values), max(values) + + +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_left_axis(ticks: Sequence[float], margin_left: int, margin_top: int, chart_width: int, chart_height: int, min_close: float, close_range: float, color: str) -> List[str]: + """ + 功能说明:渲染左侧纵轴网格和刻度。 + 参数说明:ticks 为价格刻度,margin_left 和 margin_top 为边距,chart_width 和 chart_height 为绘图区尺寸,min_close 和 close_range 为价格缩放参数,color 为轴文本颜色。 + 返回值说明:返回 SVG 片段列表。 + 注意事项:左轴刻度对应 A500。 + """ + 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, color, tick_value)) + return lines + + +def _render_right_axis(ticks: Sequence[float], right_axis: int, margin_top: int, chart_height: int, min_close: float, close_range: float, color: str) -> List[str]: + """ + 功能说明:渲染右侧纵轴刻度。 + 参数说明:ticks 为价格刻度,right_axis 为右轴横坐标,margin_top 和 chart_height 为绘图区参数,min_close 和 close_range 为价格缩放参数,color 为轴文本颜色。 + 返回值说明:返回 SVG 片段列表。 + 注意事项:右轴刻度对应沪深 300。 + """ + lines = [] + for tick_value in ticks: + y_position = margin_top + chart_height - ((tick_value - min_close) / close_range) * chart_height + lines.append('%.2f' % (right_axis + 10, y_position + 4, color, 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() \ No newline at end of file diff --git a/strategy/doc/strategy2_a500_hs300_close_chart.md b/strategy/doc/strategy2_a500_hs300_close_chart.md new file mode 100644 index 0000000..a68f12b --- /dev/null +++ b/strategy/doc/strategy2_a500_hs300_close_chart.md @@ -0,0 +1,62 @@ +# 策略2:中证 A500 与沪深 300 双轴收盘价图 + +## 目标 + +从 `sidecar` 模块获取中证 A500 与沪深 300 指数日线数据,将两者自中证 A500 成立日至今日的收盘价绘制在同一张 SVG 图中。由于两个指数价格区间不同,图表使用双 Y 轴:左轴对应中证 A500,右轴对应沪深 300。 + +## 假设 + +- 中证 A500 指数代码:`sh000510`。 +- 沪深 300 指数代码:`sh000300`。 +- 横轴起始日:`2024-09-23`。 +- 数据周期:日线。 +- K 线数据源:`sidecar.sources.tencent.TencentFinanceSource`。 +- 输出格式:SVG 图片,默认路径为 `strategy/output/a500_hs300_close.svg`。 + +## 双轴缩放规则 + +- 两个指数都以 `2024-09-23` 的收盘价作为各自基准价。 +- Y 坐标按相对基准价的涨跌比例计算,而不是按各自价格区间独立拉伸。 +- 因此两个指数起点位于同一水平线;相同涨跌比例对应相同纵向距离。 +- 终点不强行对齐,终点高度真实反映各自相对起点的涨跌幅差异。 +- 左右 Y 轴仍显示各自指数价格,同时括号展示对应涨跌比例。 + +## 数据处理 + +- 两个指数都通过 `sidecar` 统一服务读取并写入 DuckDB 缓存。 +- 先过滤 `2024-09-23` 之前的数据。 +- 再按共同交易日对齐,避免不同市场日历或缺失数据导致横轴错位。 + +## 运行方式 + +```powershell +python -m strategy.a500_hs300_close_chart --refresh +``` + +可指定输出路径: + +```powershell +python -m strategy.a500_hs300_close_chart --output strategy/output/a500_hs300_close.svg --refresh +``` + +## 输入 + +| 参数 | 类型 | 默认值 | 说明 | +|---|---|---|---| +| `--output` | string | `strategy/output/a500_hs300_close.svg` | 输出 SVG 文件路径 | +| `--refresh` | bool | `False` | 是否强制刷新 sidecar 缓存 | + +## 输出 + +生成一张 SVG 图片: + +- 横轴:共同交易日期,范围为中证 A500 成立日 `2024-09-23` 至当前可获取的最新交易日。 +- 左轴:中证 A500 日收盘价。 +- 右轴:沪深 300 日收盘价。 +- 标题:`中证 A500 与沪深 300 收盘价对比`。 + +## 测试用例 + +- `5-1`:按共同交易日对齐两组收盘价。 +- `5-2`:生成包含双轴图例和两条折线的 SVG。 +- `5-3`:相同涨跌比例映射到相同 Y 坐标。 \ No newline at end of file diff --git a/strategy/output/a500_hs300_close.svg b/strategy/output/a500_hs300_close.svg new file mode 100644 index 0000000..4febb4b --- /dev/null +++ b/strategy/output/a500_hs300_close.svg @@ -0,0 +1,42 @@ + + + 中证 A500 与沪深 300 收盘价对比 + 2024-09-23 至 2026-06-23,共 421 个共同交易日;纵轴按首日收盘价归一化 + + 中证 A500(左轴) + + 沪深 300(右轴) + + 3725.78 (+0.0%) + + 4387.73 (+17.8%) + + 5049.67 (+35.5%) + + 5711.61 (+53.3%) + + 6373.56 (+71.1%) + 3212.76 (+0.0%) + 3783.56 (+17.8%) + 4354.36 (+35.5%) + 4925.16 (+53.3%) + 5495.95 (+71.1%) + + + + + 2024-09-23 + + 2025-01-27 + + 2025-06-09 + + 2025-10-13 + + 2026-02-10 + + 2026-06-23 + + + 数据来源:sidecar / 腾讯财经;双轴按首日价格归一化,等距离代表等涨跌比例 + diff --git a/tests/test_strategy_a500_hs300.py b/tests/test_strategy_a500_hs300.py new file mode 100644 index 0000000..13c0d2f --- /dev/null +++ b/tests/test_strategy_a500_hs300.py @@ -0,0 +1,68 @@ +"""策略2中证 A500 与沪深 300 双轴图测试。""" + +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 + + +def test_5_1_align_points_by_common_date() -> None: + """ + 功能说明:验证策略2按共同交易日对齐两组收盘价。 + 参数说明:无。 + 返回值说明:无返回值。 + 注意事项:用例编号 5-1。 + """ + left = [ClosePoint(date(2024, 9, 23), 100), ClosePoint(date(2024, 9, 24), 101)] + right = [ClosePoint(date(2024, 9, 24), 201), ClosePoint(date(2024, 9, 25), 202)] + + aligned_left, aligned_right = align_points_by_date(left, right) + + assert aligned_left == [ClosePoint(date(2024, 9, 24), 101)] + assert aligned_right == [ClosePoint(date(2024, 9, 24), 201)] + + +def test_5_2_build_dual_axis_svg_chart() -> None: + """ + 功能说明:验证策略2可生成包含双轴和两条折线的 SVG。 + 参数说明:无。 + 返回值说明:无返回值。 + 注意事项:用例编号 5-2。 + """ + left = DualAxisSeries( + "中证 A500", + "sh000510", + "#2563eb", + [ClosePoint(date(2024, 9, 23), 3700), ClosePoint(date(2024, 9, 24), 3710)], + ) + right = DualAxisSeries( + "沪深 300", + "sh000300", + "#dc2626", + [ClosePoint(date(2024, 9, 23), 3300), ClosePoint(date(2024, 9, 24), 3310)], + ) + + svg = build_dual_axis_svg_chart(left, right, "中证 A500 与沪深 300 收盘价对比") + + assert svg.startswith(" None: + """ + 功能说明:验证策略2相同涨跌比例会映射到相同 Y 坐标。 + 参数说明:无。 + 返回值说明:无返回值。 + 注意事项:用例编号 5-3,两个指数起点不同但第二天都上涨 10%。 + """ + left = [ClosePoint(date(2024, 9, 23), 1000), ClosePoint(date(2024, 9, 24), 1100)] + right = [ClosePoint(date(2024, 9, 23), 3000), ClosePoint(date(2024, 9, 24), 3300)] + + left_coordinates = _scale_points_by_ratio(left, 1000, 0, 0, 100, 100, 0, 0.1) + right_coordinates = _scale_points_by_ratio(right, 3000, 0, 0, 100, 100, 0, 0.1) + + assert left_coordinates[0][1] == right_coordinates[0][1] + assert left_coordinates[1][1] == right_coordinates[1][1]