添加三类买卖点识别和图上标注,还不错,感觉可以赚钱了,明天继续优化一下
This commit is contained in:
+10
-2
@@ -268,7 +268,15 @@ Chan_BSP_MAIN_TYPE = Literal['1', '2', '3']
|
||||
class Chan_BSP_DIR(Enum):
|
||||
BUY = 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):
|
||||
T1 = '1'
|
||||
T1P = '1p'
|
||||
@@ -285,7 +293,7 @@ class Chan_BSP_TYPE(Enum):
|
||||
def main_type(self) -> Chan_BSP_MAIN_TYPE:
|
||||
return self.value[0] # type: ignore
|
||||
|
||||
|
||||
"""
|
||||
class Chan_AUTYPE(Enum):
|
||||
QFQ = auto()
|
||||
HFQ = auto()
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
import copy
|
||||
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 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.ema_dir = klu.ema_dir
|
||||
self.bsp = False
|
||||
self.bsp_type = Chan_BSP_TYPE.NONE
|
||||
# EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC}
|
||||
self.ema_status = {}
|
||||
# 向后兼容:保留 ema52_status 和 ema52_pos
|
||||
@@ -233,7 +234,10 @@ class ChanKLC():
|
||||
# threshold_pct: 阈值百分比,用于自动计算绝对阈值
|
||||
# 例如 0.001 表示 EMA 值的 0.1%,BTC $100,000 时 threshold = $100
|
||||
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):
|
||||
"""
|
||||
统一计算所有EMA与K线的位置关系和语义状态
|
||||
|
||||
+2
-2
@@ -157,8 +157,8 @@ class ChanLun():
|
||||
return self.tf_df.find_first_bsp(bi_list, bi_zs_list)
|
||||
def find_second_bsp(self, 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):
|
||||
return self.tf_df.find_third_bsp(bi_list, bi_zs_list)
|
||||
def find_all_bsp(self, 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):
|
||||
return self.tf_df.get_zs_list(bi_list, seg_list)
|
||||
def cal_bi_zs(self, seg_list):
|
||||
|
||||
@@ -156,12 +156,12 @@ class TF_DF():
|
||||
return klu_state_list
|
||||
def check_fx(self, klc):
|
||||
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:
|
||||
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")
|
||||
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:
|
||||
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")
|
||||
@@ -1399,7 +1399,7 @@ class TF_DF():
|
||||
if len(zs_list) > 0:
|
||||
bi_zs_list.append(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
|
||||
#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:
|
||||
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
|
||||
#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(
|
||||
pullback_bi, len(bsp_list),
|
||||
Chan_BSP_TYPE.T3,
|
||||
Chan_BSP_TYPE.B3,
|
||||
Chan_BSP_DIR.BUY,
|
||||
pullback_bi.sure_time,
|
||||
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)
|
||||
|
||||
# 三类卖点:向下离开中枢后反弹不破 zd
|
||||
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
|
||||
#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(
|
||||
bounce_bi, len(bsp_list),
|
||||
Chan_BSP_TYPE.T3,
|
||||
Chan_BSP_TYPE.S3,
|
||||
Chan_BSP_DIR.SELL,
|
||||
bounce_bi.sure_time,
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
笔中枢的一类买卖点识别
|
||||
@@ -1776,7 +1835,7 @@ class TF_DF():
|
||||
bi_out_count += 1
|
||||
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):
|
||||
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)
|
||||
#print("First bi out 3", first_bi_out.start_klc.start_time)
|
||||
in_again = False
|
||||
@@ -1812,7 +1871,7 @@ class TF_DF():
|
||||
bi_out_count += 1
|
||||
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):
|
||||
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)
|
||||
#print("First bi out 4", first_bi_out.start_klc.start_time)
|
||||
in_again = False
|
||||
@@ -1820,12 +1879,12 @@ class TF_DF():
|
||||
if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP:
|
||||
#print(bi.start_klc.start_time, 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)
|
||||
else:
|
||||
if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN:
|
||||
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)
|
||||
#self.print_zs(zs_list)
|
||||
return zs_list
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
@@ -349,12 +349,16 @@ class ChanLun_BTC_30(IStrategy):
|
||||
state30 = 'resample_{}_state'.format(self.get_ticker_indicator()*shift30)
|
||||
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')
|
||||
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')
|
||||
return dataframe
|
||||
|
||||
+45
-2
@@ -532,7 +532,9 @@ def analyze_chan(df, symbol=None, timeframe=None):
|
||||
bi_zs_list = []
|
||||
bsp_list = []
|
||||
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:
|
||||
bi.cal_macdhist()
|
||||
@@ -646,6 +648,7 @@ def analyze_chan(df, symbol=None, timeframe=None):
|
||||
'seg_list': seg_list,
|
||||
'zs_list': zs_list,
|
||||
'bi_zs_list': bi_zs_list, # 添加BI中枢列表
|
||||
'bsp_list': bsp_list, # 添加买卖点列表
|
||||
'klc_fx_info': klc_fx_info, # KLC分型信息
|
||||
'chan_macd': chan_macd_data, # 添加ChanMACD分析数据
|
||||
'ema52_dict': ema52_dict # 添加多时间周期EMA52数据
|
||||
@@ -1379,7 +1382,36 @@ def analyze():
|
||||
# 添加多时间周期EMA52数据
|
||||
'ema52_dict': analysis_result.get('ema52_dict', {}),
|
||||
# 直接输出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 趋势标记
|
||||
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
|
||||
|
||||
|
||||
+193
-34
@@ -912,6 +912,10 @@
|
||||
<input class="form-check-input" type="checkbox" id="toggleUOnMain">
|
||||
<label class="form-check-label" for="toggleUOnMain">显示U</label>
|
||||
</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 class="d-flex align-items-center mt-2">
|
||||
<label class="form-label me-0 mb-0">次周期:</label>
|
||||
@@ -950,6 +954,10 @@
|
||||
<input class="form-check-input" type="checkbox" id="toggleUOnElement">
|
||||
<label class="form-check-label" for="toggleUOnElement">显示U</label>
|
||||
</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>
|
||||
@@ -4691,9 +4699,135 @@
|
||||
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
|
||||
});
|
||||
}
|
||||
// 添加买卖点标记
|
||||
if ($('#showTradePoints').is(':checked')) {
|
||||
console.log('绘制买卖点 - 已启用');
|
||||
// 添加买卖点标记(新版:基于 bsp_list / element_bsp_list,按与 KLC 分型相同方式合并到主图标记)
|
||||
if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked')) {
|
||||
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;
|
||||
@@ -4837,29 +4971,31 @@
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
lineVisible: false,
|
||||
color: 'transparent',
|
||||
title: '买点'
|
||||
});
|
||||
|
||||
// 设置临时的数据点
|
||||
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]);
|
||||
// 使用主K线的收盘价作为基准数据,保证买点标记与价格在同一纵轴范围
|
||||
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 {
|
||||
// 设置买点标记,并添加偏移
|
||||
// 设置买点标记:文字在价格上方,仅显示文字不显示形状
|
||||
buyMarkersSeries.setMarkers(
|
||||
buyMarkers.map(marker => {
|
||||
// 使用固定偏移而非百分比
|
||||
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
|
||||
|
||||
// 为同一时间点的多个买点额外增加堆叠偏移
|
||||
const stackOffset = marker.stackIndex > 0 ?
|
||||
10 * marker.stackIndex : 0;
|
||||
|
||||
// 使用实际价格位置添加标记,不使用偏移
|
||||
// 使用实际价格位置,买点显示在K线上方
|
||||
return {
|
||||
...marker,
|
||||
position: 'aboveBar', // 显示在K线上方
|
||||
// 使用原始价格
|
||||
price: marker.price
|
||||
position: 'aboveBar', // 买点:价格上方
|
||||
price: marker.price,
|
||||
// 隐藏形状,仅保留文字
|
||||
size: 0,
|
||||
color: 'rgba(0, 0, 0, 0)'
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -4874,29 +5010,31 @@
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
lineVisible: false,
|
||||
color: 'transparent',
|
||||
title: '卖点'
|
||||
});
|
||||
|
||||
// 设置临时的数据点
|
||||
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]);
|
||||
// 使用主K线的收盘价作为基准数据,保证卖点标记与价格在同一纵轴范围
|
||||
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 {
|
||||
// 设置卖点标记,并添加偏移
|
||||
// 设置卖点标记:文字在价格下方,仅显示文字不显示形状
|
||||
sellMarkersSeries.setMarkers(
|
||||
sellMarkers.map(marker => {
|
||||
// 使用固定偏移而非百分比
|
||||
const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20;
|
||||
|
||||
// 为同一时间点的多个卖点额外增加堆叠偏移
|
||||
const stackOffset = marker.stackIndex > 0 ?
|
||||
10 * marker.stackIndex : 0;
|
||||
|
||||
// 使用实际价格位置添加标记,不使用偏移
|
||||
// 使用实际价格位置,卖点显示在K线下方
|
||||
return {
|
||||
...marker,
|
||||
position: 'belowBar', // 显示在K线下方
|
||||
// 使用原始价格
|
||||
price: marker.price
|
||||
position: 'belowBar', // 卖点:价格下方
|
||||
price: marker.price,
|
||||
// 隐藏形状,仅保留文字
|
||||
size: 0,
|
||||
color: 'rgba(0, 0, 0, 0)'
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -5663,10 +5801,18 @@
|
||||
...allElementFxMarkers,
|
||||
...(window.kluDivMarkersMain || []),
|
||||
...(window.kluDivMarkersElement || []),
|
||||
...trendMarkersToUse
|
||||
...trendMarkersToUse,
|
||||
...(window.bspMarkers || [])
|
||||
];
|
||||
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'));
|
||||
@@ -5754,11 +5900,16 @@
|
||||
});
|
||||
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
|
||||
}
|
||||
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
|
||||
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
|
||||
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
|
||||
// 修复:把 BSP 标记一并合并进来。
|
||||
const onlyMainAndU = [
|
||||
...(window.mainFxMarkers || []),
|
||||
...(window.kluDivMarkersMain || []),
|
||||
...(window.kluDivMarkersElement || []),
|
||||
...trendMarkersToUse
|
||||
...trendMarkersToUse,
|
||||
...(window.bspMarkers || [])
|
||||
];
|
||||
if (onlyMainAndU.length > 0) {
|
||||
console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')');
|
||||
@@ -7739,6 +7890,14 @@
|
||||
window.showUOnElement = $('#toggleUOnElement').is(':checked');
|
||||
refreshChartOnly();
|
||||
});
|
||||
|
||||
// 买卖点显示开关
|
||||
$('#showMainBsp').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
$('#showElementBsp').change(function() {
|
||||
updateChartDisplay();
|
||||
});
|
||||
|
||||
// 在控制台输出当前显示状态
|
||||
console.log('当前显示状态:', {
|
||||
|
||||
Reference in New Issue
Block a user