382 lines
16 KiB
Python
382 lines
16 KiB
Python
|
|
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
|