添加裸K形态,前端添加KLC显示

This commit is contained in:
jackyu66git
2025-10-29 01:16:50 +08:00
parent abe663e1a6
commit fe4193b153
8 changed files with 589 additions and 114 deletions
+74
View File
@@ -51,6 +51,80 @@ class Chan_KLU_TYPE(Enum):
SmallBear = auto()
Cross = auto()
class Chan_KLU_PATTERN(Enum):
# 单根K线形态
HAMMER = auto() # 锤子线
INVERTED_HAMMER = auto() # 倒锤子线
SHOOTING_STAR = auto() # 射击之星
HANGING_MAN = auto() # 上吊线
DOJI = auto() # 十字星
LONG_LEGGED_DOJI = auto() # 长腿十字星
GRAVESTONE_DOJI = auto() # 墓碑十字星
DRAGONFLY_DOJI = auto() # 蜻蜓十字星
MARUBOZU = auto() # 光头光脚
SPINNING_TOP = auto() # 纺锤线
# 双根K线形态
BULLISH_ENGULFING = auto() # 看涨吞没
BEARISH_ENGULFING = auto() # 看跌吞没
PIERCING_LINE = auto() # 刺透形态
DARK_CLOUD_COVER = auto() # 乌云盖顶
TWEEZER_TOP = auto() # 镊子顶
TWEEZER_BOTTOM = auto() # 镊子底
HARAMI = auto() # 孕线
BULLISH_HARAMI = auto() # 看涨孕线
BEARISH_HARAMI = auto() # 看跌孕线
# 三根K线形态
MORNING_STAR = auto() # 早晨之星
EVENING_STAR = auto() # 黄昏之星
THREE_WHITE_SOLDIERS = auto() # 红三兵
THREE_BLACK_CROWS = auto() # 三只乌鸦
THREE_INNER_UP = auto() # 上升三法
THREE_INNER_DOWN = auto() # 下降三法
ABANDONED_BABY = auto() # 弃婴形态
# 多根K线形态
DOUBLE_TOP = auto() # 双顶
DOUBLE_BOTTOM = auto() # 双底
TRIPLE_TOP = auto() # 三顶
TRIPLE_BOTTOM = auto() # 三底
HEAD_AND_SHOULDERS = auto() # 头肩顶
INVERSE_HEAD_SHOULDERS = auto() # 头肩底
ROUNDING_BOTTOM = auto() # 圆弧底
ROUNDING_TOP = auto() # 圆弧顶
# 缺口形态
BREAKAWAY_GAP = auto() # 突破缺口
RUNAWAY_GAP = auto() # 持续缺口
EXHAUSTION_GAP = auto() # 衰竭缺口
# 特殊形态
ISLAND_REVERSAL = auto() # 岛形反转
KEY_REVERSAL = auto() # 关键反转
INSIDE_BAR = auto() # 内包线
OUTSIDE_BAR = auto() # 外包线
# 趋势形态
HIGHER_HIGH = auto() # 更高高点
HIGHER_LOW = auto() # 更高低点
LOWER_HIGH = auto() # 更低高点
LOWER_LOW = auto() # 更低低点
# 支撑阻力形态
SUPPORT_BOUNCE = auto() # 支撑反弹
RESISTANCE_REJECTION = auto() # 阻力拒绝
BREAKOUT = auto() # 突破
BREAKDOWN = auto() # 跌破
# 成交量相关形态
VOLUME_SPIKE = auto() # 成交量激增
VOLUME_DECLINE = auto() # 成交量萎缩
# 未知/无形态
UNKNOWN = auto() # 未知形态
class Chan_FX_TYPE(Enum):
BOTTOM = auto()
TOP = auto()
+6 -2
View File
@@ -1,4 +1,4 @@
from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND
from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN
class ChanKLU:
def __init__(self, time, open, high, low, close, volume):
# _time, _close, _open, _high, _low, _extra_info={}
@@ -21,7 +21,8 @@ class ChanKLU:
self.bb52lower = 0
# === 新增:K线类型 ===
self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线
self.pattern = Chan_KLU_PATTERN.UNKNOWN
# === 新增:实时分型相关属性 ===
self.pre = None # 前一根K线
self.next = None # 后一根K线
@@ -67,10 +68,13 @@ class ChanKLU:
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
def set_macd_state(self, state):
self.macd_state = state
def set_pattern(self, pattern):
self.pattern = pattern
def cal_exception(self):
if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5:
self.exception = True
#print(self.time, self.upper_shadow_ratio, self.lower_shadow_ratio, self.body, self.lower_shadow, self.upper_shadow, self.high, self.low, self.close, self.open)
self.exception = False
def set_trend(self, trend):
self.trend = trend
def set_next(self, next):
+69 -7
View File
@@ -1,6 +1,6 @@
from datetime import timedelta
from pandas import DataFrame
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN
from ChanKLU import ChanKLU
from ChanKLC import ChanKLC
from ChanBI import ChanBI
@@ -576,7 +576,7 @@ class ChanLun():
if near_resistance_touch:
# 若动量不强,则更偏空
score -= 1 if (hist is None or pre_hist is None or hist <= pre_hist) else 0
# 3.4) 多次对 EMA52 的拒绝配合 MACD 逆向:易形成压/支并反向
# 3.4) 多次对 EMA52 的"拒绝"配合 MACD 逆向:易形成压/支并反向
# 统计近窗口内的上/下拒绝次数:
# - 上拒绝:价格位于 EMA52 下方,最高触及/越过 EMA52 但收盘仍在下方
# - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方
@@ -701,7 +701,7 @@ class ChanLun():
for i in range(1, len(hist_seq)):
if abs(hist_seq[i]) < abs(hist_seq[i-1]):
weaken_steps += 1
# 近窗口对 EMA52 的未能站上/跌破统计(放宽窗口与条件)
# 近窗口对 EMA52 的"未能站上/跌破"统计(放宽窗口与条件)
window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else []
no_up_break = False
no_down_break = False
@@ -1343,8 +1343,70 @@ class ChanLun():
return klc_list
def get_klu_list(self, dataframe):
return self.get_kl_data(dataframe)
def get_decimal(self, value):
return Decimal("{:.2f}".format(value))
klu_list = self.get_kl_data(dataframe)
return self.cal_klu_pattern(klu_list)
def cal_klu_pattern(self, klu_list):
"""
计算裸K的pattern - 只识别反转形态
"""
if not klu_list or len(klu_list) < 3:
return klu_list
for i, klu in enumerate(klu_list):
# 单根K线反转模式识别
self._detect_single_reversal_pattern(klu)
# 验证形态是否成立
if klu.pattern != Chan_KLU_PATTERN.UNKNOWN:
print(klu.time, klu.pattern)
return klu_list
def _detect_single_reversal_pattern(self, klu):
"""检测单根K线反转模式"""
body = abs(klu.close - klu.open)
upper_shadow = klu.high - max(klu.close, klu.open)
lower_shadow = min(klu.close, klu.open) - klu.low
total_range = klu.high - klu.low
# 避免除零
if total_range == 0:
return
body_ratio = body / total_range
upper_ratio = upper_shadow / total_range
lower_ratio = lower_shadow / total_range
# 锤子线/上吊线 - 反转信号
if lower_ratio / body_ratio >= 2:
# 锤子线:底部反转,需要前一根是下跌趋势
if klu.close > klu.open and klu.pre and klu.pre.close < klu.pre.open:
klu.set_pattern(Chan_KLU_PATTERN.HAMMER) # 底部反转
# 上吊线:顶部反转,需要前一根是上涨趋势
elif klu.close < klu.open and klu.pre and klu.pre.close > klu.pre.open:
klu.set_pattern(Chan_KLU_PATTERN.HANGING_MAN) # 顶部反转
# 倒锤子线/射击之星 - 反转信号
elif upper_ratio / body_ratio >= 2:
# 倒锤子线:底部反转,需要前一根是下跌趋势
if klu.close > klu.open and klu.pre and klu.pre.close < klu.pre.open:
klu.set_pattern(Chan_KLU_PATTERN.INVERTED_HAMMER) # 底部反转
# 射击之星:顶部反转,需要前一根是上涨趋势
elif klu.close < klu.open and klu.pre and klu.pre.close > klu.pre.open:
klu.set_pattern(Chan_KLU_PATTERN.SHOOTING_STAR) # 顶部反转
# 十字星 - 反转信号
elif body_ratio <= 0.1:
if upper_ratio > 0.4 and lower_ratio > 0.4:
klu.set_pattern(Chan_KLU_PATTERN.LONG_LEGGED_DOJI) # 强烈反转信号
elif upper_ratio > 0.4 and lower_ratio <= 0.1:
# 墓碑十字星:顶部反转,需要前一根是上涨趋势
if klu.pre and klu.pre.close > klu.pre.open:
klu.set_pattern(Chan_KLU_PATTERN.GRAVESTONE_DOJI) # 顶部反转
elif lower_ratio > 0.4 and upper_ratio <= 0.1:
# 蜻蜓十字星:底部反转,需要前一根是下跌趋势
if klu.pre and klu.pre.close < klu.pre.open:
klu.set_pattern(Chan_KLU_PATTERN.DRAGONFLY_DOJI) # 底部反转
else:
klu.set_pattern(Chan_KLU_PATTERN.DOJI) # 一般反转信号
+16 -11
View File
@@ -112,10 +112,7 @@ class ChanSEG():
last_zs = zs
else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure:
if bi.low < last_zs.zg:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
else:
if bi.low > last_zs.zg or bi.high < last_zs.zd:
last_zs.set_end_bi(last_zs.bi_list[-1], bi)
if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN:
zg = min(bi.high, bi.next.high, bi.next.next.high)
@@ -131,6 +128,12 @@ class ChanSEG():
zs.add_bi(bi.next.next)
zs_list.append(zs)
last_zs = zs
else:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure:
#print(bi.start_time, "BI", last_zs.is_sure)
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1])
else:
for index in range(1, len(self.bi_list)):
bi = self.bi_list[index]
@@ -151,10 +154,7 @@ class ChanSEG():
last_zs = zs
else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure:
if bi.high > last_zs.zd:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
else:
if bi.low > last_zs.zg or bi.high < last_zs.zd:
last_zs.set_end_bi(last_zs.bi_list[-1], bi)
if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP:
zg = min(bi.high, bi.next.high, bi.next.next.high)
@@ -170,8 +170,13 @@ class ChanSEG():
zs.add_bi(bi.next.next)
zs_list.append(zs)
last_zs = zs
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure:
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1])
else:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure:
#print(bi.start_time, "BI", last_zs.is_sure)
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1])
#print(self.start_time, len(zs_list))
print(self.bi_list[-1].end_time, "end_bi")
#print(self.bi_list[-1].end_time, "end_bi")
return zs_list
+35
View File
@@ -0,0 +1,35 @@
{
"strategy_name": "PatternTrader",
"params": {
"trailing": {
"trailing_stop": false,
"trailing_stop_positive": null,
"trailing_stop_positive_offset": 0.0,
"trailing_only_offset_is_reached": false
},
"max_open_trades": {
"max_open_trades": 1
},
"buy": {
"fast_ma": 9,
"lev": 2.9,
"slow_ma": 31,
"time": 29
},
"sell": {
"exit_delay": 6
},
"protection": {},
"roi": {
"0": 0.062,
"6": 0.019,
"16": 0.014,
"40": 0
},
"stoploss": {
"stoploss": -0.316
}
},
"ft_stratparam_v": 1,
"export_time": "2025-10-28 07:39:54.768566+00:00"
}
+233
View File
@@ -0,0 +1,233 @@
# --- Do not remove these libs ---
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, CategoricalParameter
from typing import Dict, List
from functools import reduce
from pandas import DataFrame
import numpy as np
import pandas as pd
# --------------------------------
# 设置pandas选项以避免FutureWarning
pd.set_option('future.no_silent_downcasting', True)
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from technical.util import resample_to_interval, resampled_merge
from freqtrade.persistence import Trade, Order
from datetime import datetime, timedelta
from typing import Optional
import logging
logger = logging.getLogger(__name__)
# freqtrade plot-dataframe --strategy PatternTrader --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309-
# freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies
# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies --timerange=20251023-
# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m --pairs BTC/USDT:USDT --timerange=20250501-
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss --strategy PatternTrader --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 200 --timerange=20250201-20250401
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces buy sell roi stoploss --strategy PatternTrader --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 600 --timerange=20250201-20250401
# sudo docker compose run --rm chan_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies --timerange=20250101-
# sudo docker compose run --rm chan_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101-
# sudo docker compose run --rm chan_btc trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies
class PatternTrader(IStrategy):
"""
极简双均线策略
只使用双均线交叉作为唯一信号
"""
INTERFACE_VERSION: int = 3
# 极简参数
fast_ma: IntParameter = IntParameter(5, 15, default=8, space='buy') # 快速均线
slow_ma: IntParameter = IntParameter(20, 50, default=30, space='buy') # 慢速均线
# 添加一个简单的sell空间参数
exit_delay: IntParameter = IntParameter(1, 10, default=3, space='sell') # 出场延迟
# 时间框架
time: IntParameter = IntParameter(15, 60, default=30, space='buy')
# ROI 超参
roi_t1: IntParameter = IntParameter(10, 60, default=30, space='roi')
roi_t2: IntParameter = IntParameter(60, 240, default=120, space='roi')
roi_p1: DecimalParameter = DecimalParameter(0.02, 0.08, default=0.05, decimals=3, space='roi')
roi_p2: DecimalParameter = DecimalParameter(0.005, 0.03, default=0.01, decimals=3, space='roi')
# 合约交易参数
can_short = True
stoploss = -0.02 # 2% 止损
# 杠杆设置
lev: DecimalParameter = DecimalParameter(1.0, 3.0, default=2.0, decimals=1, space='buy')
# 运行设置
process_only_new_candles = False
startup_candle_count: int = 100
# ROI 外部覆盖
_roi_override: Optional[Dict[str, float]] = None
@property
def minimal_roi(self) -> Dict[str, float]:
"""
基于超参动态生成 ROI 梯度
"""
if self._roi_override is not None:
return self._roi_override
t1 = int(self.roi_t1.value)
t2 = int(self.roi_t2.value)
times = sorted([t1, t2])
p1 = float(self.roi_p1.value)
p2 = float(self.roi_p2.value)
profits = sorted([p1, p2], reverse=True)
return {
"0": profits[0],
str(times[0]): profits[1],
str(times[1]): 0.0,
}
@minimal_roi.setter
def minimal_roi(self, value: Dict[str, float]) -> None:
# 允许框架在解析时覆盖 ROI 设置
self._roi_override = value
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
计算技术指标极简版
只计算双均线
"""
res = self.get_ticker_indicator() * int(self.time.value)
dataframe_3 = resample_to_interval(dataframe, res)
# 只计算双均线
dataframe_3['fast_ma'] = ta.SMA(dataframe_3['close'], timeperiod=int(self.fast_ma.value))
dataframe_3['slow_ma'] = ta.SMA(dataframe_3['close'], timeperiod=int(self.slow_ma.value))
# 计算金叉和死叉
dataframe_3['fast_ma_cross_slow_ma'] = (dataframe_3['fast_ma'] > dataframe_3['slow_ma']) & (dataframe_3['fast_ma'].shift(1) <= dataframe_3['slow_ma'].shift(1))
dataframe_3['fast_ma_cross_slow_ma_down'] = (dataframe_3['fast_ma'] < dataframe_3['slow_ma']) & (dataframe_3['fast_ma'].shift(1) >= dataframe_3['slow_ma'].shift(1))
dataframe = resampled_merge(dataframe, dataframe_3)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
基于TA指标填充进场趋势列极简版
只使用双均线交叉
"""
res = self.get_ticker_indicator() * int(self.time.value)
def _pick(df: DataFrame, name: str) -> str:
col = f"resample_{res}_{name}"
if col in df.columns:
return col
col2 = f"resample_{float(res)}_{name}"
if col2 in df.columns:
return col2
cand = [c for c in df.columns if c.endswith(f"_{name}")]
return cand[0] if len(cand) else col
fast_ma_cross_slow_ma_str = _pick(dataframe, 'fast_ma_cross_slow_ma')
fast_ma_cross_slow_ma_down_str = _pick(dataframe, 'fast_ma_cross_slow_ma_down')
# 检测多头信号:快线上穿慢线
dataframe.loc[
(
(dataframe[fast_ma_cross_slow_ma_str] == True) &
(pd.notna(dataframe[fast_ma_cross_slow_ma_str]))
),
['enter_long', 'enter_tag']] = (1, 'long_signal_simple')
# 检测空头信号:快线下穿慢线
dataframe.loc[
(
(dataframe[fast_ma_cross_slow_ma_down_str] == True) &
(pd.notna(dataframe[fast_ma_cross_slow_ma_down_str]))
),
['enter_short', 'enter_tag']] = (1, 'short_signal_simple')
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
基于TA指标填充出场趋势列极简版
反向交叉出场
"""
res = self.get_ticker_indicator() * int(self.time.value)
def _pick(df: DataFrame, name: str) -> str:
col = f"resample_{res}_{name}"
if col in df.columns:
return col
col2 = f"resample_{float(res)}_{name}"
if col2 in df.columns:
return col2
cand = [c for c in df.columns if c.endswith(f"_{name}")]
return cand[0] if len(cand) else col
fast_ma_cross_slow_ma_str = _pick(dataframe, 'fast_ma_cross_slow_ma')
fast_ma_cross_slow_ma_down_str = _pick(dataframe, 'fast_ma_cross_slow_ma_down')
# 做多出场:出现死叉
dataframe.loc[
(
(dataframe[fast_ma_cross_slow_ma_down_str] == True) &
(pd.notna(dataframe[fast_ma_cross_slow_ma_down_str]))
),
['exit_long', 'exit_tag']] = (1, 'long_exit_simple')
# 做空出场:出现金叉
dataframe.loc[
(
(dataframe[fast_ma_cross_slow_ma_str] == True) &
(pd.notna(dataframe[fast_ma_cross_slow_ma_str]))
),
['exit_short', 'exit_tag']] = (1, 'short_exit_simple')
return dataframe
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
"""
杠杆设置
"""
return float(self.lev.value)
def get_ticker_indicator(self):
return int(self.timeframe[:-1])
# 简单的测试函数
def test_strategy():
"""
测试策略基本功能
"""
try:
# 创建策略实例
strategy = PatternTrader()
# 检查基本属性
print("✅ 策略实例化成功")
print(f"策略名称: {strategy.__class__.__name__}")
print(f"接口版本: {strategy.INTERFACE_VERSION}")
print(f"支持做空: {strategy.can_short}")
print(f"默认止损: {strategy.stoploss}")
# 检查参数
print("\n✅ 策略参数检查:")
print(f"布林带长度: {strategy.bb_length.value}")
print(f"杠杆: {strategy.lev.value}")
print(f"仓位比例: {strategy.position_size_pct.value}")
print("\n🎉 策略测试通过!")
return True
except Exception as e:
print(f"❌ 策略测试失败: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
test_strategy()
+26
View File
@@ -1167,6 +1167,18 @@ def analyze():
# 添加主周期分析结果到返回数据
result.update({
'kline_data': clean_dataframe_for_json(df).to_dict('records'),
'klc_list': [{
'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(),
'open': float(klc.open),
'high': float(klc.high),
'low': float(klc.low),
'close': float(klc.close),
'volume': float(klc.volume) if hasattr(klc, 'volume') else 0,
'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''),
'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''),
'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''),
'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '')
} for klc in analysis_result['klc_list'] if hasattr(klc, 'end_time') and klc.end_time],
'bi_list': [{
'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None,
@@ -1354,6 +1366,20 @@ def analyze():
# 添加小周期K线数据
result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records')
# 添加小周期KLC列表
result['element_klc_list'] = [{
'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(),
'open': float(klc.open),
'high': float(klc.high),
'low': float(klc.low),
'close': float(klc.close),
'volume': float(klc.volume) if hasattr(klc, 'volume') else 0,
'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''),
'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''),
'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''),
'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '')
} for klc in element_analysis['klc_list'] if hasattr(klc, 'end_time') and klc.end_time]
result['element_seg_list'] = [{
'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
+130 -94
View File
@@ -648,7 +648,6 @@
align-items: center;
z-index: 1000;
}
.bb-config-content {
background: white;
border-radius: 8px;
@@ -817,6 +816,7 @@
<option value="baseline">基线</option>
<option value="renko">砖型</option>
<option value="heikin">平均K</option>
<option value="klc">KLC</option>
</select>
<!-- 为兼容现有逻辑,保留隐藏的原始K线开关,与下拉同步 -->
<input class="form-check-input" type="checkbox" id="showOriginalKline" checked style="display:none;">
@@ -2548,6 +2548,19 @@
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
series.setData(baseData);
tvWidget.series.baselineSeries = series;
} else if (klineType === 'klc') {
// KLC显示模式 - 使用蜡烛线显示KLC数据
const series = mainChart.addCandlestickSeries({
upColor: '#dc3545',
downColor: '#28a745',
borderVisible: false,
wickUpColor: '#dc3545',
wickDownColor: '#28a745',
});
// 使用KLC数据创建蜡烛图
const klcCandles = buildKLCFromAnalysis(currentData);
series.setData(klcCandles);
tvWidget.series.klcSeries = series;
}
})();
@@ -2717,7 +2730,6 @@
tvWidget.series.signalLineSeries = signalLineSeries;
tvWidget.series.histogramSeries = histogramSeries;
}
// 添加ChanMACD图表
console.log('ChanMACD图表创建条件检查:', {
showMacd: showMacd,
@@ -2733,7 +2745,6 @@
if (typeof window.showUOnElement === 'undefined') {
window.showUOnElement = $('#toggleUOnElement').is(':checked');
}
if (showMacd && chanMacdChart && ((useElementPeriod && currentData.element_macd) || currentData.macd) && (useElementPeriod ? currentData.element_kline_data : currentData.kline_data)) {
console.log('✅ 开始创建 ChanMACD 系列');
// 创建ChanMACD线系列
@@ -4338,7 +4349,6 @@
});
}
}
// 主周期未完成中枢
if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
@@ -5305,7 +5315,6 @@
// 收集所有主周期分型标记
const allMainFxMarkers = [];
const mainFxMarkers = []; // 用于tooltip支持
// 处理主周期KLC分型
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`);
@@ -5671,6 +5680,7 @@
else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries;
else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries;
else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries;
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
if (targetSeries) {
targetSeries.setMarkers(combinedMarkers);
} else {
@@ -5765,6 +5775,7 @@
else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries;
else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries;
else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries;
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
if (targetSeries2) {
targetSeries2.setMarkers(onlyMainAndU);
} else {
@@ -5782,6 +5793,7 @@
else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries;
else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries;
else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries;
else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries;
if (targetSeries3) {
targetSeries3.setMarkers([]);
}
@@ -6024,6 +6036,9 @@
} else if (klineType === 'baseline' && tvWidget.series.baselineSeries) {
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
tvWidget.series.baselineSeries.setData(baseData);
} else if (klineType === 'klc' && tvWidget.series.klcSeries) {
const klcCandles = buildKLCFromAnalysis(currentData);
tvWidget.series.klcSeries.setData(klcCandles);
}
// 更新均线数据
@@ -7010,7 +7025,6 @@
// 更新数据源信息
setupDataSourceInfo(data);
}
// 设置数据源信息显示
function setupDataSourceInfo(data) {
// 获取用户当前的周期选择
@@ -7103,7 +7117,6 @@
$('#end_time').val(formatDatetimeLocal(now));
$('#start_time').val(formatDatetimeLocal(oneDayAgo));
}
// 格式化日期为datetime-local输入框格式
function formatDatetimeLocal(date) {
const year = date.getFullYear();
@@ -7753,7 +7766,6 @@
console.error('加载A股股票列表失败');
});
}
// 检测交易对类型并返回相应的配置
function getSymbolConfig(symbol) {
const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol);
@@ -7811,7 +7823,6 @@
return chartOptions;
}
// 过滤非交易时间的数据(仅用于显示优化)
function filterTradingHours(data, symbolConfig) {
if (symbolConfig.type !== 'a_stock') {
@@ -8402,7 +8413,6 @@
function updateMAPanel() {
updateIndicatorPanel();
}
// 切换均线可见性
function toggleMAVisibility(maId) {
console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId);
@@ -8605,7 +8615,6 @@
return candles;
}
// 从蜡烛数据生成 Heikin-Ashi(平均K
function buildHeikinFromCandles(candles) {
if (!Array.isArray(candles) || candles.length === 0) return [];
@@ -8627,7 +8636,45 @@
}
return result;
}
// 从分析数据生成KLC蜡烛数据
function buildKLCFromAnalysis(data) {
if (!data) return [];
// 检查是否使用小周期数据
const useElementPeriod = $('#elementPeriodKline').is(':checked') &&
data.element_klc_list &&
Array.isArray(data.element_klc_list);
const klcList = useElementPeriod ? data.element_klc_list : data.klc_list;
if (!klcList) return [];
const klcCandles = [];
// 遍历KLC列表,转换为蜡烛数据格式
klcList.forEach(klc => {
if (!klc || !klc.date) return;
// 使用KLC的date字段,转换为时间戳格式
const date = new Date(klc.date);
const timestamp = date.getTime() / 1000;
// 创建KLC蜡烛数据
const candle = {
time: timestamp,
open: klc.open || 0,
high: klc.high || 0,
low: klc.low || 0,
close: klc.close || 0
};
klcCandles.push(candle);
});
return klcCandles;
}
// 计算默认砖大小(优先使用ATR的最新非零值,否则按收盘价的0.5%)
function computeDefaultBrickSize(candles) {
try {
@@ -8881,10 +8928,10 @@
console.log('🔄 按钮已切换为更新模式');
$('#bbConfigModal').css('display', 'flex');
// 更新预览
setTimeout(updateBBLinePreview, 50);
$('#bbConfigModal').css('display', 'flex');
// 更新预览
setTimeout(updateBBLinePreview, 50);
}
// 更新布林带
function updateBollingerBand(bbId) {
@@ -9008,78 +9055,78 @@
console.warn('addBollingerBandsToChart 未就绪');
}
// 更新均线预览
// 更新均线预览
function updateLinePreview() {
// 检查是否在均线配置窗口
if ($('#maConfigModal').is(':visible')) {
const color = $('#maColor').val();
const width = $('#maLineWidth').val();
const style = $('#maLineStyle').val();
// 检查是否在均线配置窗口
if ($('#maConfigModal').is(':visible')) {
const color = $('#maColor').val();
const width = $('#maLineWidth').val();
const style = $('#maLineStyle').val();
const line = $('#previewLine');
line.attr('stroke', color);
line.attr('stroke-width', width);
// 设置线条样式
switch(parseInt(style)) {
case 0: // 实线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
}
}
// 更新布林带预览
function updateBBLinePreview() {
const upperColor = $('#bbUpperColor').val();
const middleColor = $('#bbMiddleColor').val();
const lowerColor = $('#bbLowerColor').val();
const width = $('#bbLineWidth').val();
const style = $('#bbLineStyle').val();
const line = $('#previewLine');
line.attr('stroke', color);
line.attr('stroke-width', width);
const upperLine = $('#bbPreviewUpper');
const middleLine = $('#bbPreviewMiddle');
const lowerLine = $('#bbPreviewLower');
// 设置线条样式
switch(parseInt(style)) {
case 0: // 实线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
}
}
// 更新布林带预览
function updateBBLinePreview() {
const upperColor = $('#bbUpperColor').val();
const middleColor = $('#bbMiddleColor').val();
const lowerColor = $('#bbLowerColor').val();
const width = $('#bbLineWidth').val();
const style = $('#bbLineStyle').val();
const upperLine = $('#bbPreviewUpper');
const middleLine = $('#bbPreviewMiddle');
const lowerLine = $('#bbPreviewLower');
// 设置各条线的颜色
upperLine.attr('stroke', upperColor);
middleLine.attr('stroke', middleColor);
lowerLine.attr('stroke', lowerColor);
// 设置线条宽度和样式
[upperLine, middleLine, lowerLine].forEach(line => {
line.attr('stroke-width', width);
// 设置线条样式
switch(parseInt(style)) {
case 0: // 实线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
});
}
// 设置各条线的颜色
upperLine.attr('stroke', upperColor);
middleLine.attr('stroke', middleColor);
lowerLine.attr('stroke', lowerColor);
// 设置线条宽度和样式
[upperLine, middleLine, lowerLine].forEach(line => {
line.attr('stroke-width', width);
// 设置线条样式
switch(parseInt(style)) {
case 0: // 线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
});
}
// 监听配置变化以更新预览
$(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview);
$(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview);
$(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview);
// 点击弹窗外部关闭
$(document).on('click', '#bbConfigModal', function(e) {
if (e.target === this) {
@@ -9128,8 +9175,6 @@
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
// 添加页面滚动事件监听器,清除十字线延长线
$(window).on('scroll', function() {
try {
@@ -9146,18 +9191,10 @@
console.debug('清除滚动中的十字线时出错:', e);
}
});
});
// ====== ChanMACD图表相关函数 ======
// 清除ChanMACD标注
function clearChanMacdMarkers() {
// 清除所有系列的标记
@@ -9219,7 +9256,7 @@
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
shape: 'square',
text: `S${index}E`,
size: 0.5
size: 0.5
});
}
});
@@ -9369,7 +9406,6 @@
addStateMarkers(stateMarkers);
}
}
// 添加状态标记
function addStateMarkers(stateMarkers) {
const stateMarkersList = [];