添加三类买卖点识别和图上标注,还不错,感觉可以赚钱了,明天继续优化一下
This commit is contained in:
+10
-2
@@ -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
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
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
@@ -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
@@ -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('当前显示状态:', {
|
||||||
|
|||||||
Reference in New Issue
Block a user