From b1e7ddc261dbed6aba907e7f11c098bbb0ed4cca Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Fri, 22 Aug 2025 18:48:59 +0800 Subject: [PATCH] combine trade strategy --- core/statistics/ma_break_statistics.py | 295 ---------- .../ma_break_statistics.cpython-312.pyc | Bin 0 -> 21727 bytes core/trade/ma_break_statistics.py | 506 ++++++++++++++++++ core/trade/mean_reversion_sandbox.py | 2 +- json/peak_valley_data.json | 1 - json/trade_strategy.json | 102 ++++ market_data_main.py | 6 +- sql/query/sql_playground.sql | 2 +- trade_ma_strategy_main.py | 42 ++ 9 files changed, 654 insertions(+), 302 deletions(-) delete mode 100644 core/statistics/ma_break_statistics.py create mode 100644 core/trade/__pycache__/ma_break_statistics.cpython-312.pyc create mode 100644 core/trade/ma_break_statistics.py delete mode 100644 json/peak_valley_data.json create mode 100644 json/trade_strategy.json create mode 100644 trade_ma_strategy_main.py diff --git a/core/statistics/ma_break_statistics.py b/core/statistics/ma_break_statistics.py deleted file mode 100644 index f636b58..0000000 --- a/core/statistics/ma_break_statistics.py +++ /dev/null @@ -1,295 +0,0 @@ -import core.logger as logging -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 -from core.db.db_market_data import DBMarketData -from core.db.db_huge_volume_data import DBHugeVolumeData -from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp - -# seaborn支持中文 -plt.rcParams["font.family"] = ["SimHei"] - -logger = logging.logger - -class MaBreakStatistics: - """ - 统计MA突破之后的涨跌幅 - MA向上突破的点位周期K线:5 > 10 > 20 > 30 - 统计MA向上突破的点位周期K线,突破之后,到: - 下一个MA向下突破的点位周期K线:30 > 20 > 10 > 5 - 之间的涨跌幅 - """ - def __init__(self): - 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_market_data = DBMarketData(self.db_url) - self.db_huge_volume_data = DBHugeVolumeData(self.db_url) - 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.stats_output_dir = "./output/statistics/excel/" - os.makedirs(self.stats_output_dir, exist_ok=True) - self.stats_chart_dir = "./output/statistics/chart/" - os.makedirs(self.stats_chart_dir, exist_ok=True) - - def batch_statistics(self, all_change: bool = True): - ma_break_market_data_list = [] - for symbol in self.symbols: - for bar in self.bars: - logger.info(f"开始计算{symbol} {bar}的MA突破区间涨跌幅统计") - ma_break_market_data = self.statistics(symbol, bar, all_change) - if ma_break_market_data is not None: - ma_break_market_data_list.append(ma_break_market_data) - if len(ma_break_market_data_list) > 0: - ma_break_market_data = pd.concat(ma_break_market_data_list) - # 依据symbol和bar分组,统计每个symbol和bar的pct_chg的max, min, mean, std, median, count - pct_chg_df = (ma_break_market_data - .groupby(['symbol', 'bar'])['pct_chg'] - .agg(pct_chg_max='max', - pct_chg_min='min', - pct_chg_mean='mean', - pct_chg_std='std', - pct_chg_median='median', - pct_chg_count='count') - .reset_index()) - # 依据symbol和bar分组,统计每个symbol和bar的interval_minutes的max, min, mean, std, median, count - interval_minutes_df = (ma_break_market_data - .groupby(['symbol', 'bar'])['interval_minutes'] - .agg(interval_minutes_max='max', - interval_minutes_min='min', - interval_minutes_mean='mean', - interval_minutes_std='std', - interval_minutes_median='median', - interval_minutes_count='count') - .reset_index()) - - earliest_market_date_time = ma_break_market_data["begin_date_time"].min() - earliest_market_date_time = re.sub(r"[\:\-\s]", "", str(earliest_market_date_time)) - latest_market_date_time = ma_break_market_data["end_date_time"].max() - if latest_market_date_time is None: - latest_market_date_time = datetime.now().strftime("%Y%m%d") - latest_market_date_time = re.sub(r"[\:\-\s]", "", str(latest_market_date_time)) - if all_change: - output_file_name = f"ma_break_stats_from_{earliest_market_date_time}_to_{latest_market_date_time}_完全转势.xlsx" - else: - output_file_name = f"ma_break_stats_from_{earliest_market_date_time}_to_{latest_market_date_time}_部分转势.xlsx" - output_file_path = os.path.join(self.stats_output_dir, output_file_name) - logger.info(f"导出{output_file_path}") - with pd.ExcelWriter(output_file_path) as writer: - ma_break_market_data.to_excel(writer, sheet_name="ma_break_market_data", index=False) - pct_chg_df.to_excel(writer, sheet_name="pct_chg_stats", index=False) - interval_minutes_df.to_excel(writer, sheet_name="interval_minutes_stats", index=False) - - chart_dict = self.draw_pct_chg_mean_chart(pct_chg_df, all_change) - self.output_chart_to_excel(output_file_path, chart_dict) - else: - return None - - def statistics(self, symbol: str, bar: str, all_change: bool = False): - market_data = self.db_market_data.query_market_data_by_symbol_bar(symbol, bar, start=None, end=None) - if market_data is None or len(market_data) == 0: - logger.warning(f"获取{symbol} {bar} 数据失败") - return - else: - market_data = pd.DataFrame(market_data) - market_data.sort_values(by="timestamp", ascending=True, inplace=True) - market_data.reset_index(drop=True, inplace=True) - logger.info(f"获取{symbol} {bar} 数据成功,数据条数: {len(market_data)}") - # 获得ma5, ma10, ma20, ma30不为空的行 - market_data = market_data[(market_data["ma5"].notna()) & - (market_data["ma10"].notna()) & - (market_data["ma20"].notna()) & - (market_data["ma30"].notna())] - logger.info(f"ma5, ma10, ma20, ma30不为空的行,数据条数: {len(market_data)}") - # 计算volume_ma5 - market_data["volume_ma5"] = market_data["volume"].rolling(window=5).mean() - # 获得5上穿10且ma5 > ma10 > ma20 > ma30且close > ma20的行,成交量较前5日均量放大20%以上 - market_data["volume_pct_chg"] = (market_data["volume"] - market_data["volume_ma5"]) / market_data["volume_ma5"] - market_data["volume_pct_chg"] = market_data["volume_pct_chg"].fillna(0) - - if all_change: - long_market_data = market_data[(market_data["ma_cross"] == "5上穿10") & (market_data["ma5"] > market_data["ma10"]) & - (market_data["ma10"] > market_data["ma20"]) & - (market_data["ma20"] > market_data["ma30"]) & - (market_data["close"] > market_data["ma20"]) & - (market_data["volume_pct_chg"] > 0.2)] - logger.info(f"5上穿10, 且ma5 > ma10 > ma20 > ma30,并且close > ma20,并且成交量较前5日均量放大20%以上的行,数据条数: {len(long_market_data)}") - else: - long_market_data = market_data[(market_data["ma_cross"] == "5上穿10") & (market_data["ma5"] > market_data["ma10"]) & - (market_data["volume_pct_chg"] > 0.2)] - logger.info(f"5上穿10, 且ma5 > ma10,并且成交量较前5日均量放大20%以上的行,数据条数: {len(long_market_data)}") - if len(long_market_data) == 0: - return None - if all_change: - # 获得ma5 < ma10 < ma20 < ma30的行 - short_market_data = market_data[(market_data["ma5"] < market_data["ma10"]) & - (market_data["ma10"] < market_data["ma20"]) & - (market_data["ma20"] < market_data["ma30"])] - logger.info(f"ma5 < ma10 < ma20 < ma30的行,数据条数: {len(short_market_data)}") - else: - # ma5 < ma10 or close < ma20 - short_market_data = market_data[(market_data["ma5"] < market_data["ma10"]) | - (market_data["close"] < market_data["ma20"])] - logger.info(f"ma5 < ma10 or close < ma20的行,数据条数: {len(short_market_data)}") - # concat long_market_data和short_market_data - ma_break_market_data = pd.concat([long_market_data, short_market_data]) - # 按照timestamp排序 - ma_break_market_data = ma_break_market_data.sort_values(by="timestamp", ascending=True) - # 获得ma_break_market_data的close列 - ma_break_market_data.reset_index(drop=True, inplace=True) - ma_break_market_data_pair_list = [] - ma_break_market_data_pair = {} - for index, row in ma_break_market_data.iterrows(): - ma_cross = row["ma_cross"] - timestamp = row["timestamp"] - close = row["close"] - ma5 = row["ma5"] - ma10 = row["ma10"] - ma20 = row["ma20"] - ma30 = row["ma30"] - if pd.notna(ma_cross) and ma_cross is not None: - ma_cross = str(ma_cross) - buy_condition = False - if all_change: - 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) - if buy_condition: - ma_break_market_data_pair = {} - ma_break_market_data_pair["symbol"] = symbol - ma_break_market_data_pair["bar"] = bar - ma_break_market_data_pair["begin_timestamp"] = timestamp - ma_break_market_data_pair["begin_date_time"] = timestamp_to_datetime(timestamp) - ma_break_market_data_pair["begin_close"] = close - ma_break_market_data_pair["begin_ma5"] = ma5 - ma_break_market_data_pair["begin_ma10"] = ma10 - ma_break_market_data_pair["begin_ma20"] = ma20 - ma_break_market_data_pair["begin_ma30"] = ma30 - - if all_change: - change_condition = (ma5 < ma10 and ma10 < ma20 and ma20 < ma30) - else: - # change_condition = (ma5 < ma10 or ma10 < ma20 or ma20 < ma30) - change_condition = (ma5 < ma10) or (close < ma20) - - if change_condition: - if ma_break_market_data_pair.get("begin_timestamp", None) is None: - continue - ma_break_market_data_pair["end_timestamp"] = timestamp - ma_break_market_data_pair["end_date_time"] = timestamp_to_datetime(timestamp) - ma_break_market_data_pair["end_close"] = close - ma_break_market_data_pair["end_ma5"] = ma5 - ma_break_market_data_pair["end_ma10"] = ma10 - ma_break_market_data_pair["end_ma20"] = ma20 - ma_break_market_data_pair["end_ma30"] = ma30 - ma_break_market_data_pair["pct_chg"] = (close - ma_break_market_data_pair["begin_close"]) / ma_break_market_data_pair["begin_close"] - ma_break_market_data_pair["pct_chg"] = round(ma_break_market_data_pair["pct_chg"] * 100, 4) - ma_break_market_data_pair["interval_seconds"] = (timestamp - ma_break_market_data_pair["begin_timestamp"]) / 1000 - # 将interval转换为分钟 - ma_break_market_data_pair["interval_minutes"] = ma_break_market_data_pair["interval_seconds"] / 60 - ma_break_market_data_pair["interval_hours"] = ma_break_market_data_pair["interval_seconds"] / 3600 - ma_break_market_data_pair["interval_days"] = ma_break_market_data_pair["interval_seconds"] / 86400 - ma_break_market_data_pair_list.append(ma_break_market_data_pair) - ma_break_market_data_pair = {} - if len(ma_break_market_data_pair_list) > 0: - ma_break_market_data = pd.DataFrame(ma_break_market_data_pair_list) - return ma_break_market_data - else: - return None - - def draw_pct_chg_mean_chart(self, data: pd.DataFrame, all_change: bool = True): - """ - 绘制pct_chg mean的柱状图表(美观,保存到self.stats_chart_dir) - :param data: 波段pct_chg_mean的数据 - :return: None - """ - if data is None or data.empty: - return None - # seaborn风格设置 - sns.set_theme(style="whitegrid") - plt.rcParams["font.sans-serif"] = ["SimHei"] # 也可直接用字体名 - plt.rcParams["font.size"] = 11 # 设置字体大小 - plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 - chart_dict = {} - - for bar in data["bar"].unique(): - bar_data = data[data["bar"] == bar].copy() # 一次筛选即可 - if bar_data.empty: - continue - - bar_data.rename(columns={"pct_chg_mean": "涨跌幅均值"}, inplace=True) - # 可选:按均值排序 - bar_data.sort_values(by="涨跌幅均值", ascending=False, inplace=True) - bar_data.reset_index(drop=True, inplace=True) - - plt.figure(figsize=(10, 6)) - sns.barplot(x="symbol", y="涨跌幅均值", data=bar_data, palette="Blues_d") - plt.title(f"{bar}趋势涨跌幅均值分布") - plt.xlabel("symbol") - plt.ylabel("涨跌幅均值") - plt.xticks(rotation=45, ha="right") - plt.tight_layout() - - if all_change: - save_path = os.path.join(self.stats_chart_dir, f"{bar}_ma_break_pct_chg_mean_all_change.png") - else: - save_path = os.path.join(self.stats_chart_dir, f"{bar}_ma_break_pct_chg_mean_part_change.png") - plt.savefig(save_path, dpi=150) - plt.close() - - if all_change: - sheet_name = f"{bar}_趋势涨跌幅均值分布图表_完全转势" - else: - sheet_name = f"{bar}_趋势涨跌幅均值分布图表_部分转势" - chart_dict[sheet_name] = save_path - return chart_dict - - def output_chart_to_excel(self, excel_file_path: str, charts_dict: dict): - """ - 输出Excel文件,包含所有图表 - charts_dict: 图表数据字典,格式为: - { - "sheet_name": { - "chart_name": "chart_path" - } - } - """ - logger.info(f"将图表输出到{excel_file_path}") - - # 打开已经存在的Excel文件 - wb = openpyxl.load_workbook(excel_file_path) - - for sheet_name, chart_path in charts_dict.items(): - try: - ws = wb.create_sheet(title=sheet_name) - row_offset = 1 - # Insert chart image - img = Image(chart_path) - ws.add_image(img, f"A{row_offset}") - - except Exception as e: - logger.error(f"输出Excel Sheet {sheet_name} 失败: {e}") - continue - # Save Excel file - wb.save(excel_file_path) - print(f"Chart saved as {excel_file_path}") \ No newline at end of file diff --git a/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc b/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22ba36c6a542aa3ec4b24828509a7813e21a43e5 GIT binary patch literal 21727 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 literal 0 HcmV?d00001 diff --git a/core/trade/ma_break_statistics.py b/core/trade/ma_break_statistics.py new file mode 100644 index 0000000..82936f8 --- /dev/null +++ b/core/trade/ma_break_statistics.py @@ -0,0 +1,506 @@ +import core.logger as logging +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 +import json +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 +from core.db.db_market_data import DBMarketData +from core.db.db_huge_volume_data import DBHugeVolumeData +from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp + +# seaborn支持中文 +plt.rcParams["font.family"] = ["SimHei"] + +logger = logging.logger + + +class MaBreakStatistics: + """ + 统计MA突破之后的涨跌幅 + MA向上突破的点位周期K线:5 > 10 > 20 > 30 + 统计MA向上突破的点位周期K线,突破之后,到: + 下一个MA向下突破的点位周期K线:30 > 20 > 10 > 5 + 之间的涨跌幅 + """ + + def __init__(self): + 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_market_data = DBMarketData(self.db_url) + self.db_huge_volume_data = DBHugeVolumeData(self.db_url) + 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.stats_output_dir = "./output/trade_sandbox/ma_strategy/excel/" + os.makedirs(self.stats_output_dir, exist_ok=True) + self.stats_chart_dir = "./output/trade_sandbox/ma_strategy/chart/" + os.makedirs(self.stats_chart_dir, exist_ok=True) + self.trade_strategy_config = self.get_trade_strategy_config() + self.main_strategy = self.trade_strategy_config.get("均线系统策略", None) + + def get_trade_strategy_config(self): + with open("./json/trade_strategy.json", "r", encoding="utf-8") as f: + trade_strategy_config = json.load(f) + return trade_strategy_config + + def batch_statistics(self, strategy_name: str = "全均线策略"): + self.stats_output_dir = f"./output/trade_sandbox/ma_strategy/excel/{strategy_name}/" + os.makedirs(self.stats_output_dir, exist_ok=True) + self.stats_chart_dir = f"./output/trade_sandbox/ma_strategy/chart/{strategy_name}/" + os.makedirs(self.stats_chart_dir, exist_ok=True) + ma_break_market_data_list = [] + if strategy_name not in self.main_strategy.keys() or strategy_name is None: + strategy_name = "全均线策略" + for symbol in self.symbols: + for bar in self.bars: + logger.info(f"开始计算{symbol} {bar}的MA突破区间涨跌幅统计, 策略: {strategy_name}") + ma_break_market_data = self.trade_simulate(symbol, bar, strategy_name) + if ma_break_market_data is not None: + ma_break_market_data_list.append(ma_break_market_data) + if len(ma_break_market_data_list) > 0: + ma_break_market_data = pd.concat(ma_break_market_data_list) + # 依据symbol和bar分组,统计每个symbol和bar的pct_chg的max, min, mean, std, median, count + pct_chg_df = ( + ma_break_market_data.groupby(["symbol", "bar"])["pct_chg"] + .agg( + pct_chg_sum="sum", + pct_chg_max="max", + pct_chg_min="min", + pct_chg_mean="mean", + pct_chg_std="std", + pct_chg_median="median", + pct_chg_count="count", + ) + .reset_index() + ) + # 依据symbol和bar分组,统计每个symbol和bar的interval_minutes的max, min, mean, std, median, count + interval_minutes_df = ( + ma_break_market_data.groupby(["symbol", "bar"])["interval_minutes"] + .agg( + interval_minutes_max="max", + interval_minutes_min="min", + interval_minutes_mean="mean", + interval_minutes_std="std", + interval_minutes_median="median", + interval_minutes_count="count", + ) + .reset_index() + ) + + earliest_market_date_time = ma_break_market_data["begin_date_time"].min() + earliest_market_date_time = re.sub( + r"[\:\-\s]", "", str(earliest_market_date_time) + ) + latest_market_date_time = ma_break_market_data["end_date_time"].max() + if latest_market_date_time is None: + latest_market_date_time = datetime.now().strftime("%Y%m%d") + latest_market_date_time = re.sub( + r"[\:\-\s]", "", str(latest_market_date_time) + ) + output_file_name = f"ma_break_stats_from_{earliest_market_date_time}_to_{latest_market_date_time}_{strategy_name}.xlsx" + output_file_path = os.path.join(self.stats_output_dir, output_file_name) + logger.info(f"导出{output_file_path}") + strategy_info_df = self.get_strategy_info(strategy_name) + with pd.ExcelWriter(output_file_path) as writer: + strategy_info_df.to_excel(writer, sheet_name="策略信息", index=False) + ma_break_market_data.to_excel( + writer, sheet_name="买卖记录明细", index=False + ) + pct_chg_df.to_excel(writer, sheet_name="买卖涨跌幅统计", index=False) + interval_minutes_df.to_excel( + writer, sheet_name="买卖时间间隔统计", index=False + ) + + chart_dict = self.draw_pct_chg_mean_chart(pct_chg_df, strategy_name) + self.output_chart_to_excel(output_file_path, chart_dict) + else: + return None + + def get_strategy_info(self, strategy_name: str = "全均线策略"): + strategy_config = self.main_strategy.get(strategy_name, None) + if strategy_config is None: + logger.error(f"策略{strategy_name}不存在") + return None + strategy_info = {"策略名称": strategy_name, "买入策略": "", "卖出策略": ""} + buy_dict = strategy_config.get("buy", {}) + buy_and_list = buy_dict.get("and", []) + buy_or_list = buy_dict.get("or", []) + buy_and_text = "" + buy_or_text = "" + for and_condition in buy_and_list: + buy_and_text += f"{and_condition}, \n" + if len(buy_or_list) > 0: + for or_condition in buy_or_list: + buy_or_text += f"{or_condition}, \n" + if len(buy_or_text) > 0: + strategy_info["买入策略"] = buy_and_text + " 或者 \n" + buy_or_text + else: + strategy_info["买入策略"] = buy_and_text + sell_dict = strategy_config.get("sell", {}) + sell_and_list = sell_dict.get("and", []) + sell_or_list = sell_dict.get("or", []) + sell_and_text = "" + sell_or_text = "" + for and_condition in sell_and_list: + sell_and_text += f"{and_condition}, \n" + if len(sell_or_list) > 0: + for or_condition in sell_or_list: + sell_or_text += f"{or_condition}, \n" + if len(sell_or_text) > 0: + strategy_info["卖出策略"] = sell_and_text + " 或者 \n" + sell_or_text + else: + strategy_info["卖出策略"] = sell_and_text + # 将strategy_info转换为pd.DataFrame + strategy_info_df = pd.DataFrame([strategy_info]) + return strategy_info_df + + def trade_simulate(self, symbol: str, bar: str, strategy_name: str = "全均线策略"): + market_data = self.db_market_data.query_market_data_by_symbol_bar( + symbol, bar, start=None, end=None + ) + if market_data is None or len(market_data) == 0: + logger.warning(f"获取{symbol} {bar} 数据失败") + return + else: + market_data = pd.DataFrame(market_data) + market_data.sort_values(by="timestamp", ascending=True, inplace=True) + market_data.reset_index(drop=True, inplace=True) + logger.info(f"获取{symbol} {bar} 数据成功,数据条数: {len(market_data)}") + # 获得ma5, ma10, ma20, ma30不为空的行 + market_data = market_data[ + (market_data["ma5"].notna()) + & (market_data["ma10"].notna()) + & (market_data["ma20"].notna()) + & (market_data["ma30"].notna()) + ] + logger.info( + f"ma5, ma10, ma20, ma30不为空的行,数据条数: {len(market_data)}" + ) + # 计算volume_ma5 + market_data["volume_ma5"] = market_data["volume"].rolling(window=5).mean() + # 获得5上穿10且ma5 > ma10 > ma20 > ma30且close > ma20的行,成交量较前5日均量放大20%以上 + market_data["volume_pct_chg"] = ( + market_data["volume"] - market_data["volume_ma5"] + ) / market_data["volume_ma5"] + market_data["volume_pct_chg"] = market_data["volume_pct_chg"].fillna(0) + + # 按照timestamp排序 + market_data = market_data.sort_values(by="timestamp", ascending=True) + # 获得ma_break_market_data的close列 + market_data.reset_index(drop=True, inplace=True) + ma_break_market_data_pair_list = [] + ma_break_market_data_pair = {} + for index, row in market_data.iterrows(): + ma_cross = row["ma_cross"] + timestamp = row["timestamp"] + close = row["close"] + ma5 = row["ma5"] + ma10 = row["ma10"] + ma20 = row["ma20"] + ma30 = row["ma30"] + macd_diff = float(row["dif"]) + macd_dea = float(row["dea"]) + macd = float(row["macd"]) + + if ma_break_market_data_pair.get("begin_timestamp", None) is None: + buy_condition = self.fit_strategy( + strategy_name=strategy_name, + market_data=market_data, + row=row, + behavior="buy", + ) + + if buy_condition: + ma_break_market_data_pair = {} + ma_break_market_data_pair["symbol"] = symbol + ma_break_market_data_pair["bar"] = bar + ma_break_market_data_pair["begin_timestamp"] = timestamp + ma_break_market_data_pair["begin_date_time"] = ( + timestamp_to_datetime(timestamp) + ) + ma_break_market_data_pair["begin_close"] = close + ma_break_market_data_pair["begin_ma5"] = ma5 + ma_break_market_data_pair["begin_ma10"] = ma10 + ma_break_market_data_pair["begin_ma20"] = ma20 + ma_break_market_data_pair["begin_ma30"] = ma30 + ma_break_market_data_pair["begin_macd_diff"] = macd_diff + ma_break_market_data_pair["begin_macd_dea"] = macd_dea + ma_break_market_data_pair["begin_macd"] = macd + continue + else: + sell_condition = self.fit_strategy( + strategy_name=strategy_name, + market_data=market_data, + row=row, + behavior="sell", + ) + + if sell_condition: + ma_break_market_data_pair["end_timestamp"] = timestamp + ma_break_market_data_pair["end_date_time"] = ( + timestamp_to_datetime(timestamp) + ) + ma_break_market_data_pair["end_close"] = close + ma_break_market_data_pair["end_ma5"] = ma5 + ma_break_market_data_pair["end_ma10"] = ma10 + ma_break_market_data_pair["end_ma20"] = ma20 + ma_break_market_data_pair["end_ma30"] = ma30 + ma_break_market_data_pair["end_macd_diff"] = macd_diff + ma_break_market_data_pair["end_macd_dea"] = macd_dea + ma_break_market_data_pair["end_macd"] = macd + ma_break_market_data_pair["pct_chg"] = ( + close - ma_break_market_data_pair["begin_close"] + ) / ma_break_market_data_pair["begin_close"] + ma_break_market_data_pair["pct_chg"] = round( + ma_break_market_data_pair["pct_chg"] * 100, 4 + ) + ma_break_market_data_pair["interval_seconds"] = ( + timestamp - ma_break_market_data_pair["begin_timestamp"] + ) / 1000 + # 将interval转换为分钟 + ma_break_market_data_pair["interval_minutes"] = ( + ma_break_market_data_pair["interval_seconds"] / 60 + ) + ma_break_market_data_pair["interval_hours"] = ( + ma_break_market_data_pair["interval_seconds"] / 3600 + ) + ma_break_market_data_pair["interval_days"] = ( + ma_break_market_data_pair["interval_seconds"] / 86400 + ) + ma_break_market_data_pair_list.append(ma_break_market_data_pair) + ma_break_market_data_pair = {} + + if len(ma_break_market_data_pair_list) > 0: + ma_break_market_data = pd.DataFrame(ma_break_market_data_pair_list) + logger.info(f"获取{symbol} {bar} 的买卖记录明细成功, 买卖次数: {len(ma_break_market_data)}") + return ma_break_market_data + else: + return None + + def fit_strategy( + self, + strategy_name: str = "全均线策略", + market_data: pd.DataFrame = None, + row: pd.Series = None, + behavior: str = "buy", + ): + strategy_config = self.main_strategy.get(strategy_name, None) + if strategy_config is None: + logger.error(f"策略{strategy_name}不存在") + return False + condition_dict = strategy_config.get(behavior, None) + if condition_dict is None: + logger.error(f"策略{strategy_name}的{behavior}条件不存在") + return False + ma_cross = row["ma_cross"] + if pd.isna(ma_cross) or ma_cross is None: + ma_cross = "" + ma_cross = str(ma_cross) + ma5 = float(row["ma5"]) + ma10 = float(row["ma10"]) + ma20 = float(row["ma20"]) + ma30 = float(row["ma30"]) + close = float(row["close"]) + volume_pct_chg = float(row["volume_pct_chg"]) + macd_diff = float(row["dif"]) + macd_dea = float(row["dea"]) + macd = float(row["macd"]) + + and_list = condition_dict.get("and", []) + + condition = True + for and_condition in and_list: + if and_condition == "5上穿10": + condition = condition and ("5上穿10" in ma_cross) + elif and_condition == "10上穿20": + condition = condition and ("10上穿20" in ma_cross) + elif and_condition == "20上穿30": + condition = condition and ("20上穿30" in ma_cross) + elif and_condition == "ma5>ma10": + condition = condition and (ma5 > ma10) + elif and_condition == "ma10>ma20": + condition = condition and (ma10 > ma20) + elif and_condition == "ma20>ma30": + condition = condition and (ma20 > ma30) + elif and_condition == "close>ma20": + condition = condition and (close > ma20) + elif and_condition == "volume_pct_chg>0.2": + condition = condition and (volume_pct_chg > 0.2) + elif and_condition == "macd_diff>0": + condition = condition and (macd_diff > 0) + elif and_condition == "macd_dea>0": + condition = condition and (macd_dea > 0) + elif and_condition == "macd>0": + condition = condition and (macd > 0) + elif and_condition == "10下穿5": + condition = condition and ("10下穿5" in ma_cross) + elif and_condition == "20下穿10": + condition = condition and ("20下穿10" in ma_cross) + elif and_condition == "30下穿20": + condition = condition and ("30下穿20" in ma_cross) + elif and_condition == "ma5ma10": + condition = condition or (ma5 > ma10) + elif or_condition == "ma10>ma20": + condition = condition or (ma10 > ma20) + elif or_condition == "ma20>ma30": + condition = condition or (ma20 > ma30) + elif or_condition == "close>ma20": + condition = condition or (close > ma20) + elif or_condition == "volume_pct_chg>0.2": + condition = condition or (volume_pct_chg > 0.2) + elif or_condition == "macd_diff>0": + condition = condition or (macd_diff > 0) + elif or_condition == "macd_dea>0": + condition = condition or (macd_dea > 0) + elif or_condition == "macd>0": + condition = condition or (macd > 0) + elif or_condition == "10下穿5": + condition = condition or ("10下穿5" in ma_cross) + elif or_condition == "20下穿10": + condition = condition or ("20下穿10" in ma_cross) + elif or_condition == "30下穿20": + condition = condition or ("30下穿20" in ma_cross) + elif or_condition == "ma5ma10", + "ma10>ma20", + "ma20>ma30", + "close>ma20", + "volume_pct_chg>0.2" + ] + }, + "sell": { + "and": [ + "ma50.2" + ] + } + }, + "5上穿10策略": { + "buy": { + "and": [ + "5上穿10", + "ma5>ma10", + "volume_pct_chg>0.2" + ] + }, + "sell": { + "and": [ + "ma50.2" + ], + "or": [ + "close < ma20" + ] + } + }, + "三均线策略": { + "buy": { + "and": [ + "ma5>ma10", + "ma10>ma20", + "10上穿20", + "volume_pct_chg>0.2" + ] + }, + "sell": { + "and": [ + "ma5ma10", + "close>ma20" + ] + }, + "sell": { + "and": [ + "ma5ma10", + "macd_diff>0" + ] + }, + "sell": { + "and": [ + "ma5ma10", + "macd_diff>0", + "macd>0" + ] + }, + "sell": { + "and": [ + "ma5