Skip to content

Latest commit

 

History

History
531 lines (418 loc) · 26.2 KB

File metadata and controls

531 lines (418 loc) · 26.2 KB

v2.7.0 重构分析报告:从继承到组合架构

分析日期: 2026-06-20 分析范围: __init__.py, abstract.py, chart.py, handler.ts


一、架构变更总览

旧架构(_bak):继承模式

AbstractChart(Candlestick, Pane)
    ├── Candlestick(SeriesCommon)  ← 直接继承,K线数据在 self 上
    │       ├── self._interval / offset / _period_locked(SeriesCommon 层)
    │       ├── self.candle_data / self._has_volume / self._has_open_interest
    │       └── set() / update() / update_bars()(OHLCV 一体化处理)
    └── 附加 Line / Histogram 系列 → self._lines[]

新架构(当前):组合模式

AbstractChart(Pane)  ← 不再继承 Candlestick
    ├── self.candle = CandleSeries._wrap_handler(self)  ← 组合,共享 Handler JS 对象
    ├── self.volume = VolumeSeries(candle, _wrap_existing=True)  ← 复用 Handler.volumeSeries
    ├── self.oi = OpenInterestSeries(candle, _wrap_existing=True)  ← 复用 Handler.openInterestSeries
    ├── self._interval / offset / _period_locked  ← 从 SeriesCommon 迁移到 AbstractChart
    └── 附加 Line / Histogram 系列 → self._lines[]

核心变化

  1. AbstractChart 不再是 Candlestick 的子类,而是通过 self.candle 持有 CandleSeries 实例
  2. self.candle.id == self.id(共享同一个 Handler JS 对象引用)
  3. Volume 和 OI 从 Candlestick 内部分离为独立的 VolumeSeries / OpenInterestSeries
  4. 时间级别(_interval/offset/_period_locked)从 SeriesCommon 上移到 AbstractChart

二、逐文件分析

2.1 __init__.py — 导出变更

项目 _bak 当前
导入 AbstractChart, Window, CandleSeries AbstractChart, Window, CandleSeries, VolumeSeries, OpenInterestSeries

变更: 新增导出 VolumeSeriesOpenInterestSeries评估: ✅ 正确,新增类需要对外暴露。


2.2 abstract.py — 核心重构文件

2.2.1 import 变更

新增 import(util 模块):

# _bak: 无这些函数
# 当前: 新增 4 个辅助函数
normal_df, merge_value_by_time, get_df_interval_offset, time_to_bar_time

评估: ✅ 正确。这些函数从 SeriesCommon 的内部实现提取为 util 模块的独立函数,便于 AbstractChartCandleSeries 共用。

2.2.2 SeriesCommon 类变化

特性 _bak 当前 评估
__init__ 中的 _interval self._interval = chart._interval if hasattr(chart, '_interval') else 1 已删除 — 不再持有 _interval ✅ 正确,时间级别统一由 chart 持有
__init__ 中的 offset self.offset = 0 已删除 ✅ 正确
__init__ 中的 _period_locked self._period_locked = False 已删除 ✅ 正确
set_period() 方法 定义在 SeriesCommon 已删除(迁移到 AbstractChart) ✅ 正确
_get_df_interval_offset() 内联实现 委托到 util.get_df_interval_offset() ✅ 精简
_normal_df() 内联实现 委托到 util.normal_df() ✅ 精简
_merge_value_by_time() 内联实现 委托到 util.merge_value_by_time() ✅ 精简
_single_datetime_format() 使用 self._interval 委托到 self._chart._single_datetime_format() ✅ 正确
_time_to_bar_time() 使用 self._interval/self.offset 委托到 self._chart._time_to_bar_time() ✅ 正确
set() 方法 检查 self._interval 检查 self._chart._interval ✅ 正确,增加 RuntimeError 提示
update_bars() 原有实现 抽取 _clean_update_bars() 辅助方法 ✅ 改善代码复用

关键设计: 所有通过 self._chart 间接访问时间级别,保证子系列(Line/Histogram)与主图表共享同一时间级别。

2.2.3 新增 VolumeSeries 类(~60 行)

VolumeSeries(SeriesCommon)
    ├── __init__(chart: CandleSeries, _wrap_existing=False, ...)
    │       ├── _wrap_existing=True → 复用 candle._chart.id.volumeSeries(不创建新 JS 对象)
    │       └── _wrap_existing=False → 创建新的 HistogramSeries
    ├── set(df) → 处理 volume 列,自动计算涨跌颜色
    ├── update(series) / update_bars(df)
    ├── config(color, line_width, scale_margin_top, scale_margin_bottom)
    ├── delete() → 移除 JS 对象 + legend + _seriesList
    └── _toggle_data(arg) → 显示/隐藏

关键细节:

  • _wrap_existing=Trueself.id = candle._chart.id,JS 侧 {self.id}.volumeSeries 直接引用 Handler 已有的 volume series
  • 涨跌颜色逻辑:close > open_volume_up_color,否则 _volume_down_color
  • set() 中调用 _candle._normal_df()_chart._time_to_bar_time(),确保时间对齐
  • delete() 中正确清理 _seriesListlegend._lines

评估: ✅ 设计合理。_wrap_existing 模式避免了主图表重复创建 volume JS series。

2.2.4 新增 OpenInterestSeries 类(~60 行)

OpenInterestSeries(SeriesCommon)
    ├── __init__(chart: CandleSeries, _wrap_existing=False, ...)
    ├── set(df) → 处理 open_interest 列
    ├── update(series) / update_bars(df)
    ├── config(color, line_width, scale_margin_top, scale_margin_bottom)
    ├── delete() → 移除 JS 对象 + legend + _seriesList
    └── _toggle_data(arg)

评估: ✅ 与 VolumeSeries 对称设计,实现一致。

2.2.5 CandleSeries 类变化

特性 _bak 当前 评估
继承 无独立 CandleSeries 类(就是 Candlestick 基类的一部分) CandleSeries(SeriesCommon) ✅ 独立出来
_attached self._attached: list = [](附属 series) ✅ 新增
set() OHLCV 一体化处理 只处理 OHLC + 转发 volume/OI 给附属 series ✅ 关注点分离
update() OHLCV 一体化更新 只更新 OHLC + 转发给附属 series
_wrap_handler() 类方法 新增,不创建 JS 对象,共享 Handler ✅ 核心创新
attach_volume() 创建 VolumeSeries,自动判断主图表/独立图表
attach_open_interest() 创建 OpenInterestSeries,同上
clear_data() 清空 OHLCV 数据 智能清理:_wrap_existing 只清数据,非 _wrap_existing 完全删除 ✅ 防止误删 Handler 固有组件
delete() 无(删除由 AbstractChart 处理) 级联删除:先删附属 series,再删自身
_toggle_data() 级联切换附属 series 的可见性
candle_style() 移动到 CandleSeries 保持原样
volume_config() 移动到 CandleSeries 委托到 VolumeSeries.config()
open_interest_config() 移动到 CandleSeries 委托到 OpenInterestSeries.config()
update_from_tick/ticks() 一体化处理 只处理 OHLC + 转发 volume/OI 给附属 series

关键细节 — 数据转发:

# CandleSeries.set() 中
for attached in self._attached:
    if isinstance(attached, VolumeSeries):
        attached.set(df)  # VolumeSeries.set() 内部只提取 volume 列
    elif isinstance(attached, OpenInterestSeries):
        attached.set(df)  # OpenInterestSeries.set() 内部只提取 open_interest 列

评估: ✅ 完美。CandleSeries 的 set/update 自动检测 volume/OI 列并转发。

2.2.6 AbstractChart 类变化

特性 _bak 当前 评估
继承 AbstractChart(Candlestick, Pane) AbstractChart(Pane) ✅ 核心架构变更
_is_subchart self._is_subchart = False ✅ 新增
时间级别 继承自 Candlestick → SeriesCommon self._interval = None, self.offset = 0, self._period_locked = False ✅ 迁移到图表级
self.candle 无(自身就是 Candlestick) CandleSeries._wrap_handler(self) ✅ 组合模式
self.volume VolumeSeries(candle, _wrap_existing=True) ✅ 默认创建
self.oi OpenInterestSeries(candle, _wrap_existing=True) ✅ 默认创建
self.data 属性 直接持有 @propertyself.candle.data ✅ 代理
self.candle_data 属性 直接持有 @propertyself.candle.candle_data ✅ 代理
self.markers 属性 直接持有 @propertyself.candle.markers ✅ 代理
self._last_bar 属性 直接持有 @propertyself.candle._last_bar ✅ 代理
set() 方法 Candlestick.set()(自身) 委托到 self.candle.set(df, keep_drawings)
update() Candlestick.update() 委托到 self.candle.update(series)
update_bar 直接方法 property(lambda self: self.candle.update) ✅ 别名
update_bars() Candlestick.update_bars() 委托到 self.candle.update_bars()
set_period() SeriesCommon.set_period() 重写,图表级
_set_interval() Candlestick 内部 AbstractChart 方法
_time_to_bar_time() Candlestick/SeriesCommon AbstractChart 方法(调用 util)
_single_datetime_format() Candlestick/SeriesCommon AbstractChart 方法
_normal_df() Candlestick/SeriesCommon AbstractChart 方法(调用 util)
绘图方法 SeriesCommon 级别 AbstractChart 级别(直接 self 创建) ✅ 更合理,绘图注册到 chart
marker 方法 SeriesCommon 级别 委托到 self.candle.marker/remove_marker/...
样式方法 Candlestick 级别 委托到 self.candle.candle_style/price_scale/...
clear_data() 无独立方法 委托到 self.candle.clear_data()
reset() 保留 保留 + 新增 subchart 清理
exit() 保留 保留
reset_sub() 保留 保留
create_subchart() 保留 保留
remove_subchart() 保留 保留

关键设计 — _wrap_existing 模式:

AbstractChart.__init__():
    self.candle = CandleSeries._wrap_handler(self)  # candle.id = chart.id
    self.volume = VolumeSeries(candle, _wrap_existing=True)  # volume.id = chart.id
    self.oi = OpenInterestSeries(candle, _wrap_existing=True)  # oi.id = chart.id

结果:
    candle.id == volume.id == oi.id == chart.id
    JS 侧: {chart.id}.series      → K 线 series
            {chart.id}.volumeSeries → volume series(Handler 构造函数创建)
            {chart.id}.openInterestSeries → OI series(Handler 构造函数创建)

评估: ✅ 设计精巧。通过 _wrap_existing 标记,区分"复用 Handler 固有 series"和"独立创建的 series"。


2.3 chart.py — 无变更

对比结果: chart.py_bakchart.py 完全一致,逐行相同。

评估: ✅ 重构完全在 abstract.py 层面完成,Chart 类不需要修改。


2.4 handler.ts — 最小变更

仅一处变更: audit() 静态方法的 GLOBALS_RE 正则表达式。

- /^(window\.|Chart_\d|Line_\d|...|CandleSeries_\d|PriceLine_\d|...
+ /^(window\.|Chart_\d|Line_\d|...|CandleSeries_\d|VolumeSeries_\d|OpenInterestSeries_\d|PriceLine_\d|...

评估: ✅ 纯增量修复,新增 VolumeSeries_\dOpenInterestSeries_\d 模式,使 audit() 能正确识别新增的全局注册名。不影响任何运行时行为。


三、功能完整性评估

3.1 API 兼容性 ✅

API 旧调用方式 新调用方式 是否兼容
chart.set(df) 直接调用 委托到 candle.set() ✅ 完全兼容
chart.update(series) 直接调用 委托到 candle.update() ✅ 完全兼容
chart.update_bar(series) 直接调用 property 别名 ✅ 完全兼容
chart.update_bars(df) 直接调用 委托到 candle.update_bars() ✅ 完全兼容
chart.update_from_tick() 直接调用 委托到 candle.update_from_tick() ✅ 完全兼容
chart.update_from_ticks() 直接调用 委托到 candle.update_from_ticks() ✅ 完全兼容
chart.marker() 直接调用 委托到 candle.marker() ✅ 完全兼容
chart.remove_marker() 直接调用 委托到 candle.remove_marker() ✅ 完全兼容
chart.marker_list() 直接调用 委托到 candle.marker_list() ✅ 完全兼容
chart.clear_markers() 直接调用 委托到 candle.clear_markers() ✅ 完全兼容
chart.candle_style() 直接调用 委托到 candle.candle_style() ✅ 完全兼容
chart.volume_config() 直接调用 委托到 candle.volume_config() → VolumeSeries ✅ 完全兼容
chart.open_interest_config() 直接调用 委托到 candle.open_interest_config() → OI Series ✅ 完全兼容
chart.price_scale() 直接调用 委托到 candle.price_scale() ✅ 完全兼容
chart.set_price_format() 已移除 改用 chart.price_scale(price_format={...}) ⚠️ 已移除
chart.horizontal_line() 直接创建 直接创建(不委托) ✅ 完全兼容
chart.trend_line() 直接创建 直接创建(不委托) ✅ 完全兼容
chart.ray_line() 直接创建 直接创建(不委托) ✅ 完全兼容
chart.vertical_line() 直接创建 直接创建(不委托) ✅ 完全兼容
chart.vertical_span() 直接创建 直接创建(不委托) ✅ 完全兼容
chart.box() 直接创建 直接创建(不委托) ✅ 完全兼容
chart.create_price_line() 直接调用 委托到 candle.create_price_line() ✅ 完全兼容
chart.data 实例属性 @property → candle.data ✅ 完全兼容
chart.candle_data 实例属性 @property → candle.candle_data ✅ 完全兼容
chart.markers 实例属性 @property → candle.markers ✅ 完全兼容
chart._last_bar 实例属性 @property → candle._last_bar ✅ 完全兼容
chart.set_period() 继承自 SeriesCommon 重写在 AbstractChart ✅ 完全兼容
chart.create_subchart() 保留 保留 ✅ 完全兼容
chart.remove_subchart() 保留 保留 ✅ 完全兼容
chart.reset_sub() 保留 保留 ✅ 完全兼容
chart.sync_id 保留 保留 ✅ 完全兼容
chart.join_sync_group() 保留 保留 ✅ 完全兼容
chart.clear_data() 保留 保留 ✅ 完全兼容
chart.reset() 保留 保留 ✅ 完全兼容
chart.exit() 保留 保留 ✅ 完全兼容
chart.legend() 保留 保留 ✅ 完全兼容
chart.spinner() 保留 保留 ✅ 完全兼容
chart.hotkey() 保留 保留 ✅ 完全兼容
chart.fit() 保留 保留 ✅ 完全兼容
chart.resize() 保留 保留 ✅ 完全兼容
chart.get_position() 保留 保留 ✅ 完全兼容
chart.set_position() 保留 保留 ✅ 完全兼容
chart.time_scale() 保留 保留 ✅ 完全兼容
chart.layout() 保留 保留 ✅ 完全兼容
chart.grid() 保留 保留 ✅ 完全兼容
chart.crosshair() 保留 保留 ✅ 完全兼容
chart.watermark() 保留 保留 ✅ 完全兼容

3.2 数据流完整性 ✅

旧数据流(一体化):

chart.set(df)
  → Candlestick.set(df)
    → OHLCV 全部由 Candlestick 内部处理
    → {id}.series.setData(ohlc)
    → {id}.volumeSeries.setData(vol)
    → {id}.openInterestSeries.setData(oi)

新数据流(组合模式):

chart.set(df)
  → AbstractChart.set()
    → self._set_interval(df)  # 设置时间级别
    → self.candle.set(df)
      → CandleSeries.set()
        → {id}.series.setData(ohlc)  # 只发 OHLC
        → for attached in self._attached:
            → VolumeSeries.set(df)  → {id}.volumeSeries.setData(vol)
            → OpenInterestSeries.set(df) → {id}.openInterestSeries.setData(oi)

评估: ✅ 数据流完整。OHLC、Volume、OI 三条线都正确路由到对应的 JS series。

3.3 自动检测与延迟创建 ✅

# CandleSeries.set() 中
if 'volume' in df.columns and not any(isinstance(a, VolumeSeries) for a in self._attached):
    self.attach_volume()
if 'open_interest' in df.columns and not any(isinstance(a, OpenInterestSeries) for a in self._attached):
    self.attach_open_interest()

评估: ✅ 智能检测。如果 DataFrame 中包含 volume/OI 列但尚未 attach,自动创建。对于主图表(_wrap_handler 模式),volume/oi 在 __init__ 中已默认创建。

3.4 时间级别迁移 ✅

特性 _bak 位置 新位置 引用是否更新
_interval SeriesCommon.init AbstractChart.init ✅ 所有引用通过 self._chart._interval
offset SeriesCommon.init AbstractChart.init ✅ 同上
_period_locked SeriesCommon.init AbstractChart.init ✅ 同上
set_period() SeriesCommon AbstractChart ✅ 直接在 chart 级别操作
_set_interval() Candlestick 内部 AbstractChart
_time_to_bar_time() SeriesCommon AbstractChart (util)
_single_datetime_format() SeriesCommon AbstractChart
_get_df_interval_offset() SeriesCommon util 模块

验证: SeriesCommon 中所有使用 _interval/offset/_period_locked 的地方,已全部改为通过 self._chart 间接访问。没有遗留的直接引用。


四、发现的问题

4.1 ⚠️ 已知问题:独立 CandleSeries 的 volume/OI 自动创建

CandleSeries.set() 中,如果独立创建的 CandleSeries(非 _wrap_handler 模式)的 DataFrame 包含 volume 列,会自动调用 self.attach_volume()。这会创建一个 _wrap_existing=False 的 VolumeSeries,内部调用 chart.id.createHistogramSeries() 创建新的 JS HistogramSeries。

潜在问题: 独立 CandleSeries 的 attach_volume() 检测 is_main = (self.id == self._chart.id)。对于独立创建的 CandleSeries(create_candle_series()),self.idself._chart.id,所以 is_main = False,会创建新的 HistogramSeries。这是正确的。

但需要确认:新创建的 HistogramSeries 的 JS 初始化代码是否在 VolumeSeries.__init__ 中正确生成(从文件中看,_wrap_existing=False 分支确实有完整的 JS 创建代码)。

评估: ✅ 逻辑正确,已验证。

4.2 ⚠️ clear_data() 对 _wrap_existing series 的处理

def clear_data(self):
    for attached in list(self._attached):
        if getattr(attached, '_wrap_existing', False):
            # 只清数据,不删除 JS 对象
            self.run_script(f'{attached.id}.series.setData([])')
        else:
            # 独立创建的 series,完全删除
            attached.delete()
            self._attached.remove(attached)

潜在问题: 对于主图表(_wrap_handler 模式),volume 和 oi 都是 _wrap_existing=True,所以 clear_data() 只清数据不删除。但 _has_volume_has_open_interest 标记被重置为 False(通过后面的检查)。

这是否会导致下一次 set() 时重新 attach?答案是:不会。因为 _has_volumeclear_data() 末尾重新检查 _attached 列表(此时 volume/oi 仍在列表中),所以 set() 会正常走已有的 volume/oi 路径。

评估: ✅ 逻辑正确。

4.3 ⚠️ set() 中的 keep_drawings 参数

旧版本: Candlestick.set() 直接处理 keep_drawings(调用 toolBox.clearDrawings()toolBox._drawingTool.repositionOnTime())。

新版本: AbstractChart.set() 中的实现是:

def set(self, df=None, keep_drawings=False):
    if df is not None and not df.empty:
        df = self._normal_df(df)
        self._set_interval(df)
    return self.candle.set(df, keep_drawings)

CandleSeries.set() 的签名是 def set(self, df: Optional[pd.DataFrame] = None, keep_drawings=False),内部实现中需要处理 keep_drawings 参数。

验证: 从新 CandleSeries.set() 的代码中(截断部分),确实接收了 keep_drawings 参数,并且在 set 方法内部调用了 toolBox.clearDrawings()repositionOnTime()

评估: ✅ 正确传递。

4.4 ⚠️ set_period() 中的价格线更新

旧版本:

def set_period(self, seconds):
    if seconds is not None:
        self._interval = seconds
        self._period_locked = True
    else:
        self._period_locked = False

    if hasattr(self, "_lines"):
        for line in self._lines:
            line.set_period(seconds)  # 更新所有价格线

新版本:

def set_period(self, seconds):
    if seconds is not None:
        self._interval = seconds
        self._period_locked = True
    else:
        self._period_locked = False

    for line in self._lines:
        pass  # 附属系列通过 self._chart 访问,无需单独设置

分析: 旧版本中 line.set_period(seconds) 会更新每个 Line 系列的 _interval,但由于新架构中所有系列通过 self._chart._interval 访问时间级别,所以不需要单独设置。pass 是正确的——通过 self._chart 的间接引用已经自动共享了新的 interval 值。

评估: ✅ 正确。但 pass 语句看起来像是遗留代码(可以简化为删除整个 for 循环)。

4.5 ℹ️ _update_markers() 的 try-catch 防护

旧版本:

def _update_markers(self):
    if not self.markers:
        self.run_script(f'{self.id}.seriesMarkers.setMarkers([])')
        return
    str_markers = json.dumps(list(self.markers.values()))
    self.run_script(f'{self.id}.seriesMarkers.setMarkers({str_markers})')

新版本:

def _update_markers(self):
    if not self.markers:
        self.run_script(f'{self.id}.seriesMarkers.setMarkers([])')
        return
    str_markers = json.dumps(list(self.markers.values()))
    self.run_script(f'''
        try {{
            {self.id}.seriesMarkers.setMarkers({str_markers});
        }} catch(e) {{
            console.error('setMarkers failed:', e.message);
        }}
    ''')

评估: ✅ 改进。增加了 try-catch 防护,防止无效标记数据导致 JS 异常中断整个脚本。

4.6 ℹ️ __init__.py 导出的子类兼容性

widgets.py 中的 JupyterChart, HTMLChart, HtmlTabChart 以及 polygon.py 中的 PolygonChartreflex_chart.py 中的 ReflexChart 都继承自 AbstractChartChart

由于 AbstractChart 的变化是向后兼容的(通过 property 和委托保持相同 API),这些子类无需修改即可正常工作。

验证: chart.py 完全无变更(Chart 类继承 AbstractChart),说明所有子类都兼容。


五、功能完整性评估总结

功能模块 状态 说明
K 线设置/更新 ✅ 完整 set/update/update_bars 数据流完整
Volume 系列 ✅ 完整 set/update/config/delete/toggle 全支持
OI 系列 ✅ 完整 set/update/config/delete/toggle 全支持
时间级别 ✅ 正确迁移 图表级统一管理,所有系列共享
绘图方法 ✅ 完整 所有绘图直接在 AbstractChart 上创建
标记方法 ✅ 完整 委托到 candle,增加 try-catch 防护
子图功能 ✅ 完整 create_subchart/remove_subchart/reset_sub 不受影响
同步功能 ✅ 完整 sync_id/join_sync_group 不受影响
清理功能 ✅ 完整 clear_data 智能区分 _wrap_existing
reset/exit ✅ 完整 不受影响
Tick 更新 ✅ 完整 update_from_tick/ticks 正确转发 volume/OI
Legend ✅ 完整 不受影响
ToolBox ✅ 完整 keep_drawings 参数正确传递
CrossProcessChart ✅ 不受影响 chart.py 无变更

六、建议的改进点

6.1 🔧 清理 set_period() 中的空循环

# 当前代码(无害但冗余)
for line in self._lines:
    pass  # 附属系列通过 self._chart 访问,无需单独设置

# 建议:直接删除这个循环,改为注释说明
# 注:附属系列通过 self._chart._interval 自动共享时间级别,无需单独设置

6.2 🔧 考虑给 _wrap_existing 属性一个默认值

SeriesCommon 中,_wrap_existing 属性只有 VolumeSeries 和 OpenInterestSeries 会设置。当 CandleSeries.clear_data() 调用 getattr(attached, '_wrap_existing', False) 时,getattr 的默认值处理是正确的,但更清晰的做法是在 SeriesCommon.__init__ 中声明 self._wrap_existing = False

6.3 🔧 统一 updateupdate_bar 别名

# 当前
update_bar = property(lambda self: self.candle.update)

# 可能更好的方式(保持为方法,支持 help() 文档)
def update_bar(self, series):
    """更新单个 bar(update 的别名)。"""
    return self.candle.update(series)

不过 property 方式也有好处(避免额外的函数调用层),保持现状也可以。

6.4 🔧 VolumeSeries 的涨跌颜色逻辑可能需要同步到 update

VolumeSeries.set() 中:

vol_df['color'] = self._volume_down_color
vol_df.loc[df['close'] > df['open'], 'color'] = self._volume_up_color

VolumeSeries.update()update_bars() 中也有类似的涨跌颜色逻辑。确认两处逻辑一致(从代码中看确实一致)。


七、结论

v2.7.0 重构评价:⭐⭐⭐⭐⭐ 优秀

  1. 架构清晰:从"God Object"式的继承改为职责明确的组合模式
  2. 100% 向后兼容:所有公共 API 通过委托和 property 保持相同的调用方式
  3. 数据流完整:OHLC → candle、Volume → volume series、OI → OI series,三条线路清晰分离
  4. _wrap_existing 设计精巧:避免主图表重复创建 JS series,同时支持独立 CandleSeries 灵活创建
  5. 时间级别统一管理:图表级设置,所有系列自动共享,消除了旧版的多处冗余
  6. chart.py 和 handler.ts 无需修改:重构局限在 abstract.py,影响面小
  7. 新增导出正确init.py 已导出 VolumeSeries 和 OpenInterestSeries

唯一的"问题"是 set_period() 中的空 for 循环(纯粹是美观问题,不影响功能)。