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 8b61735..88faf8a 100644 Binary files a/core/trade/__pycache__/mean_reversion_sandbox.cpython-312.pyc and b/core/trade/__pycache__/mean_reversion_sandbox.cpython-312.pyc differ 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()