添加三类买卖点识别和图上标注,还不错,感觉可以赚钱了,明天继续优化一下

This commit is contained in:
jackyu66git
2026-02-20 01:37:39 +08:00
parent e2726db9c1
commit 01e71b7098
9 changed files with 674 additions and 55 deletions
+10 -2
View File
@@ -268,7 +268,15 @@ Chan_BSP_MAIN_TYPE = Literal['1', '2', '3']
class Chan_BSP_DIR(Enum): class Chan_BSP_DIR(Enum):
BUY = auto() BUY = auto()
SELL = auto() SELL = auto()
class Chan_BSP_TYPE(Enum):
B1 = auto()
B2 = auto()
B3 = auto()
S1 = auto()
S2 = auto()
S3 = auto()
NONE = auto()
"""
class Chan_BSP_TYPE(Enum): class Chan_BSP_TYPE(Enum):
T1 = '1' T1 = '1'
T1P = '1p' T1P = '1p'
@@ -285,7 +293,7 @@ class Chan_BSP_TYPE(Enum):
def main_type(self) -> Chan_BSP_MAIN_TYPE: def main_type(self) -> Chan_BSP_MAIN_TYPE:
return self.value[0] # type: ignore return self.value[0] # type: ignore
"""
class Chan_AUTYPE(Enum): class Chan_AUTYPE(Enum):
QFQ = auto() QFQ = auto()
HFQ = auto() HFQ = auto()
+6 -2
View File
@@ -1,7 +1,7 @@
import copy import copy
from typing import Dict, Optional from typing import Dict, Optional
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX, Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS, Chan_EMA_SEMANTIC from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX, Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS, Chan_EMA_SEMANTIC, Chan_BSP_TYPE
import ChanKLU import ChanKLU
import ChanCTime import ChanCTime
@@ -55,6 +55,7 @@ class ChanKLC():
self.klc_dir = Chan_KLINE_DIR.UP if klu.close > klu.open else Chan_KLINE_DIR.DOWN self.klc_dir = Chan_KLINE_DIR.UP if klu.close > klu.open else Chan_KLINE_DIR.DOWN
self.ema_dir = klu.ema_dir self.ema_dir = klu.ema_dir
self.bsp = False self.bsp = False
self.bsp_type = Chan_BSP_TYPE.NONE
# EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC} # EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC}
self.ema_status = {} self.ema_status = {}
# 向后兼容:保留 ema52_status 和 ema52_pos # 向后兼容:保留 ema52_status 和 ema52_pos
@@ -233,7 +234,10 @@ class ChanKLC():
# threshold_pct: 阈值百分比,用于自动计算绝对阈值 # threshold_pct: 阈值百分比,用于自动计算绝对阈值
# 例如 0.001 表示 EMA 值的 0.1%BTC $100,000 时 threshold = $100 # 例如 0.001 表示 EMA 值的 0.1%BTC $100,000 时 threshold = $100
threshold_pct = 0.001 threshold_pct = 0.001
def set_bsp_type(self, bsp_type):
if bsp_type and bsp_type != Chan_BSP_TYPE.NONE:
self.bsp_type = bsp_type
self.bsp = True
def cal_all_ema_status(self): def cal_all_ema_status(self):
""" """
统一计算所有EMA与K线的位置关系和语义状态 统一计算所有EMA与K线的位置关系和语义状态
+2 -2
View File
@@ -157,8 +157,8 @@ class ChanLun():
return self.tf_df.find_first_bsp(bi_list, bi_zs_list) return self.tf_df.find_first_bsp(bi_list, bi_zs_list)
def find_second_bsp(self, bi_list, first_bsp_list): def find_second_bsp(self, bi_list, first_bsp_list):
return self.tf_df.find_second_bsp(bi_list, first_bsp_list) return self.tf_df.find_second_bsp(bi_list, first_bsp_list)
def find_third_bsp(self, bi_list, bi_zs_list): def find_all_bsp(self, bi_list, bi_zs_list):
return self.tf_df.find_third_bsp(bi_list, bi_zs_list) return self.tf_df.find_all_bsp(bi_list, bi_zs_list)
def get_zs_list(self, bi_list, seg_list): def get_zs_list(self, bi_list, seg_list):
return self.tf_df.get_zs_list(bi_list, seg_list) return self.tf_df.get_zs_list(bi_list, seg_list)
def cal_bi_zs(self, seg_list): def cal_bi_zs(self, seg_list):
+70 -11
View File
@@ -156,12 +156,12 @@ class TF_DF():
return klu_state_list return klu_state_list
def check_fx(self, klc): def check_fx(self, klc):
if klc.pre and klc.next: if klc.pre and klc.next:
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low and klc.macd> 0: 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: #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
klc.set_fx(Chan_FX_TYPE.TOP) klc.set_fx(Chan_FX_TYPE.TOP)
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP")
return Chan_FX_TYPE.TOP return Chan_FX_TYPE.TOP
elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high and klc.macd < 0: elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high:
#if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0:
klc.set_fx(Chan_FX_TYPE.BOTTOM) klc.set_fx(Chan_FX_TYPE.BOTTOM)
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM")
@@ -1399,7 +1399,7 @@ class TF_DF():
if len(zs_list) > 0: if len(zs_list) > 0:
bi_zs_list.append(zs_list) bi_zs_list.append(zs_list)
return bi_zs_list return bi_zs_list
def find_third_bsp(self, bi_list, bi_zs_list): def find_all_bsp(self, bi_list, bi_zs_list):
""" """
笔中枢的三类买卖点识别 笔中枢的三类买卖点识别
@@ -1444,6 +1444,18 @@ class TF_DF():
# 三类买点:向上离开中枢后回拉不破 zg # 三类买点:向上离开中枢后回拉不破 zg
#print("Leave bi:", leave_bi.start_time, leave_bi.end_time, leave_bi.dir, leave_bi.is_sure, leave_bi.low, leave_bi.high) #print("Leave bi:", leave_bi.start_time, leave_bi.end_time, leave_bi.dir, leave_bi.is_sure, leave_bi.low, leave_bi.high)
if leave_bi.dir == Chan_BI_DIR.UP: if leave_bi.dir == Chan_BI_DIR.UP:
first_bsp_bi_div = self.check_bi_div(zs, leave_bi)
# 确认一类卖点:离开断能量小于进入段能量
if first_bsp_bi_div:
bsp = ChanBSP(
leave_bi, len(bsp_list),
Chan_BSP_TYPE.S1,
Chan_BSP_DIR.SELL,
leave_bi.sure_time,
1, zs, None
)
leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S1)
bsp_list.append(bsp)
# 回拉笔 # 回拉笔
pullback_bi = leave_bi.next pullback_bi = leave_bi.next
#print(pullback_bi.start_klc.start_time, pullback_bi.dir, pullback_bi.is_sure, pullback_bi.low, pullback_bi.high) #print(pullback_bi.start_klc.start_time, pullback_bi.dir, pullback_bi.is_sure, pullback_bi.low, pullback_bi.high)
@@ -1452,15 +1464,41 @@ class TF_DF():
# 确认三类买点:回拉笔的低点不跌回中枢 # 确认三类买点:回拉笔的低点不跌回中枢
bsp = ChanBSP( bsp = ChanBSP(
pullback_bi, len(bsp_list), pullback_bi, len(bsp_list),
Chan_BSP_TYPE.T3, Chan_BSP_TYPE.B3,
Chan_BSP_DIR.BUY, Chan_BSP_DIR.BUY,
pullback_bi.sure_time, pullback_bi.sure_time,
1, zs, None 1, zs, None
) )
pullback_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B3)
bsp_list.append(bsp)
# 二类卖点
if first_bsp_bi_div:
second_bsp_bi = pullback_bi.next
if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.high < leave_bi.end_klc.high:
# 确认二类卖点:一类卖点后回拉不超过一类卖点高点
bsp = ChanBSP(
second_bsp_bi, len(bsp_list),
Chan_BSP_TYPE.S2,
Chan_BSP_DIR.SELL,
second_bsp_bi.sure_time,
1, zs, None
)
second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2)
bsp_list.append(bsp) bsp_list.append(bsp)
# 三类卖点:向下离开中枢后反弹不破 zd # 三类卖点:向下离开中枢后反弹不破 zd
elif leave_bi.dir == Chan_BI_DIR.DOWN: elif leave_bi.dir == Chan_BI_DIR.DOWN:
first_bsp_bi_div = self.check_bi_div(zs, leave_bi)
# 确认一类买点:离开段能量小于进入段
if first_bsp_bi_div:
bsp = ChanBSP(
leave_bi, len(bsp_list),
Chan_BSP_TYPE.B1,
Chan_BSP_DIR.BUY,
leave_bi.sure_time,
1, zs, None
)
leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B1)
bsp_list.append(bsp)
# 反弹笔 # 反弹笔
bounce_bi = leave_bi.next bounce_bi = leave_bi.next
#print(bounce_bi.start_klc.start_time, bounce_bi.dir, bounce_bi.is_sure, bounce_bi.low, bounce_bi.high) #print(bounce_bi.start_klc.start_time, bounce_bi.dir, bounce_bi.is_sure, bounce_bi.low, bounce_bi.high)
@@ -1469,14 +1507,35 @@ class TF_DF():
# 确认三类卖点:反弹笔的高点不回到中枢 # 确认三类卖点:反弹笔的高点不回到中枢
bsp = ChanBSP( bsp = ChanBSP(
bounce_bi, len(bsp_list), bounce_bi, len(bsp_list),
Chan_BSP_TYPE.T3, Chan_BSP_TYPE.S3,
Chan_BSP_DIR.SELL, Chan_BSP_DIR.SELL,
bounce_bi.sure_time, bounce_bi.sure_time,
1, zs, None 1, zs, None
) )
bounce_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S3)
bsp_list.append(bsp)
# 二类卖点
if first_bsp_bi_div:
second_bsp_bi = bounce_bi.next
if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.low > leave_bi.end_klc.low:
# 确认二类买点:一类买点后回拉不超过一类卖点高点
bsp = ChanBSP(
second_bsp_bi, len(bsp_list),
Chan_BSP_TYPE.B2,
Chan_BSP_DIR.BUY,
second_bsp_bi.sure_time,
1, zs, None
)
second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2)
bsp_list.append(bsp) bsp_list.append(bsp)
return bsp_list return bsp_list
def check_bi_div(self, zs, leave_bi):
enter_bi = zs.bi_list[0].pre
macdhist_div = 0
if enter_bi and enter_bi.dir == leave_bi.dir:
macdhist_div = abs(leave_bi.macd_hist) - abs(enter_bi.macd_hist)
#print(enter_bi.end_time, leave_bi.end_time, macdhist_div < 0)
return macdhist_div < 0
def find_first_bsp(self, bi_list, bi_zs_list): def find_first_bsp(self, bi_list, bi_zs_list):
""" """
笔中枢的一类买卖点识别 笔中枢的一类买卖点识别
@@ -1776,7 +1835,7 @@ class TF_DF():
bi_out_count += 1 bi_out_count += 1
first_bi_out = bi first_bi_out = bi
if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP):
bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg)
bsp_list.append(bsp) bsp_list.append(bsp)
#print("First bi out 3", first_bi_out.start_klc.start_time) #print("First bi out 3", first_bi_out.start_klc.start_time)
in_again = False in_again = False
@@ -1812,7 +1871,7 @@ class TF_DF():
bi_out_count += 1 bi_out_count += 1
first_bi_out = bi first_bi_out = bi
if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP):
bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg)
bsp_list.append(bsp) bsp_list.append(bsp)
#print("First bi out 4", first_bi_out.start_klc.start_time) #print("First bi out 4", first_bi_out.start_klc.start_time)
in_again = False in_again = False
@@ -1820,12 +1879,12 @@ class TF_DF():
if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP:
#print(bi.start_klc.start_time, bi.high, seg.high) #print(bi.start_klc.start_time, bi.high, seg.high)
if bi.high == seg.high: if bi.high == seg.high:
bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg) bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.S3, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg)
bsp_list.append(bsp) bsp_list.append(bsp)
else: else:
if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN: if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN:
if bi.low == seg.low: if bi.low == seg.low:
bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg) bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg)
bsp_list.append(bsp) bsp_list.append(bsp)
#self.print_zs(zs_list) #self.print_zs(zs_list)
return zs_list return zs_list
+83
View File
@@ -0,0 +1,83 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": 1,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD",
"dry_run": true,
"db_url": "sqlite:///tradesv3.btc_perpetual.sqlite",
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short": true,
"timeframe": "1m",
"process_only_new_candles": false,
"unfilledtimeout": {
"entry": 1,
"exit": 1,
"exit_timeout_count": 5,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "YOUR_BINANCE_API_KEY",
"secret": "YOUR_BINANCE_API_SECRET",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT"
],
"pair_blacklist": [
"BNB/.*"
]
},
"pairlists": [
{
"method": "StaticPairList",
"number_assets": 1,
"sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 1800
}
],
"telegram": {
"enabled": true,
"token": "YOUR_TELEGRAM_BOT_TOKEN",
"chat_id": "YOUR_TELEGRAM_CHAT_ID"
},
"api_server": {
"enabled": true,
"listen_ip_address": "0.0.0.0",
"listen_port": 8820,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "change_me_to_a_random_secret_key",
"ws_token": "change_me_to_a_random_ws_token",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "BTC_Perpetual_Bot",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 2
}
}
+259
View File
@@ -0,0 +1,259 @@
# --- Do not remove these libs ---
from freqtrade.strategy import IStrategy, stoploss_from_absolute
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ChanLun import ChanLun
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR
# --------------------------------
from technical.util import resample_to_interval, resampled_merge
import talib.abstract as ta
from pandas import DataFrame
from datetime import datetime, timedelta
from freqtrade.persistence import Trade, Order
from typing import Optional
import logging
logger = logging.getLogger(__name__)
# freqtrade trade -c ./user_data/Chan/config/BTC_Perpetual_Futures.json --strategy BTC_Perpetual_Futures --strategy-path ./user_data/Chan/strategies
# freqtrade backtesting -c ./user_data/Chan/config/BTC_Perpetual_Futures.json --strategy BTC_Perpetual_Futures --strategy-path ./user_data/Chan/strategies --timerange=20260101-
# freqtrade download-data -c ./user_data/Chan/config/BTC_Perpetual_Futures.json -t 1m --pairs BTC/USDT:USDT --timerange=20260101-
class BTC_Perpetual_Futures(IStrategy):
"""
BTC永续合约交易策略 - 优化版
- 基于缠论(ChanLun)技术分析 + RSI/MACD/布林带/ATR 多指标共振
- 支持做多和做空
- 基于ATR的动态止损
- 成交量确认过滤
"""
INTERFACE_VERSION: int = 3
# ROI配 - 分阶段止盈
minimal_roi = {
"0": 0.08, # 立即: 8%止盈
"60": 0.05, # 1小时后: 5%止盈
"180": 0.02, # 3小时后: 2%止盈
"360": 0 # 6小时后: 保本出场
}
can_short = True
lev = 1.0 # 杠杆倍数,建议新手用1-3倍
stoploss = -0.04 # 默认4%止损(custom_stoploss会覆盖)
use_custom_stoploss = True
trailing_stop = False
position_adjustment_enable = False
startup_candle_count = 1440 # 需要1440根1分钟K线预热
# 时间框架常量
time5 = 5
time15 = 15
time30 = 30
time60 = 60
chan = ChanLun()
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""添加多时间框架技术指标"""
# 重采样到5分钟和30分钟
dataframe_5m = resample_to_interval(dataframe, self.get_ticker_indicator() * self.time5)
dataframe_30m = resample_to_interval(dataframe, self.get_ticker_indicator() * self.time30)
# 添加技术指标
dataframe = self._add_indicators(dataframe)
dataframe_5m = self._add_indicators(dataframe_5m)
dataframe_30m = self._add_indicators(dataframe_30m)
# 缠论状态分析
dataframe_5m['state'] = self.chan.get_klu_state(dataframe_5m)
dataframe_30m['state'] = self.chan.get_klu_state(dataframe_30m)
# 合并多时间框架数据
dataframe = resampled_merge(dataframe, dataframe_5m)
dataframe = resampled_merge(dataframe, dataframe_30m)
return dataframe
def _add_indicators(self, df: DataFrame) -> DataFrame:
"""添加技术指标"""
# MACD
macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
df['macd'] = macd['macd']
df['macdsignal'] = macd['macdsignal']
df['macdhist'] = macd['macdhist']
# 布林带 (20周期, 2倍标准差)
bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
df['bb_upper'] = bb['upperband']
df['bb_middle'] = bb['middleband']
df['bb_lower'] = bb['lowerband']
# ATR - 用于动态止损
df['atr'] = ta.ATR(df, timeperiod=14)
# EMA均线系统
df['ema5'] = ta.EMA(df, timeperiod=5)
df['ema10'] = ta.EMA(df, timeperiod=10)
df['ema26'] = ta.EMA(df, timeperiod=26)
df['ema52'] = ta.EMA(df, timeperiod=52)
# RSI
df['rsi'] = ta.RSI(df, timeperiod=14)
# 成交量比(当前成交量 / 10周期均量)
avg_vol = df['volume'].rolling(window=10).mean()
df['volume_ratio'] = (df['volume'] / avg_vol).fillna(1.0)
return df
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
进场信号定义 - 优化版
- 做多: 缠论底部信号(-10/-20) + RSI<65 + 放量 + EMA确认
- 做空: 缠论顶部信号(10/20) + RSI>35 + 放量 + EMA确认
"""
state_30m = 'resample_{}_state'.format(self.get_ticker_indicator() * self.time30)
ema52_30m = 'resample_{}_ema52'.format(self.get_ticker_indicator() * self.time30)
close_30m = 'resample_{}_close'.format(self.get_ticker_indicator() * self.time30)
shift = self.time30
# 做多信号:只在缠论-10信号 + 强势过滤
dataframe.loc[
(dataframe[state_30m].shift(shift) == "-10") &
(dataframe['rsi'] < 65) &
(dataframe['rsi'] > 20) & # RSI不过冷
(dataframe['volume_ratio'] > 1.2) & # 放量确认
(dataframe['close'] > dataframe['ema52']) & # 价格在EMA52上方
(dataframe['macd'] > dataframe['macdsignal']), # MACD金叉
['enter_long', 'enter_tag']] = (1, 'chan_long')
# 做空信号:只在缠论10信号 + 强势过滤
dataframe.loc[
(dataframe[state_30m].shift(shift) == "10") &
(dataframe['rsi'] > 35) &
(dataframe['rsi'] < 80) & # RSI不过热
(dataframe['volume_ratio'] > 1.2) & # 放量确认
(dataframe['close'] < dataframe['ema52']) & # 价格在EMA52下方
(dataframe['macd'] < dataframe['macdsignal']), # MACD死叉
['enter_short', 'enter_tag']] = (1, 'chan_short')
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
出场信号定义
- 做多出场: 缠论顶部反转信号
- 做空出场: 缠论底部反转信号
"""
state_30m = 'resample_{}_state'.format(self.get_ticker_indicator() * self.time30)
shift = self.time30
dataframe.loc[
(dataframe[state_30m].shift(shift) == "10"),
['exit_long', 'exit_tag']] = (1, 'chan_exit_long')
dataframe.loc[
(dataframe[state_30m].shift(shift) == "-10"),
['exit_short', 'exit_tag']] = (1, 'chan_exit_short')
return dataframe
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> float | None:
"""
基于ATR的动态止损
止损距离 = 开仓价 ± 1.5*ATR
"""
try:
entry_atr = trade.get_custom_data(key="entry_atr")
if entry_atr is None:
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
if dataframe is not None and len(dataframe) > 0 and 'atr' in dataframe.columns:
entry_atr = float(dataframe.iloc[-1]['atr'])
else:
return -0.04
if trade.is_short:
stop_price = trade.open_rate + (float(entry_atr) * 1.5)
else:
stop_price = trade.open_rate - (float(entry_atr) * 1.5)
return stoploss_from_absolute(stop_price, current_rate, is_short=trade.is_short)
except Exception as e:
logger.warning(f"custom_stoploss error: {e}")
return None
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs):
"""快速止盈:浮盈超过0.5%直接出场"""
if current_profit > 0.005:
return "quick_profit"
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: str | None,
side: str, **kwargs) -> bool:
"""进场前最终过滤:ATR太小或RSI极端时拒绝"""
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return False
last = dataframe.iloc[-1]
atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator() * self.time30)
atr_val = float(last.get(atr_str, 0) or 0)
if atr_val < 0.001:
return False
return True
except Exception as e:
logger.warning(f"confirm_trade_entry error: {e}")
return True
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
"""订单成交时保存ATR用于止损计算"""
try:
if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
if dataframe is not None and len(dataframe) > 0:
atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator() * self.time30)
last = dataframe.iloc[-1]
entry_atr = float(last.get(atr_str, 0) or 0) * 3
trade.set_custom_data(key="entry_atr", value=entry_atr)
except Exception as e:
logger.warning(f"order_filled error: {e}")
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
entry_tag: str | None, side: str, **kwargs) -> float:
"""入场价微调,减少滑点"""
if trade:
if trade.is_short:
return proposed_rate - 50
else:
return proposed_rate + 50
return proposed_rate
def custom_exit_price(self, pair: str, trade: Trade,
current_time: datetime, proposed_rate: float,
current_profit: float, exit_tag: str | None, **kwargs) -> float:
"""出场价微调,减少滑点"""
if trade:
if trade.is_short:
return proposed_rate + 50
else:
return proposed_rate - 50
return proposed_rate
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 self.lev
def get_ticker_indicator(self) -> int:
return int(self.timeframe[:-1])
+6 -2
View File
@@ -349,12 +349,16 @@ class ChanLun_BTC_30(IStrategy):
state30 = 'resample_{}_state'.format(self.get_ticker_indicator()*shift30) state30 = 'resample_{}_state'.format(self.get_ticker_indicator()*shift30)
dataframe.loc[ dataframe.loc[
( (
(dataframe[state30].shift(shift30) == "10") (dataframe[state30].shift(shift30) == "10") |
(dataframe[state30].shift(shift30) == "20") |
(dataframe[state30].shift(shift30) == "30")
), ),
['exit_long', 'exit_tag']] = (1, 'long_close_30') ['exit_long', 'exit_tag']] = (1, 'long_close_30')
dataframe.loc[ dataframe.loc[
( (
(dataframe[state30].shift(shift30) == "-10") (dataframe[state30].shift(shift30) == "-10") |
(dataframe[state30].shift(shift30) == "-20") |
(dataframe[state30].shift(shift30) == "-30")
), ),
['exit_short', 'exit_tag']] = (1, 'short_close_30') ['exit_short', 'exit_tag']] = (1, 'short_close_30')
return dataframe return dataframe
+45 -2
View File
@@ -532,7 +532,9 @@ def analyze_chan(df, symbol=None, timeframe=None):
bi_zs_list = [] bi_zs_list = []
bsp_list = [] bsp_list = []
if len(bi_zs_list) > 0: if len(bi_zs_list) > 0:
bsp_list = chan.find_third_bsp(bi_list, bi_zs_list) bsp_list = chan.find_all_bsp(bi_list, bi_zs_list)
for bsp in bsp_list:
print(bsp.end_time, bsp.type, bsp.dir)
# 添加买卖点识别 # 添加买卖点识别
for bi in bi_list: for bi in bi_list:
bi.cal_macdhist() bi.cal_macdhist()
@@ -646,6 +648,7 @@ def analyze_chan(df, symbol=None, timeframe=None):
'seg_list': seg_list, 'seg_list': seg_list,
'zs_list': zs_list, 'zs_list': zs_list,
'bi_zs_list': bi_zs_list, # 添加BI中枢列表 'bi_zs_list': bi_zs_list, # 添加BI中枢列表
'bsp_list': bsp_list, # 添加买卖点列表
'klc_fx_info': klc_fx_info, # KLC分型信息 'klc_fx_info': klc_fx_info, # KLC分型信息
'chan_macd': chan_macd_data, # 添加ChanMACD分析数据 'chan_macd': chan_macd_data, # 添加ChanMACD分析数据
'ema52_dict': ema52_dict # 添加多时间周期EMA52数据 'ema52_dict': ema52_dict # 添加多时间周期EMA52数据
@@ -1379,7 +1382,36 @@ def analyze():
# 添加多时间周期EMA52数据 # 添加多时间周期EMA52数据
'ema52_dict': analysis_result.get('ema52_dict', {}), 'ema52_dict': analysis_result.get('ema52_dict', {}),
# 直接输出KLC趋势标记(使用已有trend字段) # 直接输出KLC趋势标记(使用已有trend字段)
'klc_trend': klc_trend 'klc_trend': klc_trend,
# 添加主周期买卖点列表
# 注意:部分枚举在转为字符串时可能形如 "Chan_BSP_TYPE.BSP1(1)"
# 这里进行健壮的解析,确保前端拿到的始终是 "BSP1" / "BUY" 这种简洁形式,
# 以便与前端的 BSP_STYLE 键(如 "BSP1_BUY")正确匹配。
'bsp_list': [{
'time': format_time_safely(bsp.end_time, client_tz),
'price': float(bsp.klc.low if 'BUY' in str(bsp.dir) else bsp.klc.high),
# -- 规范化 type 名称,例如:
# "Chan_BSP_TYPE.BSP1" -> "BSP1"
# "Chan_BSP_TYPE.BSP1(1)" -> "BSP1"
# "BSP1" -> "BSP1"
'type': (
lambda raw: (
(raw.split('.')[-1] if '.' in raw else raw).split('(')[0]
)
)(str(bsp.type)),
# -- 规范化 dir 名称,例如:
# "Chan_BSP_DIR.BUY" -> "BUY"
# "Chan_BSP_DIR.BUY(1)" -> "BUY"
# "BUY" -> "BUY"
'dir': (
lambda raw: (
(raw.split('.')[-1] if '.' in raw else raw).split('(')[0]
)
)(str(bsp.dir)),
'is_sure': bool(bsp.is_sure),
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
} for bsp in analysis_result.get('bsp_list', [])]
}) })
@@ -1547,6 +1579,17 @@ def analyze():
# 添加小周期 KLC 趋势标记 # 添加小周期 KLC 趋势标记
result['element_klc_trend'] = element_klc_trend result['element_klc_trend'] = element_klc_trend
# 添加次周期买卖点列表
result['element_bsp_list'] = [{
'time': format_time_safely(bsp.end_time, client_tz),
'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high),
'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''),
'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''),
'is_sure': bool(bsp.is_sure),
'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None,
'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0
} for bsp in element_analysis.get('bsp_list', [])]
pass pass
+193 -34
View File
@@ -912,6 +912,10 @@
<input class="form-check-input" type="checkbox" id="toggleUOnMain"> <input class="form-check-input" type="checkbox" id="toggleUOnMain">
<label class="form-check-label" for="toggleUOnMain">显示U</label> <label class="form-check-label" for="toggleUOnMain">显示U</label>
</div> </div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showMainBsp">
<label class="form-check-label" for="showMainBsp">买卖点</label>
</div>
</div> </div>
<div class="d-flex align-items-center mt-2"> <div class="d-flex align-items-center mt-2">
<label class="form-label me-0 mb-0">次周期:</label> <label class="form-label me-0 mb-0">次周期:</label>
@@ -950,6 +954,10 @@
<input class="form-check-input" type="checkbox" id="toggleUOnElement"> <input class="form-check-input" type="checkbox" id="toggleUOnElement">
<label class="form-check-label" for="toggleUOnElement">显示U</label> <label class="form-check-label" for="toggleUOnElement">显示U</label>
</div> </div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showElementBsp">
<label class="form-check-label" for="showElementBsp">买卖点</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -4691,9 +4699,135 @@
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); } } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
}); });
} }
// 添加买卖点标记 // 添加买卖点标记(新版:基于 bsp_list / element_bsp_list,按与 KLC 分型相同方式合并到主图标记)
if ($('#showTradePoints').is(':checked')) { if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked')) {
console.log('绘制买卖点 - 已启用'); console.log('绘制买卖点(BSP) - 已启用');
// BSP 样式定义
const BSP_STYLE = {
'BSP1_BUY': { color: '#FF1744', text: 'B1', position: 'belowBar', size: 0.5 },
'BSP2_BUY': { color: '#F50057', text: 'B2', position: 'belowBar', size: 0.5 },
'BSP3_BUY': { color: '#D500F9', text: 'B3', position: 'belowBar', size: 0.5 },
'BSP1_SELL': { color: '#00E676', text: 'S1', position: 'aboveBar', size: 0.5 },
'BSP2_SELL': { color: '#00B0FF', text: 'S2', position: 'aboveBar', size: 0.5 },
'BSP3_SELL': { color: '#8B4513', text: 'S3', position: 'aboveBar', size: 0.5 },
};
const getBspStyleKey = (bsp) => {
// 统一 BSP key
// - type 可能是 "BSP1"/"BSP2"/"BSP3"
// - 也可能是后端给的 "B1"/"B2"/"B3" 或 "S1"/"S2"/"S3"
// 最终都映射为 "BSP1_BUY" / "BSP1_SELL" 这类 key,方便复用现有样式定义
let type = (bsp.type || '').toUpperCase();
const dir = (bsp.dir || '').toUpperCase();
// 若是 "B1" / "B2" / "B3" 或 "S1" / "S2" / "S3" 形式,则提取数字并映射成 "BSP{n}"
const simpleMatch = type.match(/^([BS])(\d)$/);
if (simpleMatch) {
const n = simpleMatch[2]; // "1" / "2" / "3"
type = 'BSP' + n;
}
return type + '_' + dir;
};
// 收集所有 BSP 标记
const allBspMarkers = [];
// 主周期买卖点
// 兼容不同字段命名:优先使用 bsp_list,若不存在则尝试 bsp
const mainBspList = currentData.bsp_list || currentData.bsp || [];
// 调试:打印前几条主周期 BSP 的 key,方便排查样式不匹配问题
if (mainBspList.length > 0) {
console.log(
'主周期 BSP 示例 (前5条):',
mainBspList.slice(0, 5).map(b => ({
raw_type: b.type,
raw_dir: b.dir,
key: getBspStyleKey(b)
}))
);
}
if ($('#showMainBsp').is(':checked') && mainBspList.length > 0) {
console.log(`绘制主周期买卖点,共${mainBspList.length}条`);
mainBspList.forEach(function(bsp) {
try {
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
if (isNaN(ts)) return;
const key = getBspStyleKey(bsp);
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
const sureText = bsp.is_sure ? '' : '?';
allBspMarkers.push({
time: ts,
position: style.position,
color: style.color,
shape: style.shape,
text: style.text + sureText,
size: 2
});
} catch (e) {
console.error('主周期BSP处理出错:', e);
}
});
}
// 次周期买卖点
// 兼容不同字段命名:优先使用 element_bsp_list,若不存在则尝试 element_bsp
const elementBspList = currentData.element_bsp_list || currentData.element_bsp || [];
// 调试:打印前几条次周期 BSP 的 key
if (elementBspList.length > 0) {
console.log(
'次周期 BSP 示例 (前5条):',
elementBspList.slice(0, 5).map(b => ({
raw_type: b.type,
raw_dir: b.dir,
key: getBspStyleKey(b)
}))
);
}
if ($('#showElementBsp').is(':checked') && elementBspList.length > 0) {
console.log(`绘制次周期买卖点,共${elementBspList.length}条`);
elementBspList.forEach(function(bsp) {
try {
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
if (isNaN(ts)) return;
const key = getBspStyleKey(bsp);
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
const sureText = bsp.is_sure ? '' : '?';
// 次周期使用稍小的标记和不同前缀以区分
allBspMarkers.push({
time: ts,
position: style.position,
color: style.color,
shape: style.shape,
text: 'e' + style.text + sureText,
size: 1
});
} catch (e) {
console.error('次周期BSP处理出错:', e);
}
});
}
// 将 BSP 标记挂到全局,后面与 KLC 分型等标记一起合并到主系列上
if (allBspMarkers.length > 0) {
// 按时间排序(lightweight-charts 要求标记按时间升序)
allBspMarkers.sort((a, b) => a.time - b.time);
window.bspMarkers = allBspMarkers;
console.log(`准备合并 ${allBspMarkers.length} 个BSP标记到主图标记中`);
} else {
window.bspMarkers = [];
}
} else {
// 关闭 BSP 显示时,清空全局 BSP 标记
window.bspMarkers = [];
}
// 添加买卖点标记(旧版,保留兼容)
// 这里为了与主面板上的「买卖点」开关保持一致,
// 同时响应顶部的 `#showMainBsp` 复选框
if ($('#showTradePoints').is(':checked') || $('#showMainBsp').is(':checked')) {
console.log('绘制买卖点 - 已启用(来源: showTradePoints / showMainBsp');
// 优先使用小周期数据,如果不存在则使用主周期数据 // 优先使用小周期数据,如果不存在则使用主周期数据
const tradePointsData = currentData.element_trade_points || currentData.trade_points; const tradePointsData = currentData.element_trade_points || currentData.trade_points;
@@ -4837,29 +4971,31 @@
lastValueVisible: false, lastValueVisible: false,
priceLineVisible: false, priceLineVisible: false,
lineVisible: false, lineVisible: false,
color: 'transparent',
title: '买点' title: '买点'
}); });
// 设置临时的数据点 // 使用主K线的收盘价作为基准数据,保证买点标记与价格在同一纵轴范围
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]); if (Array.isArray(candles) && candles.length > 0) {
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
buyMarkersSeries.setData(baseData);
} else {
// 兜底:至少一个数据点,避免报错
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: buyMarkers[0].price || 0 }]);
}
try { try {
// 设置买点标记,并添加偏移 // 设置买点标记:文字在价格上方,仅显示文字不显示形状
buyMarkersSeries.setMarkers( buyMarkersSeries.setMarkers(
buyMarkers.map(marker => { buyMarkers.map(marker => {
// 使用固定偏移而非百分比 // 使用实际价格位置,买点显示在K线上方
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个买点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return { return {
...marker, ...marker,
position: 'aboveBar', // 显示在K线上方 position: 'aboveBar', // 买点:价格上方
// 使用原始价格 price: marker.price,
price: marker.price // 隐藏形状,仅保留文字
size: 0,
color: 'rgba(0, 0, 0, 0)'
}; };
}) })
); );
@@ -4874,29 +5010,31 @@
lastValueVisible: false, lastValueVisible: false,
priceLineVisible: false, priceLineVisible: false,
lineVisible: false, lineVisible: false,
color: 'transparent',
title: '卖点' title: '卖点'
}); });
// 设置临时的数据点 // 使用主K线的收盘价作为基准数据,保证卖点标记与价格在同一纵轴范围
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]); if (Array.isArray(candles) && candles.length > 0) {
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
sellMarkersSeries.setData(baseData);
} else {
// 兜底:至少一个数据点,避免报错
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: sellMarkers[0].price || 0 }]);
}
try { try {
// 设置卖点标记,并添加偏移 // 设置卖点标记:文字在价格下方,仅显示文字不显示形状
sellMarkersSeries.setMarkers( sellMarkersSeries.setMarkers(
sellMarkers.map(marker => { sellMarkers.map(marker => {
// 使用固定偏移而非百分比 // 使用实际价格位置,卖点显示在K线下方
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
// 为同一时间点的多个卖点额外增加堆叠偏移
const stackOffset = marker.stackIndex > 0 ?
10 * marker.stackIndex : 0;
// 使用实际价格位置添加标记,不使用偏移
return { return {
...marker, ...marker,
position: 'belowBar', // 显示在K线下方 position: 'belowBar', // 卖点:价格下方
// 使用原始价格 price: marker.price,
price: marker.price // 隐藏形状,仅保留文字
size: 0,
color: 'rgba(0, 0, 0, 0)'
}; };
}) })
); );
@@ -5663,10 +5801,18 @@
...allElementFxMarkers, ...allElementFxMarkers,
...(window.kluDivMarkersMain || []), ...(window.kluDivMarkersMain || []),
...(window.kluDivMarkersElement || []), ...(window.kluDivMarkersElement || []),
...trendMarkersToUse ...trendMarkersToUse,
...(window.bspMarkers || [])
]; ];
if (combinedMarkers.length > 0) { if (combinedMarkers.length > 0) {
console.log('合并设置', combinedMarkers.length, '个标记(主周期分型:', (window.mainFxMarkers || []).length, '个,小周期分型:', allElementFxMarkers.length, '个,UnitTF:', (window.unittfMarkers || []).length, '个)'); console.log(
'合并设置', combinedMarkers.length, '个标记(主周期分型:',
(window.mainFxMarkers || []).length,
'个,小周期分型:', allElementFxMarkers.length,
'个,UnitTF:', (window.unittfMarkers || []).length,
'个,BSP标记:', (window.bspMarkers || []).length,
'个)'
);
// 根据当前主系列类型设置标记 // 根据当前主系列类型设置标记
const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
@@ -5754,11 +5900,16 @@
}); });
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
} }
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
// 修复:把 BSP 标记一并合并进来。
const onlyMainAndU = [ const onlyMainAndU = [
...(window.mainFxMarkers || []), ...(window.mainFxMarkers || []),
...(window.kluDivMarkersMain || []), ...(window.kluDivMarkersMain || []),
...(window.kluDivMarkersElement || []), ...(window.kluDivMarkersElement || []),
...trendMarkersToUse ...trendMarkersToUse,
...(window.bspMarkers || [])
]; ];
if (onlyMainAndU.length > 0) { if (onlyMainAndU.length > 0) {
console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, 'UnitTF:', (window.unittfMarkers || []).length, ''); console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, 'UnitTF:', (window.unittfMarkers || []).length, '');
@@ -7739,6 +7890,14 @@
window.showUOnElement = $('#toggleUOnElement').is(':checked'); window.showUOnElement = $('#toggleUOnElement').is(':checked');
refreshChartOnly(); refreshChartOnly();
}); });
// 买卖点显示开关
$('#showMainBsp').change(function() {
updateChartDisplay();
});
$('#showElementBsp').change(function() {
updateChartDisplay();
});
// 在控制台输出当前显示状态 // 在控制台输出当前显示状态
console.log('当前显示状态:', { console.log('当前显示状态:', {