From 43bd225cfa167dab2c3d177efaf1afef5500840c Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Wed, 20 Aug 2025 16:40:33 +0800 Subject: [PATCH] optimize trade strategy --- auto_update_market_data.py | 29 +++ core/statistics/ma_break_statistics.py | 4 +- .../mean_reversion_sandbox.cpython-312.pyc | Bin 17233 -> 21869 bytes core/trade/mean_reversion_sandbox.py | 205 +++++++++++++++--- requirements.txt | 6 +- trade_sandbox_main.py | 51 ++++- 6 files changed, 246 insertions(+), 49 deletions(-) create mode 100644 auto_update_market_data.py diff --git a/auto_update_market_data.py b/auto_update_market_data.py new file mode 100644 index 0000000..0e880ee --- /dev/null +++ b/auto_update_market_data.py @@ -0,0 +1,29 @@ +import schedule +import time +import datetime +import core.logger as logging +import subprocess +import os + +logger = logging.logger +# 定义要执行的任务 +def run_script(): + start_time = time.time() + logger.info(f"Executing script at: {datetime.datetime.now()}") + output_file = r'./output/auto_schedule.txt' + with open(output_file, 'a') as f: + f.write(f"Task ran at {datetime.datetime.now()}\n") + python_path = r"D:\miniconda3\envs\okx\python.exe" + script_path = r"D:\python_projects\crypto_quant\huge_volume_main.py" + subprocess.run([python_path, script_path]) + end_time = time.time() + logger.info(f"Script execution time: {end_time - start_time} seconds") +# 设置每小时运行一次 +interval = 60 * 60 +schedule.every(interval).seconds.do(run_script) + +# 保持程序运行并检查调度 +logger.info("Scheduler started. Press Ctrl+C to stop.") +while True: + schedule.run_pending() + time.sleep(1) \ No newline at end of file diff --git a/core/statistics/ma_break_statistics.py b/core/statistics/ma_break_statistics.py index c4b156d..f636b58 100644 --- a/core/statistics/ma_break_statistics.py +++ b/core/statistics/ma_break_statistics.py @@ -171,9 +171,9 @@ class MaBreakStatistics: ma_cross = str(ma_cross) buy_condition = False if all_change: - buy_condition = (ma5 > ma10 and ma10 > ma20 and ma20 > ma30) and (close > ma20) + buy_condition = (ma_cross == "5上穿10") and (ma5 > ma10 and ma10 > ma20 and ma20 > ma30) and (close > ma20) else: - buy_condition = ma_cross == "5上穿10" and (ma5 > ma10) + buy_condition = (ma_cross == "5上穿10") and (ma5 > ma10) if buy_condition: ma_break_market_data_pair = {} ma_break_market_data_pair["symbol"] = symbol diff --git a/core/trade/__pycache__/mean_reversion_sandbox.cpython-312.pyc b/core/trade/__pycache__/mean_reversion_sandbox.cpython-312.pyc index 8b6173570ae44f4df197c51efebdf510b2b80f77..88faf8a6c0abd143f64f3b101c123d431305a123 100644 GIT binary patch delta 7894 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 Dn5RC- diff --git a/core/trade/mean_reversion_sandbox.py b/core/trade/mean_reversion_sandbox.py index 022203c..bfd0561 100644 --- a/core/trade/mean_reversion_sandbox.py +++ b/core/trade/mean_reversion_sandbox.py @@ -4,7 +4,7 @@ import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns -from datetime import datetime +from datetime import datetime, timedelta import re from openpyxl import Workbook from openpyxl.drawing.image import Image @@ -45,21 +45,22 @@ class MeanReversionSandbox: desc_dict = { "买入": [ "1. 窗口周期为100, 即100个K线", - "2. 当前low_10_low为1, 即当前最低价格在窗口周期的10分位以下", + "2. 当前close_10_low为1, 即当前收盘价在窗口周期的10分位以下", "3. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量", - "4. 当前K线为阳线, 即close > open", + "4. 当前K线为阳线, 即close > open或者K线为一字, 长倒T线, 倒T线, 长十字星, 十字星", + # "5. 相同symbol的1H当前周期, ma5大于ma10", + # "5. KDJ, RSI, BOLL任意一个指标出现超卖", ], "止损": ["跌幅超过下跌周期跌幅中位数, 即down_median后卖出"], "止盈": { "solution_1": [ "高位放量止盈 - 简易版", - "1. 当前high_80_high为1或者high_90_high为1", + "1. 当前close_80_high为1或者close_90_high为1", "2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量", ], "solution_2": [ "高位放量止盈 - 复杂版", - "前提条件" - "1. 当前high_80_high为1或者high_90_high为1", + "前提条件" "1. 当前close_80_high为1或者close_90_high为1", "2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量", "以下两个条件, 任一满足即可", "1. K线为阴线, 即close < open", @@ -68,9 +69,10 @@ class MeanReversionSandbox: ], "solution_3": [ "上涨波段盈利中位数止盈法", - "1. 超过波段中位数涨幅, 即up_median后, 记录当前价格, 继续持仓", + "1. 超过波段中位数涨幅, 即up_median/ 到达价位90分位/ 任意技术指标出现中度超卖, 记录当前价格, 继续持仓", "2. 之后一个周期, 如果价格上涨, 则记录该价格继续持仓", "3. 之后一个周期, 如果价格跌到记录价格之下, 则卖出", + "4. 如果买入时ma5小于ma10, 过程中ma5大于ma10, 进行记录。之后出现ma5小于ma10, 则卖出", ], }, } @@ -160,7 +162,8 @@ class MeanReversionSandbox: trade_pair_dict["buy_huge_volume"] = row["huge_volume"] trade_pair_dict["buy_volume_ratio"] = row["volume_ratio"] trade_pair_dict["buy_k_shape"] = row["k_shape"] - trade_pair_dict["buy_low_10_low"] = row["low_10_low"] + trade_pair_dict["buy_close_10_low"] = row["close_10_low"] + trade_pair_dict["buy_ma5_lt_ma10"] = row["ma5"] < row["ma10"] continue if trade_pair_dict.get("buy_timestamp", None) is not None: @@ -188,22 +191,27 @@ class MeanReversionSandbox: trade_pair_dict["sell_huge_volume"] = row["huge_volume"] trade_pair_dict["sell_volume_ratio"] = row["volume_ratio"] trade_pair_dict["sell_k_shape"] = row["k_shape"] - trade_pair_dict["sell_high_80_high"] = row["high_80_high"] - trade_pair_dict["sell_high_90_high"] = row["high_90_high"] - trade_pair_dict["sell_low_10_low"] = row["low_10_low"] - trade_pair_dict["sell_low_20_low"] = row["low_20_low"] + trade_pair_dict["sell_close_80_high"] = row["close_80_high"] + trade_pair_dict["sell_close_90_high"] = row["close_90_high"] + trade_pair_dict["sell_close_10_low"] = row["close_10_low"] + trade_pair_dict["sell_close_20_low"] = row["close_20_low"] trade_pair_dict["profit_pct"] = round( (trade_pair_dict["sell_close"] - trade_pair_dict["buy_close"]) / trade_pair_dict["buy_close"] * 100, 4, ) - if trade_pair_dict["sell_type"] == "止盈" and trade_pair_dict["profit_pct"] < 0: + if trade_pair_dict["profit_pct"] <= 0: trade_pair_dict["sell_type"] = "止损" + else: + trade_pair_dict["sell_type"] = "止盈" if trade_pair_dict.get("last_max_close", None) is not None: # remove last_max_close trade_pair_dict.pop("last_max_close") + if trade_pair_dict.get("process_ma5_gt_ma10", None) is not None: + trade_pair_dict.pop("process_ma5_gt_ma10") + trade_list.append(trade_pair_dict) trade_pair_dict = {} @@ -219,30 +227,75 @@ class MeanReversionSandbox: ): """ 买入条件 - 1. 窗口周期为100, 即100个K线 - 2. 当前low_10_low为1, 即当前最低价格在窗口周期的10分位以下 - 3. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量 - 4. 当前K线为阳线, 即close > open - 5. TODO: 考虑K线形态 + 1. 窗口周期为100, 即100个K线, + 2. 当前close_10_low为1, 即当前收盘价在窗口周期的10分位以下, + 3. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量, + 4. (当前K线为阳线, 即close > open)或者K线为一字, 长倒T线, 倒T线, 长十字星, 十字星, """ if index < 2: return False - if row["close"] <= row["open"]: + if row["close"] <= row["open"] and row["k_shape"] not in [ + "一字", + "长倒T线", + "倒T线", + "长十字星", + "十字星", + ]: return False - if row["low_10_low"] != 1: + if row["close_10_low"] != 1: return False - # 如果当前与前两个K线,huge_volume都不为1,则返回False + # 如果当前与前两个K线, huge_volume都不为1, 则返回False if ( row["huge_volume"] != 1 and market_data.loc[index - 1, "huge_volume"] != 1 and market_data.loc[index - 2, "huge_volume"] != 1 ): return False - logger.info(f"符合买入条件") + + # if not self.check_metrics_over_sell(row): + # return False + # latest_1h_data = self.get_latest_1h_data(row["symbol"], row["date_time"]) + # if latest_1h_data is None or len(latest_1h_data) == 0: + # logger.info(f"符合买入条件") + # return True + # # 当前小时周期的ma5小于ma10, 表明空头趋势, 则返回False + # elif ( + # not pd.isna(latest_1h_data["ma5"]) + # and not pd.isna(latest_1h_data["ma10"]) + # and latest_1h_data["ma5"] < latest_1h_data["ma10"] + # ): + # # logger.info(f"当前小时周期的ma5小于ma10, 空头趋势, 不符合买入条件") + # return False + # else: + # logger.info(f"符合买入条件") + # return True + return True + def get_latest_1h_data(self, symbol: str, current_date_time: str): + bar = "1H" + # 根据current_date_time, 获取当前时间往前推1H的日期时间, + # 如当前时间为2025-08-20 10:20:05, 则获取2025-08-20 09:00:00 + before_date_time = datetime.strptime(current_date_time, "%Y-%m-%d %H:%M:%S") + before_date_time = before_date_time - timedelta(hours=1) + # current_date_time取整数小时,如2025-08-20 10:20:05, 取2025-08-20 10:00:00 + before_date_time = before_date_time.replace(minute=0, second=0, microsecond=0) + before_date_time = before_date_time.strftime("%Y-%m-%d %H:%M:%S") + end_date_time = datetime.strptime(current_date_time, "%Y-%m-%d %H:%M:%S") + end_date_time = end_date_time.replace(minute=0, second=0, microsecond=0) + end_date_time = end_date_time - timedelta(seconds=1) + end_date_time = end_date_time.strftime("%Y-%m-%d %H:%M:%S") + latest_1h_data = self.db_merge_market_huge_volume.merge_market_huge_volume( + symbol, bar, 100, before_date_time, end_date_time + ) + if latest_1h_data is None or len(latest_1h_data) == 0: + return None + # 只获取第一行数据 + latest_1h_data = latest_1h_data.iloc[0] + return latest_1h_data + def check_stop_loss_condition(self, trade_pair_dict: dict, row: pd.Series): symbol = trade_pair_dict["symbol"] bar = trade_pair_dict["bar"] @@ -282,9 +335,7 @@ class MeanReversionSandbox: market_data, row, index ) elif self.solution == "solution_3": - return self.check_take_profit_condition_solution_3( - trade_pair_dict, row - ) + return self.check_take_profit_condition_solution_3(trade_pair_dict, row) else: raise ValueError(f"Invalid strategy name: {self.solution}") except Exception as e: @@ -299,10 +350,10 @@ class MeanReversionSandbox: ): """ 高位放量止盈 - 简易版 - 1. 当前high_80_high为1或者high_90_high为1 + 1. 当前close_80_high为1或者close_90_high为1 2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量 """ - if row["high_80_high"] != 1 and row["high_90_high"] != 1: + if row["close_80_high"] != 1 and row["close_90_high"] != 1: return False if ( row["huge_volume"] != 1 @@ -322,7 +373,7 @@ class MeanReversionSandbox: """ 高位放量止盈 - 复杂版 前提条件 - 1. 当前high_80_high为1或者high_90_high为1 + 1. 当前close_80_high为1或者close_90_high为1 2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量 以下两个条件, 任一满足即可 1. K线为阴线, 即close < open @@ -334,25 +385,46 @@ class MeanReversionSandbox: if row["close"] < row["open"]: logger.info(f"符合高位放量止盈 - 复杂版条件") return True - elif row["k_shape"] in ["一字", "长吊锤线", "吊锤线", "长倒T线", "倒T线", "长十字星", "十字星", "长上影线纺锤体", "长下影线纺锤体"]: + elif row["k_shape"] in [ + "一字", + "长吊锤线", + "吊锤线", + "长倒T线", + "倒T线", + "长十字星", + "十字星", + "长上影线纺锤体", + "长下影线纺锤体", + ]: logger.info(f"符合高位放量止盈 - 复杂版条件") return True else: return False def check_take_profit_condition_solution_3( - self, - trade_pair_dict: dict, - row: pd.Series + self, trade_pair_dict: dict, row: pd.Series ): """ - 上涨波段盈利中位数止盈法 - 1. 超过波段中位数涨幅, 即up_median后, 记录当前价格, 继续持仓 + 上涨波段盈利阶段止盈法 + 1. 超过波段中位数涨幅, 即up_median/ 到达价位90分位/ 任意技术指标出现中度超卖, 记录当前价格, 继续持仓 2. 之后一个周期, 如果价格上涨, 则记录该价格继续持仓 3. 之后一个周期, 如果价格跌到记录价格之下, 则卖出 + 4. 如果买入时ma5小于ma10, 过程中ma5大于ma10, 进行记录。之后出现ma5小于ma10, 则卖出 """ current_close = row["close"] last_max_close = trade_pair_dict.get("last_max_close", None) + + if trade_pair_dict["buy_ma5_lt_ma10"]: + if trade_pair_dict.get("process_ma5_gt_ma10", None): + if row["ma5"] < row["ma10"]: + logger.info(f"MA5小于MA10发生转势, 卖出") + return True + + if row["ma5"] > row["ma10"]: + trade_pair_dict["process_ma5_gt_ma10"] = True + else: + trade_pair_dict["process_ma5_gt_ma10"] = False + if last_max_close is not None: if current_close >= last_max_close: logger.info(f"价格上涨, 继续持仓") @@ -372,10 +444,73 @@ class MeanReversionSandbox: ].values[0] / 100 ) - + + need_record = False buy_close = trade_pair_dict["buy_close"] price_chg = (current_close - buy_close) / buy_close if price_chg > up_median: logger.info(f"当前价格上涨超过波段中位数涨幅, 记录当前价格") + need_record = True + elif self.check_metrics_over_buy(row): + logger.info(f"技术指标超买, 记录当前价格") + need_record = True + elif row["close_90_high"] == 1: + logger.info(f"到达价位90分位, 记录当前价格") + need_record = True + else: + need_record = False + if need_record: trade_pair_dict["last_max_close"] = current_close return False + + def check_metrics_over_buy(self, row: pd.Series): + """ + 检查技术指标是否出现中度超买 + KDJ + K:85.00 + D:80.00 + J:100.00 + 说明:K 和 D 进一步上升, J 显著高于100, 表示超买加剧, 回调概率增加, 但可能仍需确认。 + RSI 14 + RSI:80.00 + 说明:RSI 进一步上升, 超买程度加深, 市场可能接近短期顶部, 回调概率增加。 + BOLL + 价格位置:价格突破上轨, 偏离上轨约 +2.00%(即价格 = 上轨 × 1.02) + 说明:价格显著突破上轨, 超买程度加深, 可能预示短期回调或反转 + """ + if row["kdj_k"] > 85 and row["kdj_d"] > 80 and row["kdj_j"] > 100: + logger.info(f"KDJ超买") + return True + if row["rsi_14"] > 80: + logger.info(f"RSI超买") + return True + if row["boll_upper"] * 1.02 < row["close"]: + logger.info(f"BOLL超买") + return True + return False + + def check_metrics_over_sell(self, row: pd.Series): + """ + 检查技术指标是否出现超卖 + KDJ + K: 25.00 + D: 30.00 + J: 20.00 + + RSI 14 + RSI:30.00 + 说明: RSI 进一步下降, 超卖程度加深, 市场可能接近短期底部, 反弹概率增加。 + + BOLL + 价格位置: 价格接近下轨 + """ + if row["kdj_k"] < 25 and row["kdj_d"] < 30 and row["kdj_j"] < 20: + logger.info(f"KDJ超卖") + return True + if row["rsi_14"] < 30: + logger.info(f"RSI超卖") + return True + if row["boll_lower"] >= row["close"]: + logger.info(f"BOLL超卖") + return True + return False diff --git a/requirements.txt b/requirements.txt index d07f186..b2491a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,12 @@ okx>=2.1.2 +python-okx >= 0.3.9 pandas>=2.0.0 requests>=2.25.0 sqlalchemy >= 2.0.41 pymysql >= 1.1.1 wechatpy >= 1.8.18 seaborn >= 0.13.2 -schedule >= 1.2.2 \ No newline at end of file +schedule >= 1.2.2 +xlsxwriter >= 3.2.5 +openpyxl >= 3.1.5 +cryptography >= 3.4.8 \ No newline at end of file diff --git a/trade_sandbox_main.py b/trade_sandbox_main.py index 6ab47d0..bcf4a9e 100644 --- a/trade_sandbox_main.py +++ b/trade_sandbox_main.py @@ -23,14 +23,21 @@ logger = logging.logger class MeanReversionSandboxMain: - def __init__(self, start_date: str, end_date: str, window_size: int): + def __init__(self, start_date: str, end_date: str, window_size: int, only_5m: bool = False, solution_list: list = None): self.symbols = MONITOR_CONFIG.get("volume_monitor", {}).get( "symbols", ["XCH-USDT"] ) - self.bars = MONITOR_CONFIG.get("volume_monitor", {}).get( - "bars", ["5m", "15m", "30m", "1H"] - ) - self.solution_list = ["solution_1", "solution_2", "solution_3"] + self.only_5m = only_5m + if only_5m: + self.bars = ["5m"] + else: + self.bars = MONITOR_CONFIG.get("volume_monitor", {}).get( + "bars", ["5m", "15m", "30m", "1H"] + ) + if solution_list is None: + self.solution_list = ["solution_1", "solution_2", "solution_3"] + else: + self.solution_list = solution_list self.start_date = start_date self.end_date = end_date self.window_size = window_size @@ -176,8 +183,13 @@ class MeanReversionSandboxMain: sheet_name = f"{solution}_chart" chart_dict[sheet_name] = {} for y_axis_field in y_axis_fields: - # 绘制2x2的画布 - fig, axs = plt.subplots(2, 2, figsize=(10, 10)) + if self.only_5m: + fig, axs = plt.subplots(1, 1, figsize=(10, 10)) + # 当只有一个子图时,将axs包装成数组以便统一处理 + axs = np.array([[axs]]) + else: + # 绘制2x2的画布 + fig, axs = plt.subplots(2, 2, figsize=(10, 10)) for j, bar in enumerate(bars_in_order): ax = axs[j // 2, j % 2] bar_data = stat_data[stat_data["bar"] == bar].copy() @@ -191,6 +203,21 @@ class MeanReversionSandboxMain: palette=colors, ax=ax, ) + + # 在柱子上方添加数值标签 + for i, (idx, row) in enumerate(bar_data.iterrows()): + value = row[y_axis_field] + # 根据数值类型格式化标签 + if "ratio" in y_axis_field: + label = f"{value:.2f}%" + else: + label = f"{value:.4f}" + + # 在柱子上方显示数值 + ax.text(i, value, label, + ha='center', va='bottom', + fontsize=9, fontweight='bold') + ax.set_ylabel(y_axis_field) ax.set_xlabel("symbol") ax.set_title(f"{solution} {bar}") @@ -203,9 +230,10 @@ class MeanReversionSandboxMain: label.set_horizontalalignment("right") # 隐藏未使用的subplot total_used = len(bars_in_order) - for k in range(total_used, 4): - ax = axs[k // 2, k % 2] - ax.axis("off") + if not self.only_5m: + for k in range(total_used, 4): + ax = axs[k // 2, k % 2] + ax.axis("off") fig.tight_layout() file_name = f"{solution}_{y_axis_field}.png" fig.savefig(os.path.join(save_path, file_name)) @@ -271,7 +299,8 @@ class MeanReversionSandboxMain: if __name__ == "__main__": start_date = "2025-05-15 00:00:00" end_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + solution_list = ["solution_3"] mean_reversion_sandbox_main = MeanReversionSandboxMain( - start_date=start_date, end_date=end_date, window_size=100 + start_date=start_date, end_date=end_date, window_size=100, only_5m=True, solution_list=solution_list ) mean_reversion_sandbox_main.batch_mean_reversion_sandbox()