fix(web): 自动刷新保留 K 线视窗;威科夫与图表增量更新
自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""增量更新:新K只追加 KLU/KLC,笔与笔中枢在当前列表上重算。
|
||||
|
||||
不改 init_TF_DF 的整段语义。笔必须整表重扫:最后一笔 is_sure 允许收回
|
||||
(OWN_CHAN_ZS_001 上 60 天出现 7 次)。笔中枢用 cal_bi_zs_list_pure。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
from technical.util import resample_to_interval
|
||||
|
||||
from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_KLC_STATE
|
||||
from chanlun.core.ChanKLU import ChanKLU
|
||||
|
||||
|
||||
class IncrementalBuilderMixin:
|
||||
def init_stream(self, df, interval=1, timeframe=None):
|
||||
"""用历史K线初始化流式状态,之后用 append_bar / replace_last_bar。"""
|
||||
if df is None or df.empty:
|
||||
raise ValueError("DataFrame for stream is empty.")
|
||||
if "date" not in df.columns:
|
||||
raise ValueError(f"DataFrame missing 'date' column. Columns: {df.columns.tolist()}")
|
||||
self.timeframe = timeframe
|
||||
self.interval = interval
|
||||
if interval == 1:
|
||||
self.dataframe = df.copy()
|
||||
else:
|
||||
self.dataframe = resample_to_interval(df, interval)
|
||||
self.dataframe = self.add_indicators(self.dataframe)
|
||||
self.klu_list = []
|
||||
self.klc_list = []
|
||||
self.bi_list = []
|
||||
self.bi_zs_list = []
|
||||
self.seg_list = []
|
||||
self.zs_list = []
|
||||
self.bsp_list = []
|
||||
self.klc_fx_list = []
|
||||
self.big_zs_list = []
|
||||
self._klc_feed_last_klu = None
|
||||
for i in range(len(self.dataframe)):
|
||||
self._append_row_at(i, rebuild=False)
|
||||
self.rebuild_bi_zs()
|
||||
return self
|
||||
|
||||
def append_bar(self, row):
|
||||
"""追加一根已收盘K线。同一时间戳则改为替换最后一根。"""
|
||||
self._ensure_stream_state()
|
||||
item = self._normalize_row(row)
|
||||
if self.klu_list and self.klu_list[-1].time == self._row_time_str(item):
|
||||
return self.replace_last_bar(item)
|
||||
self._append_item_to_dataframe(item)
|
||||
self.dataframe = self.add_indicators(self.dataframe)
|
||||
self._append_row_at(len(self.dataframe) - 1, rebuild=True)
|
||||
return self
|
||||
|
||||
def replace_last_bar(self, row):
|
||||
"""更新最后一根K(未完成K线走新OHLC)。包含关系从 KLU 列表重放。"""
|
||||
self._ensure_stream_state()
|
||||
if not self.klu_list:
|
||||
return self.append_bar(row)
|
||||
item = self._normalize_row(row)
|
||||
idx = self.dataframe.index[-1]
|
||||
for key, val in item.items():
|
||||
self.dataframe.at[idx, key] = val
|
||||
self.dataframe = self.add_indicators(self.dataframe)
|
||||
self._apply_item_to_klu(self.klu_list[-1], self.dataframe.iloc[-1])
|
||||
self._rebuild_klc_from_klu()
|
||||
self.rebuild_bi_zs()
|
||||
return self
|
||||
|
||||
def rebuild_bi_zs(self):
|
||||
"""在当前 KLC 上重算笔 + cal_bi_zs_list_pure。会先清分型标记。"""
|
||||
self._reset_klc_bi_marks(self.klc_list)
|
||||
self.bi_list = self.cal_bi_list(self.klc_list) if self.klc_list else []
|
||||
self.bi_zs_list = self.cal_bi_zs_list_pure(self.bi_list) if self.bi_list else []
|
||||
return self.bi_zs_list
|
||||
|
||||
def _ensure_stream_state(self):
|
||||
if not hasattr(self, "klu_list") or self.klu_list is None:
|
||||
self.klu_list = []
|
||||
if not hasattr(self, "klc_list") or self.klc_list is None:
|
||||
self.klc_list = []
|
||||
if not hasattr(self, "dataframe") or self.dataframe is None:
|
||||
self.dataframe = DataFrame(
|
||||
columns=["date", "open", "high", "low", "close", "volume"]
|
||||
)
|
||||
if not hasattr(self, "_klc_feed_last_klu"):
|
||||
self._klc_feed_last_klu = self.klu_list[-1] if self.klu_list else None
|
||||
if not hasattr(self, "bi_zs_list"):
|
||||
self.bi_zs_list = []
|
||||
|
||||
def _rebuild_klc_from_klu(self):
|
||||
self.klc_list = []
|
||||
last_klu = None
|
||||
for klu in self.klu_list:
|
||||
self._push_klu_into_klc_list(self.klc_list, klu, last_klu)
|
||||
last_klu = klu
|
||||
self._klc_feed_last_klu = last_klu
|
||||
|
||||
def _append_row_at(self, idx, rebuild=True):
|
||||
item = self.dataframe.iloc[idx]
|
||||
klu = self._klu_from_item(item, idx)
|
||||
if self.klu_list:
|
||||
self.klu_list[-1].set_next(klu)
|
||||
klu.set_pre(self.klu_list[-1])
|
||||
self._push_klu_into_klc_list(self.klc_list, klu, self._klc_feed_last_klu)
|
||||
self._klc_feed_last_klu = klu
|
||||
self.klu_list.append(klu)
|
||||
if rebuild:
|
||||
self.rebuild_bi_zs()
|
||||
|
||||
def _klu_from_item(self, item, idx):
|
||||
klu = ChanKLU(
|
||||
self._item_time_str(item),
|
||||
item["open"],
|
||||
item["high"],
|
||||
item["low"],
|
||||
item["close"],
|
||||
item["volume"],
|
||||
)
|
||||
klu.set_idx(idx)
|
||||
if not hasattr(klu, "ema13"):
|
||||
klu.ema13 = 0
|
||||
if "macd" in item:
|
||||
klu.set_indicators(item)
|
||||
return klu
|
||||
|
||||
def _apply_item_to_klu(self, klu, item):
|
||||
klu.time = self._item_time_str(item)
|
||||
klu.open = item["open"]
|
||||
klu.high = item["high"]
|
||||
klu.low = item["low"]
|
||||
klu.close = item["close"]
|
||||
klu.volume = item["volume"]
|
||||
klu.range = klu.high - klu.low
|
||||
klu.body = abs(klu.close - klu.open)
|
||||
if "macd" in item:
|
||||
klu.set_indicators(item)
|
||||
|
||||
def _reset_klc_bi_marks(self, klc_list):
|
||||
for klc in klc_list:
|
||||
klc.fx = Chan_FX_TYPE.UNKNOWN
|
||||
klc.klc_fx_type = Chan_KLC_FX.UNKNOWN
|
||||
klc.klc_state = Chan_KLC_STATE.UNKNOWN
|
||||
klc.bi = None
|
||||
klc.fx_confirmed = False
|
||||
|
||||
def _item_time_str(self, item):
|
||||
date = item["date"]
|
||||
if hasattr(date, "to_pydatetime"):
|
||||
date = date.to_pydatetime()
|
||||
if isinstance(date, datetime):
|
||||
return date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return str(date)
|
||||
|
||||
def _row_time_str(self, item):
|
||||
return self._item_time_str(item)
|
||||
|
||||
def _normalize_row(self, row):
|
||||
if isinstance(row, pd.Series):
|
||||
return row
|
||||
return pd.Series(row)
|
||||
|
||||
def _append_item_to_dataframe(self, item):
|
||||
row_df = DataFrame([item])
|
||||
if self.dataframe is None or self.dataframe.empty:
|
||||
self.dataframe = row_df
|
||||
else:
|
||||
self.dataframe = pd.concat([self.dataframe, row_df], ignore_index=True)
|
||||
@@ -86,7 +86,8 @@ class KlineBuilderMixin:
|
||||
return Chan_FX_TYPE.UNKNOWN
|
||||
|
||||
def check_fx(self, klc):
|
||||
if klc.pre and klc.next:
|
||||
# 右K未完成(仍在包含合并)时不分型:否则确认笔会随 next 扩区间被 check_*_fx 收回
|
||||
if klc.pre and klc.next and klc.next.end_klu is not None:
|
||||
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low:
|
||||
#if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
|
||||
klc.set_fx(Chan_FX_TYPE.TOP)
|
||||
@@ -171,6 +172,43 @@ class KlineBuilderMixin:
|
||||
def get_kl_data(self, dataframe:DataFrame):
|
||||
return self.cal_kl_data(dataframe)
|
||||
|
||||
def _push_klu_into_klc_list(self, klc_list, klu, last_klu):
|
||||
"""把一根 KLU 并入包含K线列表。与 get_klc_list 的几何规则相同。"""
|
||||
if len(klc_list) > 0:
|
||||
last_klc = klc_list[-1]
|
||||
if klu.exception:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
if last_klc.high < klu.high:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||
klc.high = klu.close if klu.close > klu.open else klu.open
|
||||
klc.low = klu.open if klu.close > klu.open else klu.close
|
||||
klc_list.append(klc)
|
||||
last_klc.set_next(klc)
|
||||
klc.set_pre(last_klc)
|
||||
last_klc.set_end_klu(last_klu)
|
||||
klc.set_pre_fx()
|
||||
else:
|
||||
included = last_klc.check_klu_included(klu)
|
||||
if not included:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
if last_klc.high < klu.high:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||
klc_list.append(klc)
|
||||
last_klc.set_next(klc)
|
||||
klc.set_pre(last_klc)
|
||||
last_klc.set_end_klu(last_klu)
|
||||
klc.set_pre_fx()
|
||||
else:
|
||||
last_klc.add_klu(klu)
|
||||
else:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
if klu.open > klu.close:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
klc = ChanKLC(klu, 0, ddir)
|
||||
klc_list.append(klc)
|
||||
|
||||
def get_klc_list(self, klu_list):
|
||||
klc_list = []
|
||||
last_klu = None
|
||||
@@ -198,41 +236,7 @@ class KlineBuilderMixin:
|
||||
ema_down_list.append(ema_down_count)
|
||||
#print(last_klu.time, ema_down_count, "DOWN END")
|
||||
ema_down_count = 0
|
||||
if len(klc_list) > 0:
|
||||
last_klc = klc_list[-1]
|
||||
if klu.exception:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
if last_klc.high < klu.high:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||
klc.high = klu.close if klu.close > klu.open else klu.open
|
||||
klc.low = klu.open if klu.close > klu.open else klu.close
|
||||
klc_list.append(klc)
|
||||
last_klc.set_next(klc)
|
||||
klc.set_pre(last_klc)
|
||||
last_klc.set_end_klu(last_klu)
|
||||
klc.set_pre_fx()
|
||||
#print(klu.time, klu.high, klu.low, klu.close, klu.open, klu.exception)
|
||||
else:
|
||||
included = last_klc.check_klu_included(klu)
|
||||
if not included:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
if last_klc.high < klu.high:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
klc = ChanKLC(klu, index=len(klc_list), ddir=ddir)
|
||||
klc_list.append(klc)
|
||||
last_klc.set_next(klc)
|
||||
klc.set_pre(last_klc)
|
||||
last_klc.set_end_klu(last_klu)
|
||||
klc.set_pre_fx()
|
||||
else:
|
||||
last_klc.add_klu(klu)
|
||||
else:
|
||||
ddir = Chan_KLINE_DIR.UP
|
||||
if klu.open > klu.close:
|
||||
ddir = Chan_KLINE_DIR.DOWN
|
||||
klc = ChanKLC(klu, 0, ddir)
|
||||
klc_list.append(klc)
|
||||
self._push_klu_into_klc_list(klc_list, klu, last_klu)
|
||||
last_klu = klu
|
||||
klc_list = self.cal_trend(klc_list)
|
||||
#print(ema52_up_list, ema52_down_list)
|
||||
|
||||
@@ -355,9 +355,12 @@ class ZsBuilderMixin:
|
||||
return bi_zs_list
|
||||
|
||||
def get_zs_range(bis):
|
||||
zg = min(bi.high for bi in bis)
|
||||
zd = max(bi.low for bi in bis)
|
||||
return zg, zd
|
||||
bis_list = bis[0:3]
|
||||
zg = min(bi.high for bi in bis_list)
|
||||
zd = max(bi.low for bi in bis_list)
|
||||
dd = min(bi.low for bi in bis_list)
|
||||
gg = max(bi.high for bi in bis_list)
|
||||
return zg, zd, dd, gg
|
||||
|
||||
def is_bi_overlap_range(bi, zg, zd):
|
||||
return bi.high >= zd and bi.low <= zg
|
||||
@@ -375,8 +378,8 @@ class ZsBuilderMixin:
|
||||
zs.bi_list = list(bis)
|
||||
for bi in zs.bi_list:
|
||||
bi.set_bi_zs(zs)
|
||||
zs.set_gg(max(bi.high for bi in zs.bi_list))
|
||||
zs.set_dd(min(bi.low for bi in zs.bi_list))
|
||||
#zs.set_gg(max(bi.high for bi in zs.bi_list))
|
||||
#zs.set_dd(min(bi.low for bi in zs.bi_list))
|
||||
zs.classify_zs()
|
||||
|
||||
last_zs = None
|
||||
@@ -394,7 +397,7 @@ class ZsBuilderMixin:
|
||||
start_idx += 1
|
||||
continue
|
||||
|
||||
zg, zd = get_zs_range([bi1, bi2, bi3])
|
||||
zg, zd, dd, gg = get_zs_range([bi1, bi2, bi3])
|
||||
if zg <= zd:
|
||||
start_idx += 1
|
||||
continue
|
||||
@@ -420,6 +423,8 @@ class ZsBuilderMixin:
|
||||
zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir)
|
||||
zs.set_zg(zg)
|
||||
zs.set_zd(zd)
|
||||
zs.set_dd(dd)
|
||||
zs.set_gg(gg)
|
||||
|
||||
set_zs_bi_list(zs, bis_for_zs)
|
||||
zs.set_end_bi(bis_for_zs[-1], bis_for_zs[-1].sure_time)
|
||||
|
||||
@@ -178,6 +178,15 @@ class ChanLun():
|
||||
def cal_bi_zs_list(self, bi_list):
|
||||
#return self.tf_df.cal_bi_zs(bi_list)
|
||||
return self.tf_df.cal_bi_zs_list(bi_list)
|
||||
def cal_bi_zs_list_pure(self, bi_list):
|
||||
return self.tf_df.cal_bi_zs_list_pure(bi_list)
|
||||
def init_stream(self, dataframe, interval=1, timeframe=None):
|
||||
self.tf_df.init_stream(dataframe, interval, timeframe)
|
||||
return self.tf_df
|
||||
def append_bar(self, row):
|
||||
return self.tf_df.append_bar(row)
|
||||
def replace_last_bar(self, row):
|
||||
return self.tf_df.replace_last_bar(row)
|
||||
def get_bi_zs_list(self, bi_list):
|
||||
return self.tf_df.get_bi_zs_list(bi_list)
|
||||
def get_decimal(self, value):
|
||||
|
||||
@@ -31,12 +31,13 @@ from chanlun.core.ChanZS import ChanZS, ChanZS_Big
|
||||
from chanlun.indicators.ChanMACD import ChanMACD
|
||||
from chanlun.pipeline.builders.bi import BiBuilderMixin
|
||||
from chanlun.pipeline.builders.bsp import BspBuilderMixin
|
||||
from chanlun.pipeline.builders.incremental import IncrementalBuilderMixin
|
||||
from chanlun.pipeline.builders.indicators import IndicatorsBuilderMixin
|
||||
from chanlun.pipeline.builders.kline import KlineBuilderMixin
|
||||
from chanlun.pipeline.builders.seg import SegBuilderMixin
|
||||
from chanlun.pipeline.builders.zs import ZsBuilderMixin
|
||||
|
||||
class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilderMixin, ZsBuilderMixin, BspBuilderMixin):
|
||||
class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilderMixin, ZsBuilderMixin, BspBuilderMixin, IncrementalBuilderMixin):
|
||||
def __init__(self, df=None, interval=0, timeframe=None):
|
||||
if df is not None:
|
||||
self.init_TF_DF(df, interval, timeframe)
|
||||
@@ -59,12 +60,14 @@ class TF_DF(IndicatorsBuilderMixin, KlineBuilderMixin, BiBuilderMixin, SegBuilde
|
||||
self.klc_list = []
|
||||
self.bi_list = []
|
||||
self.zs_list = []
|
||||
self.bi_zs_list = []
|
||||
self.bsp_list = []
|
||||
self.seg_list = []
|
||||
self.klc_fx_list = []
|
||||
self.klu_list = self.cal_kl_data(self.dataframe)
|
||||
self.klc_list = self.get_klc_list(self.klu_list)
|
||||
self.bi_list = self.cal_bi_list(self.klc_list)
|
||||
self.bi_zs_list = self.cal_bi_zs_list_pure(self.bi_list)
|
||||
self.seg_list = self.get_seg_list(self.bi_list)
|
||||
self.zs_list = self.get_zs_list(self.bi_list, self.seg_list)
|
||||
self.big_zs_list = self.get_big_zs_list(self.zs_list)
|
||||
|
||||
Reference in New Issue
Block a user