diff --git a/ChanLun.py b/ChanLun.py index c383ff4..dd23fee 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -128,6 +128,8 @@ class ChanLun(): def get_bsp_state(self, dataframe): return self.tf_df.get_bsp_state(dataframe) + def get_bsp_signal_data(self, dataframe): + return self.tf_df.get_bsp_signal_data(dataframe) def get_structure_zones(self, current_price=None, config=None): if config is None: @@ -178,6 +180,8 @@ class ChanLun(): def cal_bi_zs_list(self, bi_list): #return self.tf_df.cal_bi_zs(bi_list) return self.tf_df.cal_bi_zs_list(bi_list) + def get_bi_zs_list(self, bi_list): + return self.tf_df.get_bi_zs_list(bi_list) def get_decimal(self, value): return Decimal("{:.2f}".format(value)) def get_klc_list(self, klu_list): diff --git a/TF_DF.py b/TF_DF.py index 2d1fe47..95d8c47 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -157,15 +157,41 @@ class TF_DF(): klu_state_list.append("00") print(klu_state_list[:20]) return klu_state_list - def get_bsp_state(self, dataframe): + def get_bsp_signal_data(self, dataframe): klu_list = self.get_klu_list(dataframe) klc_list = self.get_klc_list(klu_list) bi_list = self.cal_bi_list(klc_list) - seg_list = self.get_seg_list(bi_list) - bi_zs_list = self.cal_bi_zs(seg_list) + bi_zs_list = self.cal_bi_zs_list_pure(bi_list) bsp_list = self.find_all_bsp(bi_list, bi_zs_list) + bsp_by_bi_type = {} + for bsp in bsp_list: + if bsp and bsp.bi: + bsp_by_bi_type[(bsp.bi.index, bsp.type)] = bsp bsp_state_list = [0] * len(dataframe) + bsp_zg_list = [0.0] * len(dataframe) + bsp_zd_list = [0.0] * len(dataframe) + bsp_stop_price_list = [0.0] * len(dataframe) + bsp_risk_ratio_list = [0.0] * len(dataframe) klc_index = 0 + def set_bsp_signal(index, state, bsp): + bsp_state_list[index] = state + if not bsp or not bsp.zs: + return + close = float(dataframe.iloc[index]['close']) + atr = float(dataframe.iloc[index]['atr']) if 'atr' in dataframe.columns and not pd.isna(dataframe.iloc[index]['atr']) else 0.0 + atr_ratio = atr / close if close > 0 else 0.0 + buffer = atr * 0.1 + bsp_zg_list[index] = bsp.zs.zg + bsp_zd_list[index] = bsp.zs.zd + if state == -1: + stop_price = bsp.zs.zg - buffer + risk_ratio = (close - stop_price) / close if close > stop_price else atr_ratio + else: + stop_price = bsp.zs.zd + buffer + risk_ratio = (stop_price - close) / close if close < stop_price else atr_ratio + bsp_stop_price_list[index] = stop_price + bsp_risk_ratio_list[index] = max(0.001, min(float(risk_ratio), 0.02)) + for index in range(0, len(dataframe)): if klc_index == len(klc_list): klc_index = len(klc_list) - 1 @@ -175,7 +201,7 @@ class TF_DF(): bi = klc.bi.pre if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.B3: # 第三类买点 - bsp_state_list[index] = -1 + set_bsp_signal(index, -1, bsp_by_bi_type.get((bi.index, Chan_BSP_TYPE.B3))) #print(klc.end_time, "B3") else: bsp_state_list[index] = 0 @@ -183,14 +209,22 @@ class TF_DF(): bi = klc.bi.pre if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.S3: # 第三类卖点 - bsp_state_list[index] = 1 + set_bsp_signal(index, 1, bsp_by_bi_type.get((bi.index, Chan_BSP_TYPE.S3))) #print(klc.end_time, "S3") else: bsp_state_list[index] = 0 klc_index += 1 else: bsp_state_list[index] = 0 - return bsp_state_list + return { + 'bsp_state': bsp_state_list, + 'bsp_zg': bsp_zg_list, + 'bsp_zd': bsp_zd_list, + 'bsp_stop_price': bsp_stop_price_list, + 'bsp_risk_ratio': bsp_risk_ratio_list, + } + def get_bsp_state(self, dataframe): + return self.get_bsp_signal_data(dataframe)['bsp_state'] def get_ema_state(self, dataframe): klu_list = self.get_klu_list(dataframe) klc_list = self.get_klc_list(klu_list) @@ -1316,7 +1350,7 @@ class TF_DF(): if (last_top.low < klc.pre.high or last_top.low < klc.next.high) and (klc.index - last_top.index < 100): return False return True - # 建议用这种方式生成笔中枢 + # 线段内的中枢 def cal_bi_zs(self, seg_list): bi_zs_list = [] for seg in seg_list: @@ -1324,7 +1358,7 @@ class TF_DF(): if len(zs_list) > 0: bi_zs_list = list(bi_zs_list) + list(zs_list) return bi_zs_list - # 这个种方式不是很好,会有很多重叠的 + # 跨段不相连的中枢 def cal_bi_zs_list(self, bi_list): """ 根据缠论笔中枢定义计算中枢(参照 get_zs_list 线段中枢判断规则) @@ -1455,6 +1489,282 @@ class TF_DF(): if last_bi_of_zs.is_sure: last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) return bi_zs_list + def get_bi_zs_list(self, bi_list): + """ + 根据缠论笔中枢定义计算中枢(完全参照 get_seg_zs_list 线段中枢判断规则) + 从第4根笔开始(索引3),每3根笔为一组检查 + 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) + 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) + 盘整/扩张:后中枢与前中枢整体区间有交集 → 合并扩展 + 中枢可按两笔一组继续扩展到5根、7根... + """ + bi_zs_list = [] + if len(bi_list) < 3: + return bi_zs_list + + last_zs = None + start_idx = 3 + + while start_idx < len(bi_list): + if start_idx + 2 >= len(bi_list): + break + + bi1 = bi_list[start_idx] + bi2 = bi_list[start_idx + 1] + bi3 = bi_list[start_idx + 2] + + if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): + start_idx += 1 + continue + + zg = min(bi1.high, bi2.high, bi3.high) + zd = max(bi1.low, bi2.low, bi3.low) + + if zg <= zd: + start_idx += 1 + continue + + valid = False + if last_zs is None: + if bi1.dir == Chan_BI_DIR.DOWN: + zs_dir = Chan_ZS_DIR.UP + valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + else: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + else: + is_up_zs = zd > last_zs.zg + is_down_zs = zg < last_zs.zd + + if is_up_zs: + zs_dir = Chan_ZS_DIR.UP + valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + elif is_down_zs: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + + create_new_zs = False + if not valid: + # 如果新中枢和前一个中枢的中枢区间有重叠,不形成新中枢,合并扩展 + if last_zs is not None: + is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or \ + (zg < last_zs.zg and zg > last_zs.zd) or \ + (zg > last_zs.zg and zd < last_zs.zd) or \ + (zg < last_zs.zg and zd > last_zs.zd) + if is_in_last_zs: + # 扩展当前中枢:将 bi1-bi3 加入 last_zs + for bi in [bi1, bi2, bi3]: + if bi not in last_zs.bi_list: + last_zs.add_bi(bi) + create_new_zs = False + else: + start_idx += 1 + continue + else: + start_idx += 1 + continue + else: + create_new_zs = True + + # 新中枢形成时确认前一个中枢 + if last_zs and create_new_zs: + last_bi = last_zs.bi_list[-1] + if last_bi and last_bi.is_sure: + last_zs.is_sure = True + last_zs.set_end_bi(last_bi, last_bi.sure_time) + + zs = last_zs + if create_new_zs: + gg = max(bi1.high, bi2.high, bi3.high) + dd = min(bi1.low, bi2.low, bi3.low) + zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.is_sure = False + zs.bi_list = [bi1, bi2, bi3] + + # 离开后回抽扩展检查 + added_after_leave = [] + leave_index = start_idx + 4 + while leave_index < len(bi_list): + b = bi_list[leave_index] + if not b.is_sure: + break + if b.high >= zs.zd and b.low <= zs.zg: + added_after_leave.append(b.pre) + added_after_leave.append(b) + else: + break + leave_index += 2 + + if added_after_leave: + bis_for_zs = list(zs.bi_list) + list(added_after_leave) + bi_highs = [bi.high for bi in bis_for_zs] + bi_lows = [bi.low for bi in bis_for_zs] + zs.set_gg(max(bi_highs)) + zs.set_dd(min(bi_lows)) + zs.bi_list = bis_for_zs + bi = bis_for_zs[-1] + if bi.is_sure: + zs.set_end_bi(bi, bi.sure_time) + start_idx = start_idx + len(added_after_leave) + else: + if create_new_zs: + zs.set_end_bi(bi3, bi3.sure_time) + + if create_new_zs: + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + bi_zs_list.append(zs) + last_zs = zs + + start_idx += 4 + + # 最后一个中枢:根据 bi_list 最后一笔确认状态 + if last_zs: + last_zs.is_sure = bi_list[-1].is_sure + + if last_zs and not last_zs.is_sure: + if last_zs.bi_list and len(last_zs.bi_list) > 0: + last_bi_of_zs = last_zs.bi_list[-1] + last_bi_idx = -1 + for i, bi in enumerate(bi_list): + if bi == last_bi_of_zs: + last_bi_idx = i + break + + has_leave = False + if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): + for i in range(last_bi_idx + 1, len(bi_list)): + bi = bi_list[i] + if bi.is_sure: + leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ + (bi.high < last_zs.zd and bi.low < last_zs.zd) + if leave: + has_leave = True + break + + if has_leave: + if last_bi_of_zs.is_sure: + last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) + + return bi_zs_list + + def cal_bi_zs_list_pure(self, bi_list): + bi_zs_list = [] + if len(bi_list) < 3: + return bi_zs_list + + def get_zs_range(bis): + zg = min(bi.high for bi in bis) + zd = max(bi.low for bi in bis) + return zg, zd + + def is_bi_overlap_range(bi, zg, zd): + return bi.high >= zd and bi.low <= zg + + def check_zs_position_filter(last_zs, zg, zd, bis): + if last_zs is None: + return True + if zg <= last_zs.zd: + return bis[0].dir == Chan_BI_DIR.UP and bis[-1].dir == Chan_BI_DIR.UP + if zd >= last_zs.zg: + return bis[0].dir == Chan_BI_DIR.DOWN and bis[-1].dir == Chan_BI_DIR.DOWN + return True + + def set_zs_bi_list(zs, bis): + zs.bi_list = list(bis) + for bi in zs.bi_list: + bi.set_bi_zs(zs) + zs.set_gg(max(bi.high for bi in zs.bi_list)) + zs.set_dd(min(bi.low for bi in zs.bi_list)) + zs.classify_zs() + + last_zs = None + start_idx = 0 + while start_idx + 2 < len(bi_list): + bi1 = bi_list[start_idx] + bi2 = bi_list[start_idx + 1] + bi3 = bi_list[start_idx + 2] + + if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): + start_idx += 1 + continue + + if not (bi1.dir != bi2.dir and bi1.dir == bi3.dir): + start_idx += 1 + continue + + zg, zd = get_zs_range([bi1, bi2, bi3]) + if zg <= zd: + start_idx += 1 + continue + + bis_for_zs = [bi1, bi2, bi3] + extend_idx = start_idx + 3 + while extend_idx + 1 < len(bi_list): + leave_bi = bi_list[extend_idx] + back_bi = bi_list[extend_idx + 1] + if not (leave_bi.is_sure and back_bi.is_sure): + break + if not is_bi_overlap_range(back_bi, zg, zd): + break + bis_for_zs.append(leave_bi) + bis_for_zs.append(back_bi) + extend_idx += 2 + + if not check_zs_position_filter(last_zs, zg, zd, bis_for_zs): + start_idx += 1 + continue + + zs_dir = Chan_ZS_DIR.UP if bi1.dir == Chan_BI_DIR.DOWN else Chan_ZS_DIR.DOWN + zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + + set_zs_bi_list(zs, bis_for_zs) + zs.set_end_bi(bis_for_zs[-1], bis_for_zs[-1].sure_time) + + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + + bi_zs_list.append(zs) + last_zs = zs + start_idx = start_idx + len(bis_for_zs) + + # 与 cal_bi_zs_list 一致:最后一笔未确认时末中枢标为未完成;若其后已出现确认的离开笔,仍按离开前最后一笔确认中枢结束 + if last_zs: + last_zs.is_sure = bi_list[-1].is_sure + + if last_zs and not last_zs.is_sure: + if last_zs.bi_list and len(last_zs.bi_list) > 0: + last_bi_of_zs = last_zs.bi_list[-1] + last_bi_idx = -1 + for i, bi in enumerate(bi_list): + if bi == last_bi_of_zs: + last_bi_idx = i + break + + has_leave = False + if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): + for i in range(last_bi_idx + 1, len(bi_list)): + bi = bi_list[i] + if bi.is_sure: + leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ + (bi.high < last_zs.zd and bi.low < last_zs.zd) + if leave: + has_leave = True + break + + if has_leave: + if last_bi_of_zs.is_sure: + last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) + + return bi_zs_list def find_all_bsp(self, bi_list, bi_zs_list): """ 笔中枢的三类买卖点识别 diff --git a/config/ChanLun_BTC_5m.json b/config/ChanLun_BTC_5m.json new file mode 100644 index 0000000..9852629 --- /dev/null +++ b/config/ChanLun_BTC_5m.json @@ -0,0 +1,68 @@ +{ + "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.chanlun_btc_5m.sqlite", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "can_short" : true, + "timeframe" : "5m", + "process_only_new_candles" : false, + "unfilledtimeout": { + "entry": 5, + "exit": 5, + "exit_timeout_count": 5, + "unit": "minutes" + }, + "order_types": { + "entry": "limit", + "exit": "limit", + "stoploss": "limit", + "stoploss_on_exchange": false + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true + }, + "exchange": { + "name": "binance", + "key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8", + "secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l", + "ccxt_config": { + "proxies": { + "http": "http://127.0.0.1:7897", + "https": "http://127.0.0.1:7897" + } + }, + "ccxt_async_config": { + "aiohttp_proxy": "http://127.0.0.1:7897" + }, + "pair_whitelist": [ + "BTC/USDT:USDT" + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList", + "number_assets": 1, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/strategies/1分钟第三类买卖点.md b/strategies/1分钟第三类买卖点.md new file mode 100644 index 0000000..eaaf827 --- /dev/null +++ b/strategies/1分钟第三类买卖点.md @@ -0,0 +1,321 @@ +# 1分钟第三类买卖点策略 + +## 核心思路 + +只交易 1 分钟级别中枢之后确认完成的第三类买卖点。 + +- 第三类买点:价格向上离开 1 分钟中枢后,回拉笔低点不跌回中枢上沿,确认时做多。 +- 第三类卖点:价格向下离开 1 分钟中枢后,反弹笔高点不涨回中枢下沿,确认时做空。 +- 开单时机:第三类买卖点所在笔确认完成后,下一根 1 分钟 K 线开单,避免使用未确认信号。 + +## 初始量化参数 + +以下参数作为第一版回测基准,后续根据回测结果优化。 + +| 参数 | 初始值 | 说明 | +| --- | --- | --- | +| 基础周期 | 1m | 第三类买卖点识别周期 | +| 中枢算法 | 纯笔中枢 | 连续三笔重叠形成中枢,两个中枢允许相邻,不强制中间分割笔 | +| 大周期过滤 | 5m、15m | 用于判断趋势方向和过滤震荡 | +| ATR 周期 | 14 | 用于衡量离开力度、回抽深度和止损距离 | +| 成交量均线 | 20 | 用于判断离开放量和回抽缩量 | +| 最小中枢宽度 | 0.08% | 低于该值视为噪音中枢 | +| 最大中枢宽度 | 0.80% | 高于该值止损过宽,放弃交易 | +| 有效突破距离 | max(0.03%, 0.20 * ATR14 / close) | 离开中枢时收盘价需要超过边界的最小距离 | +| 离开笔最小幅度 | max(0.12%, 1.00 * ATR14 / close) | 过滤力度不足的离开笔 | +| 回抽最大距离 | 0.60 * ATR14 | 回抽/反弹离中枢边界太远时,不追单 | +| 离开放量 | volume >= 1.20 * volume_ma20 | 确认突破有主动资金 | +| 回抽缩量 | pullback_volume <= 0.90 * leave_volume | 确认回抽不是反向强攻击 | +| 最大止损距离 | 0.80% | 超过则放弃交易 | +| 最小止损距离 | 0.10% | 低于则容易被 1m 噪音扫损 | +| 单笔风险 | 0.5% - 1.0% | 每笔亏损控制在账户权益比例内 | +| 时间止损 | 8 根 1m K 线 | 开仓后 8 分钟仍未到 0.5R,主动减仓或平仓 | +| 连续失败暂停 | 2 次 | 连续 2 次三买/三卖失败后暂停 30 分钟 | + +## 信号有效条件 + +### 中枢要求 + +- 中枢必须已经确认,不能用正在形成中的中枢。 +- 使用 1 分钟纯笔中枢:连续三笔有重叠区间即可形成中枢,后续按两笔一组延伸。 +- 两个中枢可以在笔序列上直接相邻,不要求中间必须有独立分割笔。 +- 新中枢在旧中枢下方时,必须以向上笔开始并以向上笔结束,避免把下跌途中的弱反抽误当成有效下移中枢。 +- 新中枢在旧中枢上方时,必须以向下笔开始并以向下笔结束,避免把上涨途中的弱回踩误当成有效上移中枢。 +- 中枢宽度控制在 0.08% - 0.80% 之间,太小容易是假突破,太大导致止损距离过宽。 +- 优先选择结构清晰、震荡时间充分、上下沿明确的中枢。 +- 中枢层只做结构合法性判断,不因为成交量、离开力度、回抽质量等交易偏好直接删除中枢;这些质量条件放到买卖点确认和入场过滤中处理。 + +### 离开中枢要求 + +- 做多时,离开笔必须向上有效突破中枢上沿。 +- 做空时,离开笔必须向下有效跌破中枢下沿。 +- 有效突破要求收盘价至少超过中枢边界 max(0.03%, 0.20 * ATR14 / close)。 +- 离开笔幅度至少达到 max(0.12%, 1.00 * ATR14 / close)。 +- 离开笔成交量至少达到 1.20 * volume_ma20。 +- MACD 柱子方向需要和离开方向一致,做多时 macdhist > 0,做空时 macdhist < 0。 +- 如果离开中枢后很快又回到中枢内部,视为假突破,不开单。 + +### 回抽/反弹要求 + +- 做多时,回抽低点不能跌回中枢上沿下方。 +- 做空时,反弹高点不能涨回中枢下沿上方。 +- 回抽/反弹允许 0.15 * ATR14 的刺破容忍,避免被 1m 假刺破过滤掉。 +- 回抽/反弹距离中枢边界不能超过 0.60 * ATR14,超过说明已经追远。 +- 回抽/反弹成交量需要小于离开笔成交量的 90%。 +- 回抽/反弹 K 线数量建议控制在 2 - 8 根 1m K 线内,太短容易没确认,太长说明力度衰减。 + +## 行情过滤 + +### 震荡行情 + +震荡行情尽量不做第三类买卖点,因为 1 分钟级别假突破很多。 + +过滤方式: + +- 1 分钟只负责寻找第三类买卖点,5 分钟优先负责判断是否接受该信号。 +- 5 分钟和 15 分钟方向不一致时不做。 +- 5 分钟最近中枢仍在横向扩张、价格仍在 5 分钟中枢内部时,降低 1 分钟三买/三卖信号优先级,或直接不做突破类信号。 +- 做多信号优先要求 5 分钟中枢上移或价格位于 5 分钟中枢上沿附近/上方;做空信号优先要求 5 分钟中枢下移或价格位于 5 分钟中枢下沿附近/下方。 +- 价格反复穿越 EMA24/EMA52 时不做。 +- 中枢上下沿附近频繁出现假突破时不做。 +- 最近 30 分钟内出现 2 次同方向三买/三卖失败时,暂停该方向交易 30 分钟。 +- 最近 20 根 1m K 线内,收盘价穿越 EMA52 超过 4 次,视为震荡,不做。 +- ATR14 / close 低于 0.05% 时,波动不足,不做。 + +### 趋势开始阶段 + +趋势刚开始时的第一个有效三买/三卖优先级最高。 + +做多条件: + +- 5 分钟或 15 分钟开始转多,至少满足 close > EMA52。 +- 1 分钟向上离开中枢有力度。 +- 回抽不跌回中枢,且回抽缩量。 + +做空条件: + +- 5 分钟或 15 分钟开始转空,至少满足 close < EMA52。 +- 1 分钟向下离开中枢有力度。 +- 反弹不涨回中枢,且反弹缩量。 + +### 趋势中期 + +趋势中期可以继续做顺势三买/三卖,但需要提高过滤要求。 + +- 只做顺大周期方向的信号。 +- 做多时 5 分钟 close > EMA24 > EMA52,且 15 分钟 close > EMA52。 +- 做空时 5 分钟 close < EMA24 < EMA52,且 15 分钟 close < EMA52。 +- 如果止损距离超过 0.80%,放弃交易。 +- 趋势中期的同方向第二个及之后三买/三卖,仓位降为标准仓位的 50%。 + +### 趋势末期 + +趋势末期减少追单,重点防止三买买在高点、三卖卖在低点。 + +不交易条件: + +- 离开中枢时 MACD 或成交量明显背驰。 +- 已经连续出现多个同方向中枢上移/下移。 +- 出现反向第一类或第二类买卖点。 +- 价格远离 5 分钟 EMA52 超过 max(1.20%, 2.50 * ATR14 / close),短线加速过度。 +- 连续 3 个同方向中枢上移/下移后,不再追新的 1m 三买/三卖。 + +## 特殊点位处理 + +### 第一类和第二类买卖点之后 + +如果出现第一类或第二类买卖点后,行情没有继续确认反转,而是重新形成第三类买卖点: + +- 顺原趋势的第三类买卖点可以继续做,但必须确认反向一二类买卖点失败。 +- 如果一类/二类买卖点之后形成更大级别反转结构,不再做原方向三买/三卖。 +- 如果一类/二类买卖点和三类买卖点方向冲突,以大周期方向和最新确认结构为准。 + +### 反向信号 + +- 持有多单时出现确认的第三类卖点,平多;如果大周期也转空,可以反手做空。 +- 持有空单时出现确认的第三类买点,平空;如果大周期也转多,可以反手做多。 + +## 开仓规则 + +### 做多 + +同时满足以下条件才开多: + +- 出现确认后的 1 分钟第三类买点。 +- 5 分钟或 15 分钟趋势不为空头。 +- 价格没有重新跌回中枢内部。 +- 初始止损距离在可接受范围内。 +- 没有明显背驰或趋势末期信号。 +- 开仓价距离中枢上沿不超过 0.60 * ATR14。 +- 止损距离在 0.10% - 0.80% 之间。 + +### 做空 + +同时满足以下条件才开空: + +- 出现确认后的 1 分钟第三类卖点。 +- 5 分钟或 15 分钟趋势不为多头。 +- 价格没有重新涨回中枢内部。 +- 初始止损距离在可接受范围内。 +- 没有明显背驰或趋势末期信号。 +- 开仓价距离中枢下沿不超过 0.60 * ATR14。 +- 止损距离在 0.10% - 0.80% 之间。 + +## 信号失效 + +- 第三类买点确认后,价格重新跌回中枢上沿下方,信号失效。 +- 第三类卖点确认后,价格重新涨回中枢下沿上方,信号失效。 +- 开仓后 8 根 1 分钟 K 线仍未达到 0.5R,说明信号弱,可以主动减仓或平仓。 +- 开仓后 3 根 1 分钟 K 线内直接回到中枢内部,立即平仓。 +- 出现反向确认信号时,当前持仓失效。 + +## 止盈止损 + +### 止损 + +- 做多止损:放在中枢下沿,或第三类买点回抽低点下方。 +- 做空止损:放在中枢上沿,或第三类卖点反弹高点上方。 +- 止损需要额外留出 0.10 * ATR14 的缓冲,避免刚好打在结构边界。 +- 如果止损距离大于 0.80%,不开仓。 +- 如果止损距离小于 0.10%,按 0.10% 计算仓位风险,避免仓位过大。 +- 如果价格重新回到中枢内部,优先考虑提前止损,不等硬止损。 + +### 止盈 + +按照风险收益比管理: + +- 到达 1R 时平仓一半。 +- 到达 1R 后,剩余仓位止损移动到开仓价。 +- 到达 2R 时全部止盈。 +- 如果趋势特别强,可以在 2R 附近保留小仓位,用 EMA24 或前一笔低/高点跟踪止盈。 + +### 仓位 + +- 标准单笔风险控制在账户权益的 0.5% - 1.0%。 +- 趋势开始阶段使用标准仓位。 +- 趋势中期第二个及之后同方向三买/三卖使用 50% 标准仓位。 +- 趋势末期不主动开新仓。 + +## 参数优化方法 + +这些参数不能只看单次回测收益率,需要用历史数据做分阶段优化和样本外验证。 + +### 数据切分 + +建议至少使用 6 - 12 个月 1m 数据,按时间顺序切分,不能随机打乱。 + +- 训练集:前 60%,用于搜索参数。 +- 验证集:中间 20%,用于选择参数。 +- 测试集:最后 20%,只用于最终确认,不参与调参。 + +例如: + +- 2025-01 到 2025-06:训练集。 +- 2025-07 到 2025-08:验证集。 +- 2025-09 到 2025-10:测试集。 + +如果数据足够多,建议再做滚动验证: + +- 第 1 轮:1 - 3 月训练,4 月验证。 +- 第 2 轮:2 - 4 月训练,5 月验证。 +- 第 3 轮:3 - 5 月训练,6 月验证。 +- 只有多轮都稳定的参数,才认为有效。 + +### 优先优化的参数 + +不要一次优化太多参数,先优化最影响胜率和盈亏比的核心参数。 + +| 参数 | 搜索范围 | 步长 | 优化目的 | +| --- | --- | --- | --- | +| 最小中枢宽度 | 0.05% - 0.15% | 0.02% | 过滤噪音中枢 | +| 最大中枢宽度 | 0.50% - 1.20% | 0.10% | 控制止损距离 | +| 有效突破距离 | 0.10 - 0.40 * ATR14 | 0.05 | 过滤假突破 | +| 离开笔最小幅度 | 0.80 - 1.50 * ATR14 | 0.10 | 确认离开力度 | +| 回抽容忍幅度 | 0.05 - 0.25 * ATR14 | 0.05 | 避免过严或过松 | +| 回抽最大距离 | 0.40 - 0.90 * ATR14 | 0.10 | 避免追高追低 | +| 离开放量倍数 | 1.00 - 1.80 * volume_ma20 | 0.10 | 确认突破质量 | +| 回抽缩量比例 | 0.70 - 1.00 * leave_volume | 0.05 | 判断回抽是否健康 | +| 最大止损距离 | 0.50% - 1.20% | 0.10% | 控制单笔风险 | +| 时间止损 K 线数 | 5 - 15 根 | 1 | 处理无效信号 | + +第一轮只优化这些参数。大周期过滤、仓位、止盈方式先固定,否则容易过拟合。 + +### 优化目标 + +不要只按总收益选择参数。1 分钟策略噪音大,应该综合看: + +- 样本外收益为正。 +- 最大回撤尽量小。 +- Profit Factor 大于 1.20。 +- 胜率不低于 40%,如果胜率低,则平均盈亏比必须明显高于 1.5。 +- 单月交易次数不能太少,建议每月至少 20 笔,否则统计意义不足。 +- 多空两边不能严重失衡,除非策略明确只适合单边行情。 + +参数选择优先级: + +1. 样本外稳定性。 +2. 最大回撤。 +3. Profit Factor。 +4. 平均盈亏比。 +5. 总收益率。 + +### 防止过拟合 + +以下情况说明参数可能过拟合: + +- 训练集收益很好,验证集和测试集明显变差。 +- 只有某一个月表现很好,其他月份表现一般。 +- 参数落在搜索范围边界,例如最大止损距离优化后总是取最大值。 +- 交易次数太少,靠少数几笔大盈利撑起收益。 +- 多次微调后收益提升,但回撤和稳定性变差。 + +处理方式: + +- 选择参数平台区间,不选单个尖峰最优值。 +- 如果 0.20 * ATR、0.25 * ATR、0.30 * ATR 表现接近,优先选中间值。 +- 验证集表现比训练集差很多时,降低参数复杂度。 +- 每次只优化一组相关参数,例如先优化中枢和突破,再优化止损止盈。 + +### 推荐优化顺序 + +1. 先只测原始第三类买卖点,得到基准胜率和盈亏比。 +2. 加入中枢宽度过滤,观察交易次数和假突破是否下降。 +3. 加入离开力度和成交量过滤,优化胜率。 +4. 加入回抽质量过滤,减少追高追低。 +5. 加入大周期 EMA 过滤,观察震荡行情亏损是否下降。 +6. 优化止损距离和时间止损。 +7. 最后比较止盈方式:固定 2R、1R 减半 2R 全平、2R 后跟踪止盈。 + +每一步都要和上一步对比,只保留能提升样本外表现的过滤条件。 + +### 回测命令示例 + +先跑固定参数基准: + +```bash +freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20250101-20250630 +``` + +再按训练集、验证集、测试集分别跑: + +```bash +freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20250101-20250630 +freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20250701-20250831 +freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20250901-20251031 +``` + +如果后续把参数写成 Freqtrade 的可优化参数,可以使用 hyperopt 搜索核心参数,但最终仍然要用样本外测试集确认。 + +## 回测观察指标 + +回测时重点观察: + +- 三买和三卖分别的胜率。 +- 趋势开始、中期、末期三个阶段的收益差异。 +- 止损距离过大的交易是否拖累整体收益。 +- 震荡行情中过滤条件是否能减少假突破。 +- 1R 减半和 2R 全平是否优于一次性止盈。 + +## 策略总结 + +这套策略只做确认后的 1 分钟第三类买卖点,不提前猜测。1 分钟纯笔中枢负责保留足够完整的结构事实,允许相邻中枢连续出现;交易层再通过大周期方向、中枢宽度、离开力度、回抽质量和止损距离过滤掉低质量三买三卖。核心不是在中枢层过早删除结构,而是让 1 分钟找点、5 分钟定环境。 diff --git a/strategies/ChanLun_BTC_1m_old.py b/strategies/ChanLun_BTC_1m_old.py new file mode 100644 index 0000000..40f0d69 --- /dev/null +++ b/strategies/ChanLun_BTC_1m_old.py @@ -0,0 +1,213 @@ +# --- Do not remove these libs --- +from statistics import median +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, Chan_KLC_FX, Chan_BSP_TYPE +# -------------------------------- +from technical.util import resample_to_interval, resampled_merge +import talib.abstract as ta +from pandas import DataFrame +import pandas as pd +from datetime import datetime, timedelta +from typing import Optional +import logging +logger = logging.getLogger(__name__) +### Now you can use logger.info('asfd') to log +# freqtrade plot-dataframe --strategy ChanLun_BTC_1m --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309- + +# freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20260501- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_1m.json -t 1m 1m 1h 1d 1M --pairs BTC/USDT:USDT --timerange=20250405- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_1m.json -t 1m 1h 1d 1M --pairs BTC/USDT --timerange=20170101- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_1m.json -e 200 --timerange=20250201-20250901 +# freqtrade edge -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 +# freqtrade plot-dataframe -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 + +# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20250721- +# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_1m.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies + +class ChanLun_BTC_1m(IStrategy): + """ + 交易核心(缠论): + - 仅在缠论一/二/三类买卖点出现时交易。 + - 信号触发条件:前一笔被确认(bi.is_sure)时,该笔 end_klc 已被标记为 B1/B2/B3 或 S1/S2/S3。 + - 不使用未确认笔,不使用“状态猜测”列。 + """ + INTERFACE_VERSION: int = 3 + timeframe = '1m' + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "0": 100 + } + + can_short = True + enable_long = True + enable_short = False + lev = 1.0 + stoploss = -0.3 # 兜底止损,实际由 custom_stoploss 基于中枢 zg/zd 控制 + use_custom_stoploss = True + + trailing_stop = False + trailing_stop_positive = 0.03 + trailing_stop_positive_offset = 0.06 + trailing_only_offset_is_reached = False + use_exit_signal = True + position_adjustment_enable = True + startup_candle_count = 500 + # 以 1m 为基础周期时,1h = 60 根K线(用于读取 resample_60_* 列并做确认延迟) + chan = ChanLun() + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe = self.add_indicators(dataframe) + bsp_signal_data = self.chan.get_bsp_signal_data(dataframe) + for column, values in bsp_signal_data.items(): + dataframe[column] = values + return dataframe + def add_indicators(self, df): + df = self.add_base_indicators(df) + base_interval = self.get_ticker_indicator() + for interval in (5, 15, 60): + if interval <= base_interval: + df = self.copy_base_indicators_to_resample(df, interval) + continue + resampled = resample_to_interval(df, interval) + resampled = self.add_base_indicators(resampled) + df = resampled_merge(df, resampled) + return df + def copy_base_indicators_to_resample(self, df, interval): + prefix = f'resample_{interval}_' + for column in ( + 'date', 'open', 'high', 'low', 'close', 'volume', + 'atr', 'macd', 'macdsignal', 'macdhist', 'ema24', 'ema52', + 'atr_ratio', 'resistance_240', 'support_240', 'trend' + ): + if column in df.columns: + df[f'{prefix}{column}'] = df[column] + return df + def add_base_indicators(self, df): + fast = 12 + slow = 26 + period = 9 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + df['atr'] = ta.ATR(df, timeperiod=14) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ema24'] = ta.EMA(df, timeperiod=24) + df['ema52'] = ta.EMA(df, timeperiod=52) + df['atr_ratio'] = df['atr'] / df['close'] + df['resistance_240'] = df['high'].rolling(240).max().shift(1) + df['support_240'] = df['low'].rolling(240).min().shift(1) + df['trend'] = 0 + df.loc[(df['close'] > df['ema52']) & (df['ema24'] >= df['ema52']), 'trend'] = 1 + df.loc[(df['close'] < df['ema52']) & (df['ema24'] <= df['ema52']), 'trend'] = -1 + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + min_atr_ratio = 0.0005 + long_min_sr_distance_r = 1.0 + short_min_sr_distance_r = 0.8 + long_space_ratio = (dataframe['resistance_240'].shift(1) - dataframe['close'].shift(1)) / dataframe['close'].shift(1) + short_space_ratio = (dataframe['close'].shift(1) - dataframe['support_240'].shift(1)) / dataframe['close'].shift(1) + # 多周期趋势共振:3个周期中至少2个同向(而非全部3个) + long_tf_aligned = ( + (dataframe['resample_5_trend'].shift(1) == 1).astype(int) + + (dataframe['resample_15_trend'].shift(1) == 1).astype(int) + + (dataframe['resample_60_trend'].shift(1) == 1).astype(int) + ) >= 2 + short_tf_aligned = ( + (dataframe['resample_5_trend'].shift(1) == -1).astype(int) + + (dataframe['resample_15_trend'].shift(1) == -1).astype(int) + + (dataframe['resample_60_trend'].shift(1) == -1).astype(int) + ) >= 2 + dataframe.loc[ + ( + self.enable_long & + (dataframe['bsp_state'].shift(1) == -1) & + (dataframe['bsp_risk_ratio'].shift(1) > 0) & + (long_space_ratio >= dataframe['bsp_risk_ratio'].shift(1) * long_min_sr_distance_r) & + (dataframe['macdhist'].shift(1) > 0) & + (dataframe['atr_ratio'].shift(1) >= min_atr_ratio) & + (dataframe['trend'].shift(1) == 1) & + long_tf_aligned + ), + ['enter_long', 'enter_tag']] = (1, 'long_signal_chan') + dataframe.loc[ + ( + self.enable_short & + (dataframe['bsp_state'].shift(1) == 1) & + (dataframe['bsp_risk_ratio'].shift(1) > 0) & + (short_space_ratio >= dataframe['bsp_risk_ratio'].shift(1) * short_min_sr_distance_r) & + (dataframe['macdhist'].shift(1) < 0) & + (dataframe['atr_ratio'].shift(1) >= min_atr_ratio) & + (dataframe['trend'].shift(1) == -1) & + short_tf_aligned + ), + ['enter_short', 'enter_tag']] = (1, 'short_signal_chan') + return dataframe + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['exit_long'] = 0 + dataframe['exit_short'] = 0 + return dataframe + def get_trade_risk_ratio(self, pair: str, trade) -> float: + risk_ratio = trade.get_custom_data('risk_ratio') + if risk_ratio: + return float(risk_ratio) + + risk_ratio = 0.001 + try: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if len(dataframe) > 0: + entry_rows = dataframe[dataframe['date'] <= trade.open_date_utc] + entry_candle = entry_rows.iloc[-1] if len(entry_rows) > 0 else dataframe.iloc[-1] + signal_rows = entry_rows.tail(3) + signal_rows = signal_rows[signal_rows['bsp_risk_ratio'] > 0] + if len(signal_rows) > 0: + signal_candle = signal_rows.iloc[-1] + risk_ratio = float(signal_candle['bsp_risk_ratio']) + trade.set_custom_data('bsp_stop_price', float(signal_candle['bsp_stop_price'])) + trade.set_custom_data('bsp_zg', float(signal_candle['bsp_zg'])) + trade.set_custom_data('bsp_zd', float(signal_candle['bsp_zd'])) + else: + risk_ratio = max(0.001, min(float(entry_candle['atr_ratio']), 0.005)) + except Exception: + risk_ratio = 0.001 + + trade.set_custom_data('risk_ratio', risk_ratio) + return risk_ratio + def adjust_trade_position(self, trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: float | None, max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs): + risk_ratio = self.get_trade_risk_ratio(trade.pair, trade) + if current_profit >= risk_ratio and trade.nr_of_successful_exits == 0: + return -(trade.stake_amount / 2), 'take_half_1r' + return None + def custom_exit(self, pair: str, trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs): + risk_ratio = self.get_trade_risk_ratio(pair, trade) + if trade.nr_of_successful_exits > 0 and current_profit <= 0.001: + return 'breakeven_after_1r' + if current_profit >= risk_ratio * 2: + return 'take_profit_2r' + return None + def custom_stoploss(self, pair: str, trade, current_time: datetime, current_rate: float, + current_profit: float, after_fill: bool, **kwargs) -> float | None: + bsp_stop_price = trade.get_custom_data('bsp_stop_price') + if bsp_stop_price: + sl = stoploss_from_absolute(float(bsp_stop_price), current_rate, is_short=trade.is_short) + return min(sl, -0.05) + return -0.05 + 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): + return int(self.timeframe[:-1]) \ No newline at end of file diff --git a/strategies/ChanLun_BTC_5m.py b/strategies/ChanLun_BTC_5m.py new file mode 100644 index 0000000..027c670 --- /dev/null +++ b/strategies/ChanLun_BTC_5m.py @@ -0,0 +1,201 @@ +# --- Do not remove these libs --- +from statistics import median +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, Chan_KLC_FX, Chan_BSP_TYPE +# -------------------------------- +from technical.util import resample_to_interval, resampled_merge +import talib.abstract as ta +from pandas import DataFrame +import pandas as pd +from datetime import datetime, timedelta +from typing import Optional +import logging +logger = logging.getLogger(__name__) + +class ChanLun_BTC_5m(IStrategy): + """ChanLun_BTC_5m: 5m B3 signals with trailing stop exit.""" + + INTERFACE_VERSION: int = 3 + timeframe = '5m' + minimal_roi = {"0": 100} + + can_short = True + enable_long = True + enable_short = False + lev = 1.0 + stoploss = -0.3 + use_custom_stoploss = True + + trailing_stop = False + use_exit_signal = True + position_adjustment_enable = False + startup_candle_count = 500 + chan = ChanLun() + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe = self.add_indicators(dataframe) + bsp_signal_data = self.chan.get_bsp_signal_data(dataframe) + for column, values in bsp_signal_data.items(): + dataframe[column] = values + return dataframe + + def add_indicators(self, df): + df = self.add_base_indicators(df) + base_interval = self.get_ticker_indicator() + for interval in (5, 15, 60): + if interval <= base_interval: + df = self.copy_base_indicators_to_resample(df, interval) + continue + resampled = resample_to_interval(df, interval) + resampled = self.add_base_indicators(resampled) + df = resampled_merge(df, resampled) + return df + + def copy_base_indicators_to_resample(self, df, interval): + prefix = f'resample_{interval}_' + for column in ( + 'date', 'open', 'high', 'low', 'close', 'volume', + 'atr', 'macd', 'macdsignal', 'macdhist', 'ema24', 'ema52', + 'atr_ratio', 'resistance_240', 'support_240', 'trend' + ): + if column in df.columns: + df[f'{prefix}{column}'] = df[column] + return df + + def add_base_indicators(self, df): + fast = 12 + slow = 26 + period = 9 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + df['atr'] = ta.ATR(df, timeperiod=14) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ema24'] = ta.EMA(df, timeperiod=24) + df['ema52'] = ta.EMA(df, timeperiod=52) + df['atr_ratio'] = df['atr'] / df['close'] + df['resistance_240'] = df['high'].rolling(240).max().shift(1) + df['support_240'] = df['low'].rolling(240).min().shift(1) + df['trend'] = 0 + df.loc[(df['close'] > df['ema52']) & (df['ema24'] >= df['ema52']), 'trend'] = 1 + df.loc[(df['close'] < df['ema52']) & (df['ema24'] <= df['ema52']), 'trend'] = -1 + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + min_atr_ratio = 0.0005 + long_min_sr_distance_r = 1.0 + short_min_sr_distance_r = 0.8 + long_space_ratio = (dataframe['resistance_240'].shift(1) - dataframe['close'].shift(1)) / dataframe['close'].shift(1) + short_space_ratio = (dataframe['close'].shift(1) - dataframe['support_240'].shift(1)) / dataframe['close'].shift(1) + long_tf_aligned = ( + (dataframe['resample_5_trend'].shift(1) == 1).astype(int) + + (dataframe['resample_15_trend'].shift(1) == 1).astype(int) + + (dataframe['resample_60_trend'].shift(1) == 1).astype(int) + ) >= 2 + short_tf_aligned = ( + (dataframe['resample_5_trend'].shift(1) == -1).astype(int) + + (dataframe['resample_15_trend'].shift(1) == -1).astype(int) + + (dataframe['resample_60_trend'].shift(1) == -1).astype(int) + ) >= 2 + dataframe.loc[ + ( + self.enable_long & + (dataframe['bsp_state'].shift(1) == -1) & + (dataframe['bsp_risk_ratio'].shift(1) > 0) & + (long_space_ratio >= dataframe['bsp_risk_ratio'].shift(1) * long_min_sr_distance_r) & + (dataframe['macdhist'].shift(1) > 0) & + (dataframe['atr_ratio'].shift(1) >= min_atr_ratio) & + (dataframe['trend'].shift(1) == 1) & + long_tf_aligned + ), + ['enter_long', 'enter_tag']] = (1, 'long_signal_chan') + dataframe.loc[ + ( + self.enable_short & + (dataframe['bsp_state'].shift(1) == 1) & + (dataframe['bsp_risk_ratio'].shift(1) > 0) & + (short_space_ratio >= dataframe['bsp_risk_ratio'].shift(1) * short_min_sr_distance_r) & + (dataframe['macdhist'].shift(1) < 0) & + (dataframe['atr_ratio'].shift(1) >= min_atr_ratio) & + (dataframe['trend'].shift(1) == -1) & + short_tf_aligned + ), + ['enter_short', 'enter_tag']] = (1, 'short_signal_chan') + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['exit_long'] = 0 + dataframe['exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs): + # Time-based exit only - trailing stop handles profit taking + elapsed = current_time - trade.open_date_utc + if elapsed >= timedelta(hours=72) and current_profit < 0.005: + return 'time_stop_72h' + return None + + def custom_stoploss(self, pair: str, trade, current_time: datetime, current_rate: float, + current_profit: float, after_fill: bool, **kwargs) -> float | None: + # Initialize stored state + if not trade.get_custom_data('trail_activated'): + trade.set_custom_data('trail_activated', False) + trade.set_custom_data('max_profit', 0.0) + # Read bsp_stop_price from signal + try: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if len(dataframe) > 0: + entry_rows = dataframe[dataframe['date'] <= trade.open_date_utc] + signal_rows = entry_rows.tail(3) + signal_rows = signal_rows[signal_rows['bsp_risk_ratio'] > 0] + if len(signal_rows) > 0: + signal_candle = signal_rows.iloc[-1] + trade.set_custom_data('bsp_stop_price', float(signal_candle['bsp_stop_price'])) + except Exception: + pass + + max_profit = max(float(trade.get_custom_data('max_profit')), current_profit) + trade.set_custom_data('max_profit', max_profit) + trail_activated = trade.get_custom_data('trail_activated') + + # Stage 1: Initial stop at bsp_stop with -5% floor + if not trail_activated: + if max_profit >= 0.02: + # Activate trail: move stop to breakeven + trade.set_custom_data('trail_activated', True) + sl = stoploss_from_absolute(trade.open_rate, current_rate, is_short=trade.is_short) + return max(sl, -0.005) + else: + bsp_stop = trade.get_custom_data('bsp_stop_price') + if bsp_stop: + sl = stoploss_from_absolute(float(bsp_stop), current_rate, is_short=trade.is_short) + return min(sl, -0.05) + return -0.05 + else: + # Stage 2: Trail from max profit + if max_profit >= 0.04: + trail_offset = 0.02 # Trail 2% behind max + trail_price = trade.open_rate * (1 + max_profit - trail_offset) + sl = stoploss_from_absolute(trail_price, current_rate, is_short=trade.is_short) + return max(sl, -0.02) + elif max_profit >= 0.02: + # Breakeven to 1% trail + sl = stoploss_from_absolute(trade.open_rate * 1.005, current_rate, is_short=trade.is_short) + return max(sl, -0.005) + else: + sl = stoploss_from_absolute(trade.open_rate * 0.998, current_rate, is_short=trade.is_short) + return max(sl, -0.02) + + 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): + return int(self.timeframe[:-1]) diff --git a/strategies/Template.py b/strategies/Template.py new file mode 100644 index 0000000..1840e66 --- /dev/null +++ b/strategies/Template.py @@ -0,0 +1,133 @@ +# --- Do not remove these libs --- +from statistics import median +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, Chan_KLC_FX, Chan_BSP_TYPE +# -------------------------------- +from technical.util import resample_to_interval, resampled_merge +import talib.abstract as ta +from pandas import DataFrame +import pandas as pd +from datetime import datetime, timedelta +from typing import Optional +import logging +logger = logging.getLogger(__name__) +### Now you can use logger.info('asfd') to log +# freqtrade plot-dataframe --strategy ChanLun_BTC_1m --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309- + +# freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20260501- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_1m.json -t 1m 1m 1h 1d 1M --pairs BTC/USDT:USDT --timerange=20250405- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_1m.json -t 1m 1h 1d 1M --pairs BTC/USDT --timerange=20170101- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_1m.json -e 200 --timerange=20250201-20250901 +# freqtrade edge -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 +# freqtrade plot-dataframe -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 + +# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies --timerange=20250721- +# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_1m.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies + +class Template(IStrategy): + """ + 交易核心(缠论): + - 仅在缠论一/二/三类买卖点出现时交易。 + - 信号触发条件:前一笔被确认(bi.is_sure)时,该笔 end_klc 已被标记为 B1/B2/B3 或 S1/S2/S3。 + - 不使用未确认笔,不使用“状态猜测”列。 + """ + INTERFACE_VERSION: int = 3 + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + # 30m and 1h + + minimal_roi = { + "0": 0.05, + "60": 0.03, + "120": 0.01, + "180": 0 + } + # 5m and 15m + minimal_roi_1 = { + "0": 0.1, + "60": 0.05, + "120": 0.02, + "240": 0 + } + # 15m and 30m + minimal_roi_1 = { + "0": 0.1, + "240": 0.05, + "480": 0.03, + "600": 0 + } + minimal_roi_1 = { + "0": 1.50, + "120": 0.05, + "240": 0.025, + "360": 0 + } + + can_short = True + lev = 1.0 + stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制 + + trailing_stop = False + trailing_stop_positive = 0.03 + trailing_stop_positive_offset = 0.06 + trailing_only_offset_is_reached = False + + # 关闭分批止盈/仓位调整 + startup_candle_count = 500 + # 以 1m 为基础周期时,1h = 60 根K线(用于读取 resample_60_* 列并做确认延迟) + chan = ChanLun() + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe = self.add_indicators(dataframe) + dataframe['bsp_state'] = self.chan.get_bsp_state(dataframe) + return dataframe + def add_indicators(self, df): + fast = 12 + slow = 26 + period = 9 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + df['atr'] = ta.ATR(df, timeperiod=14) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ema24'] = ta.EMA(df, timeperiod=24) + df['ema52'] = ta.EMA(df, timeperiod=52) + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe['bsp_state'].shift(1) == -1) + ), + ['enter_long', 'enter_tag']] = (1, 'long_signal_chan') + dataframe.loc[ + ( + (dataframe['bsp_state'].shift(1) == 1) + ), + ['enter_short', 'enter_tag']] = (1, 'short_signal_chan') + return dataframe + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # 出场和进场共用同一套“确认笔 + end_klc 买卖点”语义。 + dataframe.loc[ + ( + (dataframe['bsp_state'].shift(1) == 1) + ), + ['exit_long', 'exit_tag']] = (1, 'long_signal_chan') + dataframe.loc[ + ( + (dataframe['bsp_state'].shift(1) == -1) + ), + ['exit_short', 'exit_tag']] = (1, 'short_signal_chan') + return dataframe + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, + **kwargs) -> float: + return self.lev + def get_ticker_indicator(self): + return int(self.timeframe[:-1]) \ No newline at end of file diff --git a/web/app.py b/web/app.py index c2d409f..9915639 100644 --- a/web/app.py +++ b/web/app.py @@ -559,8 +559,8 @@ def analyze_chan(df, symbol=None, timeframe=None): zs_list = chan.calculate_seg_zs(seg_list) # 计算笔中枢(BI中枢)并拍平成列表 - #bi_zs_list = chan.cal_bi_zs_list(bi_list) - bi_zs_list = chan.cal_bi_zs(seg_list) + bi_zs_list = chan.cal_bi_zs_list_pure(bi_list) + #bi_zs_list = chan.cal_bi_zs(seg_list) bsp_list = [] if len(bi_zs_list) > 0: bsp_list = chan.find_all_bsp(bi_list, bi_zs_list) @@ -1806,113 +1806,118 @@ def analyze(): pass - # 结构价值区分析(Structure Zone)—— 独立拉取多周期数据,缓存避免重复请求 - zone_timeframes_str = request.args.get('zone_timeframes', '') - zone_kl_lines = int(request.args.get('zone_kl_lines', 1000)) - try: - zone_config = StructureZoneConfig(kl_lines_per_tf=zone_kl_lines) - if zone_timeframes_str: - zone_config.zone_timeframes = [t.strip() for t in zone_timeframes_str.split(',') if t.strip()] - analyses = {} - ema52_dict = {} - latest_close = 0.0 - now = time.time() + # 结构价值区分析(Structure Zone)—— 按需拉取:仅当 include_structure_zones 为真时执行多周期拉取(默认跳过以减轻负载) + include_zones_param = request.args.get('include_structure_zones', '') + include_structure_zones = str(include_zones_param).lower() in ('1', 'true', 'yes') + if include_structure_zones: + zone_timeframes_str = request.args.get('zone_timeframes', '') + zone_kl_lines = int(request.args.get('zone_kl_lines', 1000)) + try: + zone_config = StructureZoneConfig(kl_lines_per_tf=zone_kl_lines) + if zone_timeframes_str: + zone_config.zone_timeframes = [t.strip() for t in zone_timeframes_str.split(',') if t.strip()] + analyses = {} + ema52_dict = {} + latest_close = 0.0 + now = time.time() - def _fetch_single_tf_zone(tf_name): - """单个时间周期的结构区数据拉取(线程安全)""" - cache_key = f"{symbol}:{tf_name}:{zone_kl_lines}" - cached = _zone_cache.get(cache_key) - if cached and cached['expires'] > now: - print(f" 结构区缓存命中: {tf_name}") - return { - 'tf_name': tf_name, - 'analyses': cached['analyses'], - 'ema52': cached['ema52'], - 'close': cached.get('close', 0.0), - 'cached': True, - } + def _fetch_single_tf_zone(tf_name): + """单个时间周期的结构区数据拉取(线程安全)""" + cache_key = f"{symbol}:{tf_name}:{zone_kl_lines}" + cached = _zone_cache.get(cache_key) + if cached and cached['expires'] > now: + print(f" 结构区缓存命中: {tf_name}") + return { + 'tf_name': tf_name, + 'analyses': cached['analyses'], + 'ema52': cached['ema52'], + 'close': cached.get('close', 0.0), + 'cached': True, + } - try: - tf_df = get_kl_data(symbol, tf_name, limit=zone_kl_lines) - if tf_df is None or len(tf_df) == 0: + try: + tf_df = get_kl_data(symbol, tf_name, limit=zone_kl_lines) + if tf_df is None or len(tf_df) == 0: + return None + tf_df = add_indicators(tf_df) + tf_analysis = analyze_chan(tf_df, symbol, tf_name) + zs_serialized = [{ + 'start_time': (zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) if zs.start_klc else None, + 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, + 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, + 'is_sure': zs.is_sure + } for zs in tf_analysis.get('zs_list', []) if zs.is_sure] + bi_zs_serialized = [{ + 'start_time': ((zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())), + 'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None, + 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, + 'is_sure': bool(getattr(zs, 'is_sure', False)) + } for zs in tf_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)] + last_ema = tf_df['ema52'].iloc[-1] if 'ema52' in tf_df.columns else 0 + ema_val = float(last_ema) if last_ema and last_ema > 0 else None + last_close = float(tf_df['close'].iloc[-1]) + tf_result = { + 'tf_name': tf_name, + 'analyses': {'zs_list': zs_serialized, 'bi_zs_list': bi_zs_serialized}, + 'ema52': ema_val, + 'close': last_close, + 'cached': False, + } + # 写入缓存 + _zone_cache[cache_key] = { + 'analyses': tf_result['analyses'], + 'ema52': ema_val, + 'close': last_close, + 'expires': now + _zone_cache_ttl(tf_name), + } + print(f" 结构区数据: {tf_name} -> zs={len(zs_serialized)}, bi_zs={len(bi_zs_serialized)}, ema52={ema_val}") + return tf_result + except Exception as e: + print(f" 结构区 {tf_name} 拉取失败: {e}") return None - tf_df = add_indicators(tf_df) - tf_analysis = analyze_chan(tf_df, symbol, tf_name) - zs_serialized = [{ - 'start_time': (zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) if zs.start_klc else None, - 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, - 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, - 'is_sure': zs.is_sure - } for zs in tf_analysis.get('zs_list', []) if zs.is_sure] - bi_zs_serialized = [{ - 'start_time': ((zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())), - 'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None, - 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, - 'is_sure': bool(getattr(zs, 'is_sure', False)) - } for zs in tf_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)] - last_ema = tf_df['ema52'].iloc[-1] if 'ema52' in tf_df.columns else 0 - ema_val = float(last_ema) if last_ema and last_ema > 0 else None - last_close = float(tf_df['close'].iloc[-1]) - tf_result = { - 'tf_name': tf_name, - 'analyses': {'zs_list': zs_serialized, 'bi_zs_list': bi_zs_serialized}, - 'ema52': ema_val, - 'close': last_close, - 'cached': False, - } - # 写入缓存 - _zone_cache[cache_key] = { - 'analyses': tf_result['analyses'], - 'ema52': ema_val, - 'close': last_close, - 'expires': now + _zone_cache_ttl(tf_name), - } - print(f" 结构区数据: {tf_name} -> zs={len(zs_serialized)}, bi_zs={len(bi_zs_serialized)}, ema52={ema_val}") - return tf_result - except Exception as e: - print(f" 结构区 {tf_name} 拉取失败: {e}") - return None - with ThreadPoolExecutor(max_workers=len(zone_config.zone_timeframes)) as executor: - futures = {executor.submit(_fetch_single_tf_zone, tf): tf for tf in zone_config.zone_timeframes} - for future in as_completed(futures): - tf_result = future.result() - if tf_result is None: - continue - tf_name = tf_result['tf_name'] - analyses[tf_name] = tf_result['analyses'] - ema52_dict[tf_name] = tf_result['ema52'] - if tf_result['close'] and (not latest_close or latest_close == 0.0): - latest_close = tf_result['close'] + with ThreadPoolExecutor(max_workers=len(zone_config.zone_timeframes)) as executor: + futures = {executor.submit(_fetch_single_tf_zone, tf): tf for tf in zone_config.zone_timeframes} + for future in as_completed(futures): + tf_result = future.result() + if tf_result is None: + continue + tf_name = tf_result['tf_name'] + analyses[tf_name] = tf_result['analyses'] + ema52_dict[tf_name] = tf_result['ema52'] + if tf_result['close'] and (not latest_close or latest_close == 0.0): + latest_close = tf_result['close'] - structure_zones = analyze_structure_zones_from_serialized( - analyses, ema52_dict, latest_close, config=zone_config - ) - result['structure_zones'] = [{ - 'id': z.id, - 'lower': z.lower, - 'upper': z.upper, - 'center': z.center, - 'width_pct': z.width_pct, - 'zone_type': z.zone_type, - 'timeframes': z.timeframes, - 'structure_types': z.structure_types, - 'boundary_types': z.boundary_types, - 'overlap_count': z.overlap_count, - 'touch_count': z.touch_count, - 'recency_score': z.recency_score, - 'ema52_distance_pct': z.ema52_distance_pct, - 'ema52_aligned': z.ema52_aligned, - 'strength_score': z.strength_score, - 'confidence': z.confidence, - 'first_seen': z.first_seen, - 'last_seen': z.last_seen, - 'metadata': z.metadata, - } for z in structure_zones] - except Exception as e: - print(f"StructureZone 分析出错: {e}") - import traceback - traceback.print_exc() + structure_zones = analyze_structure_zones_from_serialized( + analyses, ema52_dict, latest_close, config=zone_config + ) + result['structure_zones'] = [{ + 'id': z.id, + 'lower': z.lower, + 'upper': z.upper, + 'center': z.center, + 'width_pct': z.width_pct, + 'zone_type': z.zone_type, + 'timeframes': z.timeframes, + 'structure_types': z.structure_types, + 'boundary_types': z.boundary_types, + 'overlap_count': z.overlap_count, + 'touch_count': z.touch_count, + 'recency_score': z.recency_score, + 'ema52_distance_pct': z.ema52_distance_pct, + 'ema52_aligned': z.ema52_aligned, + 'strength_score': z.strength_score, + 'confidence': z.confidence, + 'first_seen': z.first_seen, + 'last_seen': z.last_seen, + 'metadata': z.metadata, + } for z in structure_zones] + except Exception as e: + print(f"StructureZone 分析出错: {e}") + import traceback + traceback.print_exc() + result['structure_zones'] = [] + else: result['structure_zones'] = [] return jsonify(result) diff --git a/web/templates/index.html b/web/templates/index.html index 884d798..4f3cc84 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1918,8 +1918,14 @@ }); // 结构价值区复选框变更事件 $(document).on('change', '#showMainStructureZone', function() { - console.log('结构区切换为:', $('#showMainStructureZone').is(':checked')); - updateChartDisplay(); + const on = $('#showMainStructureZone').is(':checked'); + console.log('结构区切换为:', on); + // 勾选后才向服务器请求多周期结构区数据;取消勾选仅重绘,不重复拉取 + if (on) { + updateChart(); + } else { + updateChartDisplay(); + } }); // 添加趋势显示复选框变更事件(主/元素),变更后刷新主图 @@ -2190,7 +2196,8 @@ start_time: startTimeMs, end_time: endTimeMs, elements_only: false, - zone_kl_lines: parseInt($('#zoneKlLines').val()) || 1000 + zone_kl_lines: parseInt($('#zoneKlLines').val()) || 1000, + include_structure_zones: $('#showMainStructureZone').is(':checked') ? 1 : 0 }, success: function(data) { // 隐藏加载图标