本地策略可行

This commit is contained in:
cheney
2026-06-23 11:15:04 +08:00
parent c4db32374e
commit db4e519cc2
5 changed files with 541 additions and 1 deletions
+10 -1
View File
@@ -32,4 +32,13 @@ uvicorn sidecar.api:app --host 127.0.0.1 --port 8765
python -m strategy.a500_close_chart --refresh python -m strategy.a500_close_chart --refresh
``` ```
默认输出:`strategy/output/a500_close.svg`。 默认输出:`strategy/output/a500_close.svg`。
## 策略2:中证 A500 与沪深 300 双轴收盘价图
运行:
```powershell
python -m strategy.a500_hs300_close_chart --refresh
```
默认输出:`strategy/output/a500_hs300_close.svg`。
+359
View File
@@ -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 """<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="56" text-anchor="middle" font-size="13" fill="#6b7280">{subtitle}</text>
<circle cx="{legend_left_x}" cy="74" r="5" fill="{left_color}" />
<text x="{legend_left_text_x}" y="78" font-size="13" fill="#374151">{left_name}(左轴)</text>
<circle cx="{legend_right_x}" cy="74" r="5" fill="{right_color}" />
<text x="{legend_right_text_x}" y="78" font-size="13" fill="#374151">{right_name}(右轴)</text>
{y_axis}
<line x1="{left_axis}" y1="{top}" x2="{left_axis}" y2="{bottom}" stroke="#374151" />
<line x1="{right_axis}" y1="{top}" x2="{right_axis}" y2="{bottom}" stroke="#374151" />
<line x1="{left_axis}" y1="{bottom}" x2="{right_axis}" y2="{bottom}" stroke="#374151" />
{x_axis}
<polyline fill="none" stroke="{left_color}" stroke-width="2.4" points="{left_polyline}" />
<polyline fill="none" stroke="{right_color}" stroke-width="2.4" points="{right_polyline}" />
<text x="{center_x}" y="{footer_y}" text-anchor="middle" font-size="12" fill="#9ca3af">数据来源:sidecar / 腾讯财经;双轴按首日价格归一化,等距离代表等涨跌比例</text>
</svg>
""".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('<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="%s">%.2f (%+.1f%%)</text>' % (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('<text x="%d" y="%.2f" text-anchor="start" font-size="12" fill="%s">%.2f (%+.1f%%)</text>' % (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('<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="%s">%.2f</text>' % (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('<text x="%d" y="%.2f" text-anchor="start" font-size="12" fill="%s">%.2f</text>' % (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('<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()
@@ -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 坐标。
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

+68
View File
@@ -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("<svg")
assert "中证 A500 与沪深 300 收盘价对比" in svg
assert "中证 A500(左轴)" in svg
assert "沪深 300(右轴)" in svg
assert svg.count("<polyline") == 2
def test_5_3_same_return_ratio_maps_to_same_y() -> 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]