crypto_quant/core/trade/mean_reversion_sandbox.py

382 lines
16 KiB
Python
Raw Normal View History

2025-08-20 03:33:13 +00:00
import json
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
import re
from openpyxl import Workbook
from openpyxl.drawing.image import Image
import openpyxl
from openpyxl.styles import Font
from PIL import Image as PILImage
from config import MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE
import core.logger as logging
from core.db.db_merge_market_huge_volume import DBMergeMarketHugeVolume
from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp
# seaborn支持中文
plt.rcParams["font.family"] = ["SimHei"]
logger = logging.logger
class MeanReversionSandbox:
def __init__(self, solution: str):
mysql_user = MYSQL_CONFIG.get("user", "xch")
mysql_password = MYSQL_CONFIG.get("password", "")
if not mysql_password:
raise ValueError("MySQL password is not set")
mysql_host = MYSQL_CONFIG.get("host", "localhost")
mysql_port = MYSQL_CONFIG.get("port", 3306)
mysql_database = MYSQL_CONFIG.get("database", "okx")
self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}"
self.db_merge_market_huge_volume = DBMergeMarketHugeVolume(self.db_url)
self.peak_valley_data = self.get_peak_valley_data()
self.solution = solution
self.save_path = f"./output/trade_sandbox/mean_reversion/{self.solution}/"
os.makedirs(self.save_path, exist_ok=True)
self.strategy_description = self.get_startegy_description()
def get_startegy_description(self):
desc_dict = {
"买入": [
"1. 窗口周期为100, 即100个K线",
"2. 当前low_10_low为1, 即当前最低价格在窗口周期的10分位以下",
"3. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量",
"4. 当前K线为阳线, 即close > open",
],
"止损": ["跌幅超过下跌周期跌幅中位数, 即down_median后卖出"],
"止盈": {
"solution_1": [
"高位放量止盈 - 简易版",
"1. 当前high_80_high为1或者high_90_high为1",
"2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量",
],
"solution_2": [
"高位放量止盈 - 复杂版",
"前提条件"
"1. 当前high_80_high为1或者high_90_high为1",
"2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量",
"以下两个条件, 任一满足即可",
"1. K线为阴线, 即close < open",
"2. K线为阳线, 即close >= open, 且k_shape满足:",
"一字, 长吊锤线, 吊锤线, 长倒T线, 倒T线, 长十字星, 十字星, 长上影线纺锤体, 长下影线纺锤体",
],
"solution_3": [
"上涨波段盈利中位数止盈法",
"1. 超过波段中位数涨幅, 即up_median后, 记录当前价格, 继续持仓",
"2. 之后一个周期, 如果价格上涨, 则记录该价格继续持仓",
"3. 之后一个周期, 如果价格跌到记录价格之下, 则卖出",
],
},
}
buy_list = desc_dict.get("买入", [])
stop_loss_list = desc_dict.get("止损", [])
take_profit_list = desc_dict.get("止盈", {}).get(self.solution, [])
if len(take_profit_list) == 0:
self.solution = "solution_1"
take_profit_list = desc_dict.get("止盈", {}).get(self.solution, [])
desc = f"策略名称: {self.solution}\n\n"
buy_desc = "\n".join(buy_list)
stop_loss_desc = "\n".join(stop_loss_list)
take_profit_desc = "\n".join(take_profit_list)
desc += f"买入策略\n {buy_desc}\n\n"
desc += f"止损策略\n {stop_loss_desc}\n\n"
desc += f"止盈策略\n {take_profit_desc}\n\n"
with open(f"{self.save_path}/策略描述.txt", "w", encoding="utf-8") as f:
f.write(desc)
return desc
def get_peak_valley_data(self):
os.makedirs("./json/", exist_ok=True)
json_file_path = "./json/peak_valley_data.json"
if not os.path.exists(json_file_path):
excel_file_path = "./output/statistics/excel/price_volume_stats_window_size_100_from_20250515000000_to_20250819110500.xlsx"
if not os.path.exists(excel_file_path):
raise FileNotFoundError(f"Excel file not found: {excel_file_path}")
sheet_name = "波峰波谷统计"
df = pd.read_excel(excel_file_path, sheet_name=sheet_name)
if df is None or len(df) == 0:
raise ValueError("Excel file is empty")
data_list = []
for index, row in df.iterrows():
data_list.append(
{
"symbol": row["symbol"],
"bar": row["bar"],
"up_mean": row["up_mean"],
"up_median": row["up_median"],
"down_mean": row["down_mean"],
"down_median": row["down_median"],
}
)
with open(json_file_path, "w", encoding="utf-8") as f:
json.dump(data_list, f, ensure_ascii=False, indent=4)
peak_valley_data = pd.DataFrame(data_list)
else:
with open(json_file_path, "r", encoding="utf-8") as f:
peak_valley_data = json.load(f)
return pd.DataFrame(peak_valley_data)
def trade_sandbox(
self, symbol: str, bar: str, window_size: int, start: str, end: str
):
logger.info(f"策略描述: {self.strategy_description}")
logger.info(
f"开始获取{symbol} {bar}{window_size}分钟窗口大小的数据, 开始时间: {start}, 结束时间: {end}"
)
market_data = self.db_merge_market_huge_volume.merge_market_huge_volume(
symbol, bar, window_size, start, end
)
if market_data is None:
return None
logger.info(f"数据条数: {len(market_data)}")
trade_list = []
trade_pair_dict = {}
for index, row in market_data.iterrows():
# check buy condition
if trade_pair_dict.get("buy_timestamp", None) is None:
buy_condition = self.check_buy_condition(market_data, row, index)
else:
buy_condition = False
if buy_condition:
trade_pair_dict = {}
trade_pair_dict["solution"] = self.solution
trade_pair_dict["symbol"] = symbol
trade_pair_dict["bar"] = bar
trade_pair_dict["window_size"] = window_size
trade_pair_dict["buy_timestamp"] = row["timestamp"]
trade_pair_dict["buy_date_time"] = timestamp_to_datetime(
row["timestamp"]
)
trade_pair_dict["buy_close"] = row["close"]
trade_pair_dict["buy_pct_chg"] = row["pct_chg"]
trade_pair_dict["buy_volume"] = row["volume"]
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"]
continue
if trade_pair_dict.get("buy_timestamp", None) is not None:
sell_condition = False
# check stop loss condition
sell_condition = self.check_stop_loss_condition(trade_pair_dict, row)
if sell_condition:
trade_pair_dict["sell_type"] = "止损"
else:
# check take profit condition
sell_condition = self.check_take_profit_condition(
trade_pair_dict, market_data, row, index
)
if sell_condition:
trade_pair_dict["sell_type"] = "止盈"
if sell_condition:
trade_pair_dict["sell_timestamp"] = row["timestamp"]
trade_pair_dict["sell_date_time"] = timestamp_to_datetime(
row["timestamp"]
)
trade_pair_dict["sell_close"] = row["close"]
trade_pair_dict["sell_pct_chg"] = row["pct_chg"]
trade_pair_dict["sell_volume"] = row["volume"]
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["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:
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")
trade_list.append(trade_pair_dict)
trade_pair_dict = {}
if len(trade_list) == 0:
return None
trade_data = pd.DataFrame(trade_list)
trade_data.sort_values(by="buy_timestamp", inplace=True)
trade_data.reset_index(drop=True, inplace=True)
return trade_data
def check_buy_condition(
self, market_data: pd.DataFrame, row: pd.Series, index: int
):
"""
买入条件
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线形态
"""
if index < 2:
return False
if row["close"] <= row["open"]:
return False
if row["low_10_low"] != 1:
return 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"符合买入条件")
return True
def check_stop_loss_condition(self, trade_pair_dict: dict, row: pd.Series):
symbol = trade_pair_dict["symbol"]
bar = trade_pair_dict["bar"]
# 获取下跌周期跌幅中位数, 为百分比
down_median = (
self.peak_valley_data.loc[
(self.peak_valley_data["symbol"] == symbol)
& (self.peak_valley_data["bar"] == bar),
"down_median",
].values[0]
/ 100
)
buy_close = trade_pair_dict["buy_close"]
current_close = row["close"]
if (
current_close < buy_close
and (current_close - buy_close) / buy_close < down_median
):
logger.info(f"符合止损条件")
return True
return False
def check_take_profit_condition(
self,
trade_pair_dict: dict,
market_data: pd.DataFrame,
row: pd.Series,
index: int,
):
try:
if self.solution == "solution_1":
return self.check_take_profit_condition_solution_1(
market_data, row, index
)
elif self.solution == "solution_2":
return self.check_take_profit_condition_solution_2(
market_data, row, index
)
elif self.solution == "solution_3":
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:
logger.error(f"检查止盈条件时发生错误: {e}")
return False
def check_take_profit_condition_solution_1(
self,
market_data: pd.DataFrame,
row: pd.Series,
index: int,
):
"""
高位放量止盈 - 简易版
1. 当前high_80_high为1或者high_90_high为1
2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量
"""
if row["high_80_high"] != 1 and row["high_90_high"] != 1:
return 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"符合高位放量止盈 - 简易版条件")
return True
def check_take_profit_condition_solution_2(
self,
market_data: pd.DataFrame,
row: pd.Series,
index: int,
):
"""
高位放量止盈 - 复杂版
前提条件
1. 当前high_80_high为1或者high_90_high为1
2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量
以下两个条件, 任一满足即可
1. K线为阴线, 即close < open
2. K线为阳线, 即close >= open, 且k_shape满足:
一字, 长吊锤线, 吊锤线, 长倒T线, 倒T线, 长十字星, 十字星, 长上影线纺锤体, 长下影线纺锤体
"""
if not self.check_take_profit_condition_solution_1(market_data, row, index):
return False
if row["close"] < row["open"]:
logger.info(f"符合高位放量止盈 - 复杂版条件")
return True
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
):
"""
上涨波段盈利中位数止盈法
1. 超过波段中位数涨幅, 即up_median后, 记录当前价格, 继续持仓
2. 之后一个周期, 如果价格上涨, 则记录该价格继续持仓
3. 之后一个周期, 如果价格跌到记录价格之下, 则卖出
"""
current_close = row["close"]
last_max_close = trade_pair_dict.get("last_max_close", None)
if last_max_close is not None:
if current_close >= last_max_close:
logger.info(f"价格上涨, 继续持仓")
trade_pair_dict["last_max_close"] = current_close
return False
else:
logger.info(f"符合上涨波段盈利中位数止盈法条件")
return True
else:
symbol = trade_pair_dict["symbol"]
bar = trade_pair_dict["bar"]
up_median = (
self.peak_valley_data.loc[
(self.peak_valley_data["symbol"] == symbol)
& (self.peak_valley_data["bar"] == bar),
"up_median",
].values[0]
/ 100
)
buy_close = trade_pair_dict["buy_close"]
price_chg = (current_close - buy_close) / buy_close
if price_chg > up_median:
logger.info(f"当前价格上涨超过波段中位数涨幅, 记录当前价格")
trade_pair_dict["last_max_close"] = current_close
return False