添加裸K形态,前端添加KLC显示
This commit is contained in:
+74
@@ -51,6 +51,80 @@ class Chan_KLU_TYPE(Enum):
|
|||||||
SmallBear = auto()
|
SmallBear = auto()
|
||||||
Cross = 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):
|
class Chan_FX_TYPE(Enum):
|
||||||
BOTTOM = auto()
|
BOTTOM = auto()
|
||||||
TOP = auto()
|
TOP = auto()
|
||||||
|
|||||||
+5
-1
@@ -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:
|
class ChanKLU:
|
||||||
def __init__(self, time, open, high, low, close, volume):
|
def __init__(self, time, open, high, low, close, volume):
|
||||||
# _time, _close, _open, _high, _low, _extra_info={}
|
# _time, _close, _open, _high, _low, _extra_info={}
|
||||||
@@ -21,6 +21,7 @@ class ChanKLU:
|
|||||||
self.bb52lower = 0
|
self.bb52lower = 0
|
||||||
# === 新增:K线类型 ===
|
# === 新增:K线类型 ===
|
||||||
self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线
|
self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线
|
||||||
|
self.pattern = Chan_KLU_PATTERN.UNKNOWN
|
||||||
|
|
||||||
# === 新增:实时分型相关属性 ===
|
# === 新增:实时分型相关属性 ===
|
||||||
self.pre = None # 前一根K线
|
self.pre = None # 前一根K线
|
||||||
@@ -67,10 +68,13 @@ class ChanKLU:
|
|||||||
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
|
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
|
||||||
def set_macd_state(self, state):
|
def set_macd_state(self, state):
|
||||||
self.macd_state = state
|
self.macd_state = state
|
||||||
|
def set_pattern(self, pattern):
|
||||||
|
self.pattern = pattern
|
||||||
def cal_exception(self):
|
def cal_exception(self):
|
||||||
if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5:
|
if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5:
|
||||||
self.exception = True
|
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)
|
#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):
|
def set_trend(self, trend):
|
||||||
self.trend = trend
|
self.trend = trend
|
||||||
def set_next(self, next):
|
def set_next(self, next):
|
||||||
|
|||||||
+68
-6
@@ -1,6 +1,6 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pandas import DataFrame
|
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 ChanKLU import ChanKLU
|
||||||
from ChanKLC import ChanKLC
|
from ChanKLC import ChanKLC
|
||||||
from ChanBI import ChanBI
|
from ChanBI import ChanBI
|
||||||
@@ -576,7 +576,7 @@ class ChanLun():
|
|||||||
if near_resistance_touch:
|
if near_resistance_touch:
|
||||||
# 若动量不强,则更偏空
|
# 若动量不强,则更偏空
|
||||||
score -= 1 if (hist is None or pre_hist is None or hist <= pre_hist) else 0
|
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 但收盘仍在下方
|
||||||
# - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方
|
# - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方
|
||||||
@@ -701,7 +701,7 @@ class ChanLun():
|
|||||||
for i in range(1, len(hist_seq)):
|
for i in range(1, len(hist_seq)):
|
||||||
if abs(hist_seq[i]) < abs(hist_seq[i-1]):
|
if abs(hist_seq[i]) < abs(hist_seq[i-1]):
|
||||||
weaken_steps += 1
|
weaken_steps += 1
|
||||||
# 近窗口对 EMA52 的“未能站上/跌破”统计(放宽窗口与条件)
|
# 近窗口对 EMA52 的"未能站上/跌破"统计(放宽窗口与条件)
|
||||||
window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else []
|
window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else []
|
||||||
no_up_break = False
|
no_up_break = False
|
||||||
no_down_break = False
|
no_down_break = False
|
||||||
@@ -1343,8 +1343,70 @@ class ChanLun():
|
|||||||
return klc_list
|
return klc_list
|
||||||
|
|
||||||
def get_klu_list(self, dataframe):
|
def get_klu_list(self, dataframe):
|
||||||
return self.get_kl_data(dataframe)
|
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
|
||||||
|
|
||||||
def get_decimal(self, value):
|
for i, klu in enumerate(klu_list):
|
||||||
return Decimal("{:.2f}".format(value))
|
# 单根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
@@ -112,10 +112,7 @@ class ChanSEG():
|
|||||||
last_zs = zs
|
last_zs = zs
|
||||||
else:
|
else:
|
||||||
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure:
|
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:
|
if bi.low > last_zs.zg or bi.high < last_zs.zd:
|
||||||
last_zs.add_bi(bi.pre)
|
|
||||||
last_zs.add_bi(bi)
|
|
||||||
else:
|
|
||||||
last_zs.set_end_bi(last_zs.bi_list[-1], bi)
|
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:
|
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)
|
zg = min(bi.high, bi.next.high, bi.next.next.high)
|
||||||
@@ -131,6 +128,12 @@ class ChanSEG():
|
|||||||
zs.add_bi(bi.next.next)
|
zs.add_bi(bi.next.next)
|
||||||
zs_list.append(zs)
|
zs_list.append(zs)
|
||||||
last_zs = 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:
|
else:
|
||||||
for index in range(1, len(self.bi_list)):
|
for index in range(1, len(self.bi_list)):
|
||||||
bi = self.bi_list[index]
|
bi = self.bi_list[index]
|
||||||
@@ -151,10 +154,7 @@ class ChanSEG():
|
|||||||
last_zs = zs
|
last_zs = zs
|
||||||
else:
|
else:
|
||||||
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure:
|
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:
|
if bi.low > last_zs.zg or bi.high < last_zs.zd:
|
||||||
last_zs.add_bi(bi.pre)
|
|
||||||
last_zs.add_bi(bi)
|
|
||||||
else:
|
|
||||||
last_zs.set_end_bi(last_zs.bi_list[-1], bi)
|
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:
|
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)
|
zg = min(bi.high, bi.next.high, bi.next.next.high)
|
||||||
@@ -170,8 +170,13 @@ class ChanSEG():
|
|||||||
zs.add_bi(bi.next.next)
|
zs.add_bi(bi.next.next)
|
||||||
zs_list.append(zs)
|
zs_list.append(zs)
|
||||||
last_zs = zs
|
last_zs = zs
|
||||||
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure:
|
else:
|
||||||
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1])
|
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.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
|
return zs_list
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
@@ -1167,6 +1167,18 @@ def analyze():
|
|||||||
# 添加主周期分析结果到返回数据
|
# 添加主周期分析结果到返回数据
|
||||||
result.update({
|
result.update({
|
||||||
'kline_data': clean_dataframe_for_json(df).to_dict('records'),
|
'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': [{
|
'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(),
|
'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,
|
'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线数据
|
# 添加小周期K线数据
|
||||||
result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records')
|
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'] = [{
|
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(),
|
'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,
|
'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,
|
||||||
|
|||||||
+124
-88
@@ -648,7 +648,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bb-config-content {
|
.bb-config-content {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -817,6 +816,7 @@
|
|||||||
<option value="baseline">基线</option>
|
<option value="baseline">基线</option>
|
||||||
<option value="renko">砖型</option>
|
<option value="renko">砖型</option>
|
||||||
<option value="heikin">平均K</option>
|
<option value="heikin">平均K</option>
|
||||||
|
<option value="klc">KLC</option>
|
||||||
</select>
|
</select>
|
||||||
<!-- 为兼容现有逻辑,保留隐藏的原始K线开关,与下拉同步 -->
|
<!-- 为兼容现有逻辑,保留隐藏的原始K线开关,与下拉同步 -->
|
||||||
<input class="form-check-input" type="checkbox" id="showOriginalKline" checked style="display:none;">
|
<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 }));
|
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
|
||||||
series.setData(baseData);
|
series.setData(baseData);
|
||||||
tvWidget.series.baselineSeries = series;
|
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.signalLineSeries = signalLineSeries;
|
||||||
tvWidget.series.histogramSeries = histogramSeries;
|
tvWidget.series.histogramSeries = histogramSeries;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加ChanMACD图表
|
// 添加ChanMACD图表
|
||||||
console.log('ChanMACD图表创建条件检查:', {
|
console.log('ChanMACD图表创建条件检查:', {
|
||||||
showMacd: showMacd,
|
showMacd: showMacd,
|
||||||
@@ -2733,7 +2745,6 @@
|
|||||||
if (typeof window.showUOnElement === 'undefined') {
|
if (typeof window.showUOnElement === 'undefined') {
|
||||||
window.showUOnElement = $('#toggleUOnElement').is(':checked');
|
window.showUOnElement = $('#toggleUOnElement').is(':checked');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMacd && chanMacdChart && ((useElementPeriod && currentData.element_macd) || currentData.macd) && (useElementPeriod ? currentData.element_kline_data : currentData.kline_data)) {
|
if (showMacd && chanMacdChart && ((useElementPeriod && currentData.element_macd) || currentData.macd) && (useElementPeriod ? currentData.element_kline_data : currentData.kline_data)) {
|
||||||
console.log('✅ 开始创建 ChanMACD 系列');
|
console.log('✅ 开始创建 ChanMACD 系列');
|
||||||
// 创建ChanMACD线系列
|
// 创建ChanMACD线系列
|
||||||
@@ -4338,7 +4349,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主周期未完成中枢
|
// 主周期未完成中枢
|
||||||
if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
|
if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
|
||||||
console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
|
console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
|
||||||
@@ -5305,7 +5315,6 @@
|
|||||||
// 收集所有主周期分型标记
|
// 收集所有主周期分型标记
|
||||||
const allMainFxMarkers = [];
|
const allMainFxMarkers = [];
|
||||||
const mainFxMarkers = []; // 用于tooltip支持
|
const mainFxMarkers = []; // 用于tooltip支持
|
||||||
|
|
||||||
// 处理主周期KLC分型
|
// 处理主周期KLC分型
|
||||||
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
|
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
|
||||||
console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`);
|
console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`);
|
||||||
@@ -5671,6 +5680,7 @@
|
|||||||
else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries;
|
else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries;
|
||||||
else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries;
|
else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries;
|
||||||
else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries;
|
else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries;
|
||||||
|
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
|
||||||
if (targetSeries) {
|
if (targetSeries) {
|
||||||
targetSeries.setMarkers(combinedMarkers);
|
targetSeries.setMarkers(combinedMarkers);
|
||||||
} else {
|
} else {
|
||||||
@@ -5765,6 +5775,7 @@
|
|||||||
else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries;
|
else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries;
|
||||||
else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries;
|
else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries;
|
||||||
else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries;
|
else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries;
|
||||||
|
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
|
||||||
if (targetSeries2) {
|
if (targetSeries2) {
|
||||||
targetSeries2.setMarkers(onlyMainAndU);
|
targetSeries2.setMarkers(onlyMainAndU);
|
||||||
} else {
|
} else {
|
||||||
@@ -5782,6 +5793,7 @@
|
|||||||
else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries;
|
else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries;
|
||||||
else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries;
|
else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries;
|
||||||
else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries;
|
else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries;
|
||||||
|
else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries;
|
||||||
if (targetSeries3) {
|
if (targetSeries3) {
|
||||||
targetSeries3.setMarkers([]);
|
targetSeries3.setMarkers([]);
|
||||||
}
|
}
|
||||||
@@ -6024,6 +6036,9 @@
|
|||||||
} else if (klineType === 'baseline' && tvWidget.series.baselineSeries) {
|
} else if (klineType === 'baseline' && tvWidget.series.baselineSeries) {
|
||||||
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
|
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
|
||||||
tvWidget.series.baselineSeries.setData(baseData);
|
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);
|
setupDataSourceInfo(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置数据源信息显示
|
// 设置数据源信息显示
|
||||||
function setupDataSourceInfo(data) {
|
function setupDataSourceInfo(data) {
|
||||||
// 获取用户当前的周期选择
|
// 获取用户当前的周期选择
|
||||||
@@ -7103,7 +7117,6 @@
|
|||||||
$('#end_time').val(formatDatetimeLocal(now));
|
$('#end_time').val(formatDatetimeLocal(now));
|
||||||
$('#start_time').val(formatDatetimeLocal(oneDayAgo));
|
$('#start_time').val(formatDatetimeLocal(oneDayAgo));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期为datetime-local输入框格式
|
// 格式化日期为datetime-local输入框格式
|
||||||
function formatDatetimeLocal(date) {
|
function formatDatetimeLocal(date) {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
@@ -7753,7 +7766,6 @@
|
|||||||
console.error('加载A股股票列表失败');
|
console.error('加载A股股票列表失败');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测交易对类型并返回相应的配置
|
// 检测交易对类型并返回相应的配置
|
||||||
function getSymbolConfig(symbol) {
|
function getSymbolConfig(symbol) {
|
||||||
const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol);
|
const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol);
|
||||||
@@ -7811,7 +7823,6 @@
|
|||||||
|
|
||||||
return chartOptions;
|
return chartOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤非交易时间的数据(仅用于显示优化)
|
// 过滤非交易时间的数据(仅用于显示优化)
|
||||||
function filterTradingHours(data, symbolConfig) {
|
function filterTradingHours(data, symbolConfig) {
|
||||||
if (symbolConfig.type !== 'a_stock') {
|
if (symbolConfig.type !== 'a_stock') {
|
||||||
@@ -8402,7 +8413,6 @@
|
|||||||
function updateMAPanel() {
|
function updateMAPanel() {
|
||||||
updateIndicatorPanel();
|
updateIndicatorPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换均线可见性
|
// 切换均线可见性
|
||||||
function toggleMAVisibility(maId) {
|
function toggleMAVisibility(maId) {
|
||||||
console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId);
|
console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId);
|
||||||
@@ -8605,7 +8615,6 @@
|
|||||||
|
|
||||||
return candles;
|
return candles;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从蜡烛数据生成 Heikin-Ashi(平均K)
|
// 从蜡烛数据生成 Heikin-Ashi(平均K)
|
||||||
function buildHeikinFromCandles(candles) {
|
function buildHeikinFromCandles(candles) {
|
||||||
if (!Array.isArray(candles) || candles.length === 0) return [];
|
if (!Array.isArray(candles) || candles.length === 0) return [];
|
||||||
@@ -8628,6 +8637,44 @@
|
|||||||
return result;
|
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%)
|
// 计算默认砖大小(优先使用ATR的最新非零值,否则按收盘价的0.5%)
|
||||||
function computeDefaultBrickSize(candles) {
|
function computeDefaultBrickSize(candles) {
|
||||||
try {
|
try {
|
||||||
@@ -8881,10 +8928,10 @@
|
|||||||
|
|
||||||
console.log('🔄 按钮已切换为更新模式');
|
console.log('🔄 按钮已切换为更新模式');
|
||||||
|
|
||||||
$('#bbConfigModal').css('display', 'flex');
|
$('#bbConfigModal').css('display', 'flex');
|
||||||
|
|
||||||
// 更新预览
|
// 更新预览
|
||||||
setTimeout(updateBBLinePreview, 50);
|
setTimeout(updateBBLinePreview, 50);
|
||||||
}
|
}
|
||||||
// 更新布林带
|
// 更新布林带
|
||||||
function updateBollingerBand(bbId) {
|
function updateBollingerBand(bbId) {
|
||||||
@@ -9008,78 +9055,78 @@
|
|||||||
console.warn('addBollingerBandsToChart 未就绪');
|
console.warn('addBollingerBandsToChart 未就绪');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新均线预览
|
// 更新均线预览
|
||||||
function updateLinePreview() {
|
function updateLinePreview() {
|
||||||
// 检查是否在均线配置窗口
|
// 检查是否在均线配置窗口
|
||||||
if ($('#maConfigModal').is(':visible')) {
|
if ($('#maConfigModal').is(':visible')) {
|
||||||
const color = $('#maColor').val();
|
const color = $('#maColor').val();
|
||||||
const width = $('#maLineWidth').val();
|
const width = $('#maLineWidth').val();
|
||||||
const style = $('#maLineStyle').val();
|
const style = $('#maLineStyle').val();
|
||||||
|
|
||||||
const line = $('#previewLine');
|
const line = $('#previewLine');
|
||||||
line.attr('stroke', color);
|
line.attr('stroke', color);
|
||||||
line.attr('stroke-width', width);
|
line.attr('stroke-width', width);
|
||||||
|
|
||||||
// 设置线条样式
|
// 设置线条样式
|
||||||
switch(parseInt(style)) {
|
switch(parseInt(style)) {
|
||||||
case 0: // 实线
|
case 0: // 实线
|
||||||
line.attr('stroke-dasharray', 'none');
|
line.attr('stroke-dasharray', 'none');
|
||||||
break;
|
break;
|
||||||
case 1: // 点线
|
case 1: // 点线
|
||||||
line.attr('stroke-dasharray', '2,3');
|
line.attr('stroke-dasharray', '2,3');
|
||||||
break;
|
break;
|
||||||
case 2: // 虚线
|
case 2: // 虚线
|
||||||
line.attr('stroke-dasharray', '5,5');
|
line.attr('stroke-dasharray', '5,5');
|
||||||
break;
|
break;
|
||||||
case 3: // 大虚线
|
case 3: // 大虚线
|
||||||
line.attr('stroke-dasharray', '10,5');
|
line.attr('stroke-dasharray', '10,5');
|
||||||
break;
|
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');
|
||||||
function updateBBLinePreview() {
|
const middleLine = $('#bbPreviewMiddle');
|
||||||
const upperColor = $('#bbUpperColor').val();
|
const lowerLine = $('#bbPreviewLower');
|
||||||
const middleColor = $('#bbMiddleColor').val();
|
|
||||||
const lowerColor = $('#bbLowerColor').val();
|
|
||||||
const width = $('#bbLineWidth').val();
|
|
||||||
const style = $('#bbLineStyle').val();
|
|
||||||
|
|
||||||
const upperLine = $('#bbPreviewUpper');
|
// 设置各条线的颜色
|
||||||
const middleLine = $('#bbPreviewMiddle');
|
upperLine.attr('stroke', upperColor);
|
||||||
const lowerLine = $('#bbPreviewLower');
|
middleLine.attr('stroke', middleColor);
|
||||||
|
lowerLine.attr('stroke', lowerColor);
|
||||||
|
|
||||||
// 设置各条线的颜色
|
// 设置线条宽度和样式
|
||||||
upperLine.attr('stroke', upperColor);
|
[upperLine, middleLine, lowerLine].forEach(line => {
|
||||||
middleLine.attr('stroke', middleColor);
|
line.attr('stroke-width', width);
|
||||||
lowerLine.attr('stroke', lowerColor);
|
|
||||||
|
|
||||||
// 设置线条宽度和样式
|
// 设置线条样式
|
||||||
[upperLine, middleLine, lowerLine].forEach(line => {
|
switch(parseInt(style)) {
|
||||||
line.attr('stroke-width', width);
|
case 0: // 实线
|
||||||
|
line.attr('stroke-dasharray', 'none');
|
||||||
// 设置线条样式
|
break;
|
||||||
switch(parseInt(style)) {
|
case 1: // 点线
|
||||||
case 0: // 实线
|
line.attr('stroke-dasharray', '2,3');
|
||||||
line.attr('stroke-dasharray', 'none');
|
break;
|
||||||
break;
|
case 2: // 虚线
|
||||||
case 1: // 点线
|
line.attr('stroke-dasharray', '5,5');
|
||||||
line.attr('stroke-dasharray', '2,3');
|
break;
|
||||||
break;
|
case 3: // 大虚线
|
||||||
case 2: // 虚线
|
line.attr('stroke-dasharray', '10,5');
|
||||||
line.attr('stroke-dasharray', '5,5');
|
break;
|
||||||
break;
|
}
|
||||||
case 3: // 大虚线
|
});
|
||||||
line.attr('stroke-dasharray', '10,5');
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听配置变化以更新预览
|
// 监听配置变化以更新预览
|
||||||
$(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview);
|
$(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) {
|
$(document).on('click', '#bbConfigModal', function(e) {
|
||||||
if (e.target === this) {
|
if (e.target === this) {
|
||||||
@@ -9128,8 +9175,6 @@
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 添加页面滚动事件监听器,清除十字线延长线
|
// 添加页面滚动事件监听器,清除十字线延长线
|
||||||
$(window).on('scroll', function() {
|
$(window).on('scroll', function() {
|
||||||
try {
|
try {
|
||||||
@@ -9146,18 +9191,10 @@
|
|||||||
console.debug('清除滚动中的十字线时出错:', e);
|
console.debug('清除滚动中的十字线时出错:', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ====== ChanMACD图表相关函数 ======
|
// ====== ChanMACD图表相关函数 ======
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 清除ChanMACD标注
|
// 清除ChanMACD标注
|
||||||
function clearChanMacdMarkers() {
|
function clearChanMacdMarkers() {
|
||||||
// 清除所有系列的标记
|
// 清除所有系列的标记
|
||||||
@@ -9219,7 +9256,7 @@
|
|||||||
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
|
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
|
||||||
shape: 'square',
|
shape: 'square',
|
||||||
text: `S${index}E`,
|
text: `S${index}E`,
|
||||||
size: 0.5
|
size: 0.5
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -9369,7 +9406,6 @@
|
|||||||
addStateMarkers(stateMarkers);
|
addStateMarkers(stateMarkers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加状态标记
|
// 添加状态标记
|
||||||
function addStateMarkers(stateMarkers) {
|
function addStateMarkers(stateMarkers) {
|
||||||
const stateMarkersList = [];
|
const stateMarkersList = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user