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
|
2025-08-20 08:40:33 +00:00
|
|
|
|
from datetime import datetime, timedelta
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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
|
2025-08-31 03:20:59 +00:00
|
|
|
|
from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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线",
|
2025-08-21 10:37:33 +00:00
|
|
|
|
"2. 满足close_10_low为1, 即当前收盘价在窗口周期的10分位以下",
|
2025-08-20 03:33:13 +00:00
|
|
|
|
"3. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量",
|
2025-08-20 08:40:33 +00:00
|
|
|
|
"4. 当前K线为阳线, 即close > open或者K线为一字, 长倒T线, 倒T线, 长十字星, 十字星",
|
|
|
|
|
|
# "5. 相同symbol的1H当前周期, ma5大于ma10",
|
|
|
|
|
|
# "5. KDJ, RSI, BOLL任意一个指标出现超卖",
|
2025-08-20 03:33:13 +00:00
|
|
|
|
],
|
|
|
|
|
|
"止损": ["跌幅超过下跌周期跌幅中位数, 即down_median后卖出"],
|
|
|
|
|
|
"止盈": {
|
|
|
|
|
|
"solution_1": [
|
|
|
|
|
|
"高位放量止盈 - 简易版",
|
2025-08-20 08:40:33 +00:00
|
|
|
|
"1. 当前close_80_high为1或者close_90_high为1",
|
2025-08-20 03:33:13 +00:00
|
|
|
|
"2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量",
|
|
|
|
|
|
],
|
|
|
|
|
|
"solution_2": [
|
|
|
|
|
|
"高位放量止盈 - 复杂版",
|
2025-08-20 08:40:33 +00:00
|
|
|
|
"前提条件" "1. 当前close_80_high为1或者close_90_high为1",
|
2025-08-20 03:33:13 +00:00
|
|
|
|
"2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量",
|
|
|
|
|
|
"以下两个条件, 任一满足即可",
|
|
|
|
|
|
"1. K线为阴线, 即close < open",
|
|
|
|
|
|
"2. K线为阳线, 即close >= open, 且k_shape满足:",
|
|
|
|
|
|
"一字, 长吊锤线, 吊锤线, 长倒T线, 倒T线, 长十字星, 十字星, 长上影线纺锤体, 长下影线纺锤体",
|
|
|
|
|
|
],
|
|
|
|
|
|
"solution_3": [
|
|
|
|
|
|
"上涨波段盈利中位数止盈法",
|
2025-08-20 08:40:33 +00:00
|
|
|
|
"1. 超过波段中位数涨幅, 即up_median/ 到达价位90分位/ 任意技术指标出现中度超卖, 记录当前价格, 继续持仓",
|
2025-08-20 03:33:13 +00:00
|
|
|
|
"2. 之后一个周期, 如果价格上涨, 则记录该价格继续持仓",
|
|
|
|
|
|
"3. 之后一个周期, 如果价格跌到记录价格之下, 则卖出",
|
2025-08-20 08:40:33 +00:00
|
|
|
|
"4. 如果买入时ma5小于ma10, 过程中ma5大于ma10, 进行记录。之后出现ma5小于ma10, 则卖出",
|
2025-08-20 03:33:13 +00:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
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):
|
2025-08-22 10:48:59 +00:00
|
|
|
|
excel_file_path = "./output/statistics/excel/price_volume_stats_window_size_100_from_20250515000000_to_20250822145000.xlsx"
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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:
|
2025-08-21 10:37:33 +00:00
|
|
|
|
buy_condition = self.check_buy_condition(market_data, row, index, window_size)
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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"]
|
2025-08-20 08:40:33 +00:00
|
|
|
|
trade_pair_dict["buy_close_10_low"] = row["close_10_low"]
|
|
|
|
|
|
trade_pair_dict["buy_ma5_lt_ma10"] = row["ma5"] < row["ma10"]
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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)
|
2025-08-21 10:37:33 +00:00
|
|
|
|
sell = sell_condition["sell"]
|
|
|
|
|
|
if sell:
|
2025-08-20 03:33:13 +00:00
|
|
|
|
trade_pair_dict["sell_type"] = "止损"
|
|
|
|
|
|
else:
|
|
|
|
|
|
# check take profit condition
|
|
|
|
|
|
sell_condition = self.check_take_profit_condition(
|
|
|
|
|
|
trade_pair_dict, market_data, row, index
|
|
|
|
|
|
)
|
2025-08-21 10:37:33 +00:00
|
|
|
|
sell = sell_condition["sell"]
|
|
|
|
|
|
if sell:
|
2025-08-20 03:33:13 +00:00
|
|
|
|
trade_pair_dict["sell_type"] = "止盈"
|
|
|
|
|
|
|
2025-08-21 10:37:33 +00:00
|
|
|
|
if sell:
|
|
|
|
|
|
trade_pair_dict["sell_reason"] = sell_condition["reason"]
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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"]
|
2025-08-20 08:40:33 +00:00
|
|
|
|
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"]
|
2025-08-20 03:33:13 +00:00
|
|
|
|
trade_pair_dict["profit_pct"] = round(
|
|
|
|
|
|
(trade_pair_dict["sell_close"] - trade_pair_dict["buy_close"])
|
|
|
|
|
|
/ trade_pair_dict["buy_close"]
|
|
|
|
|
|
* 100,
|
|
|
|
|
|
4,
|
|
|
|
|
|
)
|
2025-08-20 08:40:33 +00:00
|
|
|
|
if trade_pair_dict["profit_pct"] <= 0:
|
2025-08-20 03:33:13 +00:00
|
|
|
|
trade_pair_dict["sell_type"] = "止损"
|
2025-08-20 08:40:33 +00:00
|
|
|
|
else:
|
|
|
|
|
|
trade_pair_dict["sell_type"] = "止盈"
|
2025-08-20 03:33:13 +00:00
|
|
|
|
if trade_pair_dict.get("last_max_close", None) is not None:
|
|
|
|
|
|
# remove last_max_close
|
|
|
|
|
|
trade_pair_dict.pop("last_max_close")
|
|
|
|
|
|
|
2025-08-20 08:40:33 +00:00
|
|
|
|
if trade_pair_dict.get("process_ma5_gt_ma10", None) is not None:
|
|
|
|
|
|
trade_pair_dict.pop("process_ma5_gt_ma10")
|
|
|
|
|
|
|
2025-08-20 03:33:13 +00:00
|
|
|
|
trade_list.append(trade_pair_dict)
|
|
|
|
|
|
trade_pair_dict = {}
|
|
|
|
|
|
|
|
|
|
|
|
if len(trade_list) == 0:
|
|
|
|
|
|
return None
|
|
|
|
|
|
trade_data = pd.DataFrame(trade_list)
|
2025-08-21 10:37:33 +00:00
|
|
|
|
trade_data = trade_data[
|
|
|
|
|
|
[
|
|
|
|
|
|
"solution",
|
|
|
|
|
|
"symbol",
|
|
|
|
|
|
"bar",
|
|
|
|
|
|
"window_size",
|
|
|
|
|
|
"sell_type",
|
|
|
|
|
|
"sell_reason",
|
|
|
|
|
|
"profit_pct",
|
|
|
|
|
|
"buy_timestamp",
|
|
|
|
|
|
"buy_date_time",
|
|
|
|
|
|
"sell_timestamp",
|
|
|
|
|
|
"sell_date_time",
|
|
|
|
|
|
"buy_close",
|
|
|
|
|
|
"buy_pct_chg",
|
|
|
|
|
|
"sell_close",
|
|
|
|
|
|
"sell_pct_chg",
|
|
|
|
|
|
"buy_volume",
|
|
|
|
|
|
"buy_huge_volume",
|
|
|
|
|
|
"buy_volume_ratio",
|
|
|
|
|
|
"buy_k_shape",
|
|
|
|
|
|
"buy_close_10_low",
|
|
|
|
|
|
"buy_ma5_lt_ma10",
|
|
|
|
|
|
"sell_volume",
|
|
|
|
|
|
"sell_huge_volume",
|
|
|
|
|
|
"sell_volume_ratio",
|
|
|
|
|
|
"sell_k_shape",
|
|
|
|
|
|
"sell_close_80_high",
|
|
|
|
|
|
"sell_close_90_high",
|
|
|
|
|
|
"sell_close_10_low",
|
|
|
|
|
|
"sell_close_20_low",
|
|
|
|
|
|
]
|
|
|
|
|
|
]
|
2025-08-20 03:33:13 +00:00
|
|
|
|
trade_data.sort_values(by="buy_timestamp", inplace=True)
|
|
|
|
|
|
trade_data.reset_index(drop=True, inplace=True)
|
|
|
|
|
|
return trade_data
|
|
|
|
|
|
|
|
|
|
|
|
def check_buy_condition(
|
2025-08-21 10:37:33 +00:00
|
|
|
|
self, market_data: pd.DataFrame, row: pd.Series, index: int, window_size: int
|
2025-08-20 03:33:13 +00:00
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
买入条件
|
2025-08-20 08:40:33 +00:00
|
|
|
|
1. 窗口周期为100, 即100个K线,
|
2025-08-21 10:37:33 +00:00
|
|
|
|
2. 满足close_10_low为1, 即当前收盘价在窗口周期的10分位以下
|
2025-08-20 08:40:33 +00:00
|
|
|
|
3. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量,
|
|
|
|
|
|
4. (当前K线为阳线, 即close > open)或者K线为一字, 长倒T线, 倒T线, 长十字星, 十字星,
|
2025-08-20 03:33:13 +00:00
|
|
|
|
"""
|
|
|
|
|
|
if index < 2:
|
|
|
|
|
|
return False
|
2025-08-20 08:40:33 +00:00
|
|
|
|
if row["close"] <= row["open"] and row["k_shape"] not in [
|
|
|
|
|
|
"一字",
|
|
|
|
|
|
"长倒T线",
|
|
|
|
|
|
"倒T线",
|
|
|
|
|
|
"长十字星",
|
|
|
|
|
|
"十字星",
|
|
|
|
|
|
]:
|
2025-08-20 03:33:13 +00:00
|
|
|
|
return False
|
|
|
|
|
|
|
2025-08-21 10:37:33 +00:00
|
|
|
|
|
|
|
|
|
|
# 满足close_10_low为1, 即当前收盘价在窗口周期的10分位以下
|
2025-08-20 08:40:33 +00:00
|
|
|
|
if row["close_10_low"] != 1:
|
2025-08-20 03:33:13 +00:00
|
|
|
|
return False
|
|
|
|
|
|
|
2025-08-20 08:40:33 +00:00
|
|
|
|
# 如果当前与前两个K线, huge_volume都不为1, 则返回False
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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
|
2025-08-20 08:40:33 +00:00
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
2025-08-20 03:33:13 +00:00
|
|
|
|
return True
|
|
|
|
|
|
|
2025-08-20 08:40:33 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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"]
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result = {"sell": False, "reason": ""}
|
2025-08-20 03:33:13 +00:00
|
|
|
|
if (
|
|
|
|
|
|
current_close < buy_close
|
|
|
|
|
|
and (current_close - buy_close) / buy_close < down_median
|
|
|
|
|
|
):
|
|
|
|
|
|
logger.info(f"符合止损条件")
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = True
|
|
|
|
|
|
result["reason"] = f"亏损超过下跌波段跌幅中位数"
|
|
|
|
|
|
return result
|
|
|
|
|
|
result["sell"] = False
|
|
|
|
|
|
result["reason"] = "未达到止损条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
|
|
|
|
|
|
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":
|
2025-08-20 08:40:33 +00:00
|
|
|
|
return self.check_take_profit_condition_solution_3(trade_pair_dict, row)
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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,
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
高位放量止盈 - 简易版
|
2025-08-20 08:40:33 +00:00
|
|
|
|
1. 当前close_80_high为1或者close_90_high为1
|
2025-08-20 03:33:13 +00:00
|
|
|
|
2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量
|
|
|
|
|
|
"""
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result = {"sell": False, "reason": ""}
|
2025-08-20 08:40:33 +00:00
|
|
|
|
if row["close_80_high"] != 1 and row["close_90_high"] != 1:
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = False
|
|
|
|
|
|
result["reason"] = "未达到止盈条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
if (
|
|
|
|
|
|
row["huge_volume"] != 1
|
|
|
|
|
|
and market_data.loc[index - 1, "huge_volume"] != 1
|
|
|
|
|
|
and market_data.loc[index - 2, "huge_volume"] != 1
|
|
|
|
|
|
):
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = False
|
|
|
|
|
|
result["reason"] = "未达到止盈条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
logger.info(f"符合高位放量止盈 - 简易版条件")
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = True
|
|
|
|
|
|
result["reason"] = "符合高位放量止盈 - 简易版条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
|
|
|
|
|
|
def check_take_profit_condition_solution_2(
|
|
|
|
|
|
self,
|
|
|
|
|
|
market_data: pd.DataFrame,
|
|
|
|
|
|
row: pd.Series,
|
|
|
|
|
|
index: int,
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
高位放量止盈 - 复杂版
|
|
|
|
|
|
前提条件
|
2025-08-20 08:40:33 +00:00
|
|
|
|
1. 当前close_80_high为1或者close_90_high为1
|
2025-08-20 03:33:13 +00:00
|
|
|
|
2. 之前2个K线与当前K线, 存在任意一个K线huge_volume为1, 即存在一个K线是巨量
|
|
|
|
|
|
以下两个条件, 任一满足即可
|
|
|
|
|
|
1. K线为阴线, 即close < open
|
|
|
|
|
|
2. K线为阳线, 即close >= open, 且k_shape满足:
|
|
|
|
|
|
一字, 长吊锤线, 吊锤线, 长倒T线, 倒T线, 长十字星, 十字星, 长上影线纺锤体, 长下影线纺锤体
|
|
|
|
|
|
"""
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result = {"sell": False, "reason": ""}
|
2025-08-20 03:33:13 +00:00
|
|
|
|
if not self.check_take_profit_condition_solution_1(market_data, row, index):
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = False
|
|
|
|
|
|
result["reason"] = "未达到止盈条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
if row["close"] < row["open"]:
|
|
|
|
|
|
logger.info(f"符合高位放量止盈 - 复杂版条件")
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = True
|
|
|
|
|
|
result["reason"] = "符合高位放量止盈 - 复杂版条件"
|
|
|
|
|
|
return result
|
2025-08-20 08:40:33 +00:00
|
|
|
|
elif row["k_shape"] in [
|
|
|
|
|
|
"一字",
|
|
|
|
|
|
"长吊锤线",
|
|
|
|
|
|
"吊锤线",
|
|
|
|
|
|
"长倒T线",
|
|
|
|
|
|
"倒T线",
|
|
|
|
|
|
"长十字星",
|
|
|
|
|
|
"十字星",
|
|
|
|
|
|
"长上影线纺锤体",
|
|
|
|
|
|
"长下影线纺锤体",
|
|
|
|
|
|
]:
|
2025-08-20 03:33:13 +00:00
|
|
|
|
logger.info(f"符合高位放量止盈 - 复杂版条件")
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = True
|
|
|
|
|
|
result["reason"] = "符合高位放量止盈 - 复杂版条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
else:
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = False
|
|
|
|
|
|
result["reason"] = "未达到止盈条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
|
|
|
|
|
|
def check_take_profit_condition_solution_3(
|
2025-08-20 08:40:33 +00:00
|
|
|
|
self, trade_pair_dict: dict, row: pd.Series
|
2025-08-20 03:33:13 +00:00
|
|
|
|
):
|
|
|
|
|
|
"""
|
2025-08-20 08:40:33 +00:00
|
|
|
|
上涨波段盈利阶段止盈法
|
|
|
|
|
|
1. 超过波段中位数涨幅, 即up_median/ 到达价位90分位/ 任意技术指标出现中度超卖, 记录当前价格, 继续持仓
|
2025-08-20 03:33:13 +00:00
|
|
|
|
2. 之后一个周期, 如果价格上涨, 则记录该价格继续持仓
|
|
|
|
|
|
3. 之后一个周期, 如果价格跌到记录价格之下, 则卖出
|
2025-08-20 08:40:33 +00:00
|
|
|
|
4. 如果买入时ma5小于ma10, 过程中ma5大于ma10, 进行记录。之后出现ma5小于ma10, 则卖出
|
2025-08-20 03:33:13 +00:00
|
|
|
|
"""
|
|
|
|
|
|
current_close = row["close"]
|
|
|
|
|
|
last_max_close = trade_pair_dict.get("last_max_close", None)
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result = {"sell": False, "reason": ""}
|
2025-08-20 08:40:33 +00:00
|
|
|
|
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发生转势, 卖出")
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = True
|
|
|
|
|
|
result["reason"] = "MA5小于MA10发生转势"
|
|
|
|
|
|
return result
|
2025-08-20 08:40:33 +00:00
|
|
|
|
|
|
|
|
|
|
if row["ma5"] > row["ma10"]:
|
|
|
|
|
|
trade_pair_dict["process_ma5_gt_ma10"] = True
|
|
|
|
|
|
else:
|
|
|
|
|
|
trade_pair_dict["process_ma5_gt_ma10"] = False
|
|
|
|
|
|
|
2025-08-20 03:33:13 +00:00
|
|
|
|
if last_max_close is not None:
|
|
|
|
|
|
if current_close >= last_max_close:
|
|
|
|
|
|
logger.info(f"价格上涨, 继续持仓")
|
|
|
|
|
|
trade_pair_dict["last_max_close"] = current_close
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = False
|
|
|
|
|
|
result["reason"] = "价格上涨, 继续持仓"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f"符合上涨波段盈利中位数止盈法条件")
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = True
|
|
|
|
|
|
result["reason"] = "符合上涨波段盈利中位数止盈条件"
|
|
|
|
|
|
return result
|
2025-08-20 03:33:13 +00:00
|
|
|
|
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
|
|
|
|
|
|
)
|
2025-08-20 08:40:33 +00:00
|
|
|
|
|
|
|
|
|
|
need_record = False
|
2025-08-20 03:33:13 +00:00
|
|
|
|
buy_close = trade_pair_dict["buy_close"]
|
|
|
|
|
|
price_chg = (current_close - buy_close) / buy_close
|
|
|
|
|
|
if price_chg > up_median:
|
|
|
|
|
|
logger.info(f"当前价格上涨超过波段中位数涨幅, 记录当前价格")
|
2025-08-20 08:40:33 +00:00
|
|
|
|
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:
|
2025-08-20 03:33:13 +00:00
|
|
|
|
trade_pair_dict["last_max_close"] = current_close
|
2025-08-21 10:37:33 +00:00
|
|
|
|
result["sell"] = False
|
|
|
|
|
|
result["reason"] = "未达到止盈条件"
|
|
|
|
|
|
return result
|
2025-08-20 08:40:33 +00:00
|
|
|
|
|
|
|
|
|
|
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
|