From 18168010ce9c61e0b38b2faffea10202c53c85e6 Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Thu, 9 Oct 2025 11:29:00 +0800 Subject: [PATCH] QUANT optimize --- core/biz/metrics_calculation.py | 191 ++++++++ .../ma_break_statistics.cpython-312.pyc | Bin 52539 -> 54388 bytes core/trade/ma_break_statistics.py | 444 ++++++++++++------ trade_ma_strategy_main.py | 238 ++++++++-- 4 files changed, 681 insertions(+), 192 deletions(-) diff --git a/core/biz/metrics_calculation.py b/core/biz/metrics_calculation.py index 659c26b..15d9570 100644 --- a/core/biz/metrics_calculation.py +++ b/core/biz/metrics_calculation.py @@ -49,6 +49,7 @@ import core.logger as logging import pandas as pd import numpy as np import talib as tb +from typing import List, Tuple from talib import MA_Type logger = logging.logger @@ -306,6 +307,40 @@ class MetricsCalculation: return data + def calculate_percentile_indicators( + self, + data: pd.DataFrame, + window_size: int = 50, + price_column: str = "close", + percentiles: List[Tuple[float, str]] = [(0.8, "80"), (0.2, "20"), (0.9, "90"), (0.1, "10")] + ) -> pd.DataFrame: + """ + 计算分位数指标 + :param data: 数据DataFrame + :param window_size: 窗口大小 + :param percentiles: 分位数配置列表,格式为[(分位数, 名称后缀)] + :return: 包含分位数指标的DataFrame + """ + for percentile, suffix in percentiles: + # 计算分位数 + data[f"{price_column}_{suffix}_percentile"] = ( + data[price_column].rolling(window=window_size, min_periods=1).quantile(percentile) + ) + + # 判断价格是否达到分位数 + if suffix in ["80", "90"]: + # 高点分位数 + data[f"{price_column}_{suffix}_high"] = ( + data[price_column] >= data[f"{price_column}_{suffix}_percentile"] + ).astype(int) + else: + # 低点分位数 + data[f"{price_column}_{suffix}_low"] = ( + data[price_column] <= data[f"{price_column}_{suffix}_percentile"] + ).astype(int) + + return data + def update_macd_divergence_column(self, df: pd.DataFrame): """ 更新整个DataFrame的macd_divergence列 @@ -1217,3 +1252,159 @@ class MetricsCalculation: avg_spacing = (spacing_5_10 + spacing_10_20 + spacing_20_30) / 3 return avg_spacing + + def get_peaks_valleys_mean(self, data: pd.DataFrame): + """计算上涨波峰和下跌波谷的均值与中位数""" + # 确保输入数据包含必要的列 + if not all(col in data.columns for col in ["open", "high", "low", "close"]): + raise ValueError( + "DataFrame must contain 'open', 'high', 'low', 'close' columns" + ) + + if len(data) < 100: + return None, None + + window = 5 + # 初始化结果列表 + peaks_valleys = [] + + # 检测波峰(基于high价格) + highs = data["high"] + for i in range(window, len(data) - window): + if i + window >= len(data): + break + # 当前K线的high价格 + current_high = highs.iloc[i] + # 窗口内的前后K线的high价格 + window_highs = highs.iloc[i - window : i + window + 1] + # 如果当前high是窗口内的最大值,标记为波峰 + if ( + current_high == window_highs.max() + and current_high > highs.iloc[i - 1] + and current_high > highs.iloc[i + 1] + ): + peaks_valleys.append( + { + "symbol": data.iloc[i]["symbol"], + "bar": data.iloc[i]["bar"], + "timestamp": data.iloc[i]["timestamp"], + "date_time": data.iloc[i]["date_time"], + "price": current_high, + "type": "peak", + } + ) + + # 检测波谷(基于low价格) + lows = data["low"] + for i in range(window, len(data) - window): + if i + window >= len(data): + break + # 当前K线的low价格 + current_low = lows.iloc[i] + # 窗口内的前后K线的low价格 + window_lows = lows.iloc[i - window : i + window + 1] + # 如果当前low是窗口内的最小值,标记为波谷 + if ( + current_low == window_lows.min() + and current_low < lows.iloc[i - 1] + and current_low < lows.iloc[i + 1] + ): + peaks_valleys.append( + { + "symbol": data.iloc[i]["symbol"], + "bar": data.iloc[i]["bar"], + "timestamp": data.iloc[i]["timestamp"], + "date_time": data.iloc[i]["date_time"], + "price": current_low, + "type": "valley", + } + ) + + # 转换为DataFrame并按时间排序 + result_df = pd.DataFrame(peaks_valleys) + if not result_df.empty: + result_df = result_df.sort_values(by="timestamp").reset_index(drop=True) + else: + result_df = pd.DataFrame( + columns=["symbol", "timestamp", "date_time", "bar", "price", "type"] + ) + + # 检查result_df,如果type为peak时,下一条数据type依然为peak,则删除当前数据 + if not result_df.empty: + # 使用布尔索引来标记要删除的行 + to_drop_peaks = [] + handled_indexes = [] + for i in range(len(result_df) - 1): + if i in handled_indexes: + continue + if result_df.iloc[i]["type"] == "peak": + current_peak_value = result_df.iloc[i]["price"] + current_peak_index = i + # 如type连续为peak,只应该保留price最大的行,删除其他行 + # 如type连续为peak且存在price为8 7 10 9 8 11 10的情况,只应该保留price为11的行 + for j in range(i + 1, len(result_df)): + if result_df.iloc[j]["type"] == "peak": + next_peak_value = result_df.iloc[j]["price"] + if current_peak_value > next_peak_value: + to_drop_peaks.append(j) + else: + to_drop_peaks.append(current_peak_index) + current_peak_value = next_peak_value + current_peak_index = j + handled_indexes.append(j) + else: + break + + # 删除标记的行 + result_df = result_df.drop(to_drop_peaks).reset_index(drop=True) + + # 如type连续为valley,只应该保留price最小的行,删除其他行 + # 如type连续为valley且存在price为8 7 10 9 8的情况,只应该保留price为7的行 + to_drop_valleys = [] + handled_indexes = [] + for i in range(len(result_df) - 1): + if i in handled_indexes: + continue + if result_df.iloc[i]["type"] == "valley": + current_valley_value = result_df.iloc[i]["price"] + current_valley_index = i + for j in range(i + 1, len(result_df)): + if result_df.iloc[j]["type"] == "valley": + next_valley_value = result_df.iloc[j]["price"] + if current_valley_value < next_valley_value: + to_drop_valleys.append(j) + else: + to_drop_valleys.append(current_valley_index) + current_valley_value = next_valley_value + current_valley_index = j + handled_indexes.append(j) + else: + break + + # 删除标记的行 + result_df = result_df.drop(to_drop_valleys).reset_index(drop=True) + # 初始化价格变化列 + result_df["price_change"] = 0.0 + result_df["price_change_ratio"] = 0.0 + + # 计算下一条数据与当前数据之间的价格差,并计算价格差与当前数据价格的比率 + peaks_mean = None + valleys_mean = None + if len(result_df) > 1: + for i in range(len(result_df) - 1): + result_df.iloc[i + 1, result_df.columns.get_loc("price_change")] = ( + result_df.iloc[i + 1]["price"] - result_df.iloc[i]["price"] + ) + result_df.iloc[ + i + 1, result_df.columns.get_loc("price_change_ratio") + ] = ( + result_df.iloc[i + 1]["price_change"] / result_df.iloc[i]["price"] + ) * 100 + # peaks mean为result_df中price_change_ratio > 0的price_change_ratio的均值与中位数 + peaks_mean = abs(float(result_df[result_df["price_change_ratio"] > 0]["price_change_ratio"].mean())) + peaks_median = abs(float(result_df[result_df["price_change_ratio"] > 0]["price_change_ratio"].median())) + # valleys mean为result_df中price_change_ratio < 0的price_change_ratio的均值与中位数 + valleys_mean = abs(float(result_df[result_df["price_change_ratio"] < 0]["price_change_ratio"].mean())) + valleys_median = abs(float(result_df[result_df["price_change_ratio"] < 0]["price_change_ratio"].median())) + result = {"peaks_valleys_data": result_df, "peaks_mean": peaks_mean, "peaks_median": peaks_median, "valleys_mean": valleys_mean, "valleys_median": valleys_median} + return result diff --git a/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc b/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc index 3c88a56e852e1acb03c1cd6fd9846ef84707876c..3507a497adc53c86b968d06a3029f796c7ca1ced 100644 GIT binary patch delta 13838 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 b1-<>;V2{6!&m%V`4j49A*s9MMM5^>(v_l*& diff --git a/core/trade/ma_break_statistics.py b/core/trade/ma_break_statistics.py index 140b733..a374334 100644 --- a/core/trade/ma_break_statistics.py +++ b/core/trade/ma_break_statistics.py @@ -52,8 +52,18 @@ class MaBreakStatistics: is_astock: bool = False, is_aindex: bool = False, is_binance: bool = True, - buy_by_long_period: dict = {"by_week": False, "by_month": False}, - long_period_condition: dict = {"ma5>ma10": True, "ma10>ma20": False, "macd_diff>0": True, "macd>0": True}, + buy_by_long_period: dict = { + "by_week": False, + "by_month": False, + "buy_by_10_percentile": False, + }, + long_period_condition: dict = { + "ma5>ma10": True, + "ma10>ma20": False, + "macd_diff>0": True, + "macd>0": True, + }, + cut_loss_by_valleys_median: bool = False, commission_per_share: float = 0.0008, ): if is_astock or is_aindex: @@ -146,6 +156,8 @@ class MaBreakStatistics: self.main_strategy = self.trade_strategy_config.get("均线系统策略", None) self.buy_by_long_period = buy_by_long_period self.long_period_condition = long_period_condition + self.cut_loss_by_valleys_median = cut_loss_by_valleys_median + self.metrics_calculation = MetricsCalculation() def get_trade_strategy_config(self): with open("./json/trade_strategy.json", "r", encoding="utf-8") as f: @@ -160,6 +172,8 @@ class MaBreakStatistics: by_long_period += "1W" if by_month: by_long_period += "1M" + if self.buy_by_long_period.get("buy_by_10_percentile", False): + by_long_period += "_10percentile" if by_long_period == "": return "no_long_period_judge" by_condition = "" @@ -172,62 +186,66 @@ class MaBreakStatistics: if self.long_period_condition.get("macd>0", False): by_condition += "_macdgt0" return by_long_period + "_" + by_condition - def batch_statistics(self, strategy_name: str = "全均线策略"): if self.is_us_stock: - self.stats_output_dir = ( - f"./output/trade_sandbox/ma_strategy/us_stock/excel/{strategy_name}/" - ) - self.stats_chart_dir = ( - f"./output/trade_sandbox/ma_strategy/us_stock/chart/{strategy_name}/" - ) + main_folder = "./output/trade_sandbox/ma_strategy/us_stock/" + if self.cut_loss_by_valleys_median: + main_folder += "cut_loss_by_valleys_median/" + else: + main_folder += "no_cut_loss_by_valleys_median/" + self.stats_output_dir = f"{main_folder}excel/{strategy_name}/" + self.stats_chart_dir = f"{main_folder}chart/{strategy_name}/" elif self.is_binance: - self.stats_output_dir = ( - f"./output/trade_sandbox/ma_strategy/binance/excel/{strategy_name}/" - ) - self.stats_chart_dir = ( - f"./output/trade_sandbox/ma_strategy/binance/chart/{strategy_name}/" - ) + main_folder = "./output/trade_sandbox/ma_strategy/binance/" + if self.cut_loss_by_valleys_median: + main_folder += "cut_loss_by_valleys_median/" + else: + main_folder += "no_cut_loss_by_valleys_median/" + self.stats_output_dir = f"{main_folder}excel/{strategy_name}/" + self.stats_chart_dir = f"{main_folder}chart/{strategy_name}/" elif self.is_astock: long_period_desc = self.get_by_long_period_desc() + main_folder = "./output/trade_sandbox/ma_strategy/astock/" + if self.cut_loss_by_valleys_median: + main_folder += "cut_loss_by_valleys_median/" + else: + main_folder += "no_cut_loss_by_valleys_median/" if len(long_period_desc) > 0: self.stats_output_dir = ( - f"./output/trade_sandbox/ma_strategy/astock/{long_period_desc}/excel/{strategy_name}/" + f"{main_folder}{long_period_desc}/excel/{strategy_name}/" ) self.stats_chart_dir = ( - f"./output/trade_sandbox/ma_strategy/astock/{long_period_desc}/chart/{strategy_name}/" + f"{main_folder}{long_period_desc}/chart/{strategy_name}/" ) else: - self.stats_output_dir = ( - f"./output/trade_sandbox/ma_strategy/astock/excel/{strategy_name}/" - ) - self.stats_chart_dir = ( - f"./output/trade_sandbox/ma_strategy/astock/chart/{strategy_name}/" - ) + self.stats_output_dir = f"{main_folder}excel/{strategy_name}/" + self.stats_chart_dir = f"{main_folder}chart/{strategy_name}/" elif self.is_aindex: + main_folder = "./output/trade_sandbox/ma_strategy/aindex/" + if self.cut_loss_by_valleys_median: + main_folder += "cut_loss_by_valleys_median/" + else: + main_folder += "no_cut_loss_by_valleys_median/" long_period_desc = self.get_by_long_period_desc() if len(long_period_desc) > 0: self.stats_output_dir = ( - f"./output/trade_sandbox/ma_strategy/aindex/{long_period_desc}/excel/{strategy_name}/" + f"{main_folder}{long_period_desc}/excel/{strategy_name}/" ) self.stats_chart_dir = ( - f"./output/trade_sandbox/ma_strategy/aindex/{long_period_desc}/chart/{strategy_name}/" + f"{main_folder}{long_period_desc}/chart/{strategy_name}/" ) else: - self.stats_output_dir = ( - f"./output/trade_sandbox/ma_strategy/aindex/excel/{strategy_name}/" - ) - self.stats_chart_dir = ( - f"./output/trade_sandbox/ma_strategy/aindex/chart/{strategy_name}/" - ) + self.stats_output_dir = f"{main_folder}excel/{strategy_name}/" + self.stats_chart_dir = f"{main_folder}chart/{strategy_name}/" else: - self.stats_output_dir = ( - f"./output/trade_sandbox/ma_strategy/okx/excel/{strategy_name}/" - ) - self.stats_chart_dir = ( - f"./output/trade_sandbox/ma_strategy/okx/chart/{strategy_name}/" - ) + main_folder = "./output/trade_sandbox/ma_strategy/okx/" + if self.cut_loss_by_valleys_median: + main_folder += "cut_loss_by_valleys_median/" + else: + main_folder += "no_cut_loss_by_valleys_median/" + self.stats_output_dir = f"{main_folder}excel/{strategy_name}/" + self.stats_chart_dir = f"{main_folder}chart/{strategy_name}/" os.makedirs(self.stats_output_dir, exist_ok=True) os.makedirs(self.stats_chart_dir, exist_ok=True) @@ -275,39 +293,53 @@ class MaBreakStatistics: by="end_timestamp", ascending=True, inplace=True ) symbol_bar_data.reset_index(drop=True, inplace=True) - initial_capital = int(market_data_pct_chg_df.loc[ - (market_data_pct_chg_df["symbol"] == symbol) - & (market_data_pct_chg_df["bar"] == bar), - "initial_capital", - ].values[0]) - final_account_value = float(symbol_bar_data["end_account_value"].iloc[-1]) - account_value_chg = (final_account_value - initial_capital) / initial_capital * 100 + initial_capital = int( + market_data_pct_chg_df.loc[ + (market_data_pct_chg_df["symbol"] == symbol) + & (market_data_pct_chg_df["bar"] == bar), + "initial_capital", + ].values[0] + ) + final_account_value = float( + symbol_bar_data["end_account_value"].iloc[-1] + ) + account_value_chg = ( + (final_account_value - initial_capital) + / initial_capital + * 100 + ) account_value_chg = round(account_value_chg, 4) market_pct_chg = market_data_pct_chg_df.loc[ (market_data_pct_chg_df["symbol"] == symbol) & (market_data_pct_chg_df["bar"] == bar), "pct_chg", ].values[0] - total_buy_commission = float(symbol_bar_data["buy_commission"].sum()) - total_sell_commission = float(symbol_bar_data["sell_commission"].sum()) + total_buy_commission = float( + symbol_bar_data["buy_commission"].sum() + ) + total_sell_commission = float( + symbol_bar_data["sell_commission"].sum() + ) total_commission = total_buy_commission + total_sell_commission total_commission = round(total_commission, 4) total_buy_commission = round(total_buy_commission, 4) total_sell_commission = round(total_sell_commission, 4) symbol_name = str(symbol_bar_data["symbol_name"].iloc[0]) - account_value_chg_list.append({ - "strategy_name": strategy_name, - "symbol": symbol, - "symbol_name": symbol_name, - "bar": bar, - "total_buy_commission": total_buy_commission, - "total_sell_commission": total_sell_commission, - "total_commission": total_commission, - "initial_account_value": initial_capital, - "final_account_value": final_account_value, - "account_value_chg": account_value_chg, - "market_pct_chg": market_pct_chg, - }) + account_value_chg_list.append( + { + "strategy_name": strategy_name, + "symbol": symbol, + "symbol_name": symbol_name, + "bar": bar, + "total_buy_commission": total_buy_commission, + "total_sell_commission": total_sell_commission, + "total_commission": total_commission, + "initial_account_value": initial_capital, + "final_account_value": final_account_value, + "account_value_chg": account_value_chg, + "market_pct_chg": market_pct_chg, + } + ) account_value_chg_df = pd.DataFrame(account_value_chg_list) account_value_chg_df = account_value_chg_df[ [ @@ -326,7 +358,9 @@ class MaBreakStatistics: ] account_value_statistics_df = ( - ma_break_market_data.groupby(["symbol", "symbol_name", "bar"])["end_account_value"] + ma_break_market_data.groupby(["symbol", "symbol_name", "bar"])[ + "end_account_value" + ] .agg( account_value_max="max", account_value_min="min", @@ -355,7 +389,9 @@ class MaBreakStatistics: # 依据symbol和bar分组,统计每个symbol和bar的interval_minutes的max, min, mean, std, median, count interval_minutes_df = ( - ma_break_market_data.groupby(["symbol", "symbol_name", "bar"])["interval_minutes"] + ma_break_market_data.groupby(["symbol", "symbol_name", "bar"])[ + "interval_minutes" + ] .agg( interval_minutes_max="max", interval_minutes_min="min", @@ -404,7 +440,9 @@ class MaBreakStatistics: ma_break_market_data.to_excel( writer, sheet_name="买卖记录明细", index=False ) - account_value_chg_df.to_excel(writer, sheet_name="资产价值变化", index=False) + account_value_chg_df.to_excel( + writer, sheet_name="资产价值变化", index=False + ) account_value_statistics_df.to_excel( writer, sheet_name="买卖账户价值统计", index=False ) @@ -412,7 +450,9 @@ class MaBreakStatistics: writer, sheet_name="买卖时间间隔统计", index=False ) - chart_dict = self.draw_quant_pct_chg_bar_chart(account_value_chg_df, strategy_name) + chart_dict = self.draw_quant_pct_chg_bar_chart( + account_value_chg_df, strategy_name + ) self.output_chart_to_excel(output_file_path, chart_dict) chart_dict = self.draw_quant_line_chart( ma_break_market_data, market_data_pct_chg_df, strategy_name @@ -442,7 +482,7 @@ class MaBreakStatistics: strategy_info["买入策略"] = buy_and_text + " 或者 \n" + buy_or_text else: strategy_info["买入策略"] = buy_and_text - + # 假如根据长周期判断买入,则需要设置长周期策略 by_week = self.buy_by_long_period.get("by_week", False) by_month = self.buy_by_long_period.get("by_month", False) @@ -540,6 +580,8 @@ class MaBreakStatistics: strategy_name=strategy_name, row=row, behavior="buy", + buy_price=None, + window_100_valleys_median=None, ) if buy_condition: @@ -574,10 +616,21 @@ class MaBreakStatistics: ma_break_market_data_pair["begin_account_value"] = account_value continue else: + valleys_median = None + if self.cut_loss_by_valleys_median and index >= 100: + window_100_records = market_data.iloc[index - 100 : index] + peaks_valleys = self.metrics_calculation.get_peaks_valleys_mean( + window_100_records + ) + valleys_median = peaks_valleys.get("valleys_median", None) + if valleys_median is not None and valleys_median > 0: + valleys_median = valleys_median / 100 sell_condition = self.fit_strategy( strategy_name=strategy_name, row=row, behavior="sell", + buy_price=ma_break_market_data_pair["begin_close"], + window_100_valleys_median=valleys_median, ) if sell_condition or index == len(market_data) - 1: @@ -612,6 +665,12 @@ class MaBreakStatistics: ma_break_market_data_pair["pct_chg"] = round( ma_break_market_data_pair["pct_chg"] * 100, 4 ) + if valleys_median is not None: + ma_break_market_data_pair["valleys_median"] = ( + valleys_median * 100 + ) + else: + ma_break_market_data_pair["valleys_median"] = None ma_break_market_data_pair["profit_loss"] = profit_loss ma_break_market_data_pair["sell_commission"] = sell_commission ma_break_market_data_pair["end_account_value"] = account_value @@ -804,7 +863,12 @@ class MaBreakStatistics: current_end_date_str = current_end_date.strftime("%Y-%m-%d") logger.info(f"获取{symbol}数据:{start_date_str}至{current_end_date_str}") current_data = self.db_market_data.query_market_data_by_symbol_bar( - symbol, bar, fields, start=start_date_str, end=current_end_date_str, table_name=table_name + symbol, + bar, + fields, + start=start_date_str, + end=current_end_date_str, + table_name=table_name, ) if current_data is not None and len(current_data) > 0: current_data = pd.DataFrame(current_data) @@ -816,7 +880,7 @@ class MaBreakStatistics: if self.is_astock or self.is_aindex: data = self.update_data(data) return data - + def get_long_period_data(self, symbol: str, bar: str, end_date: str): """ 获取长周期数据 @@ -843,56 +907,67 @@ class MaBreakStatistics: if len(end_date) != 10: end_date = self.change_date_format(end_date) if bar == "1M": - # 获取上两个月的日期 - last_date = datetime.strptime(end_date, "%Y-%m-%d") - timedelta(days=60) + # 获取上五年的日期 + last_date = datetime.strptime(end_date, "%Y-%m-%d") - timedelta( + days=360 * 5 + ) last_date = last_date.strftime("%Y-%m-%d") elif bar == "1W": - # 获取上两周的日期 - last_date = datetime.strptime(end_date, "%Y-%m-%d") - timedelta(days=14) + # 获取上两年的日期 + last_date = datetime.strptime(end_date, "%Y-%m-%d") - timedelta( + days=360 * 2 + ) last_date = last_date.strftime("%Y-%m-%d") else: last_date = None - + if len(table_name) == 0 or last_date is None: return None fields = [ - "a.ts_code as symbol", - "b.name as symbol_name", - f"'{bar}' as bar", - "0 as timestamp", - "trade_date as date_time", - "open", - "high", - "low", - "close", - "vol as volume", - "MA5 as ma5", - "MA10 as ma10", - "MA20 as ma20", - "MA30 as ma30", - "均线交叉 as ma_cross", - "DIF as dif", - "DEA as dea", - "MACD as macd", - ] + "a.ts_code as symbol", + "b.name as symbol_name", + f"'{bar}' as bar", + "0 as timestamp", + "trade_date as date_time", + "open", + "high", + "low", + "close", + "vol as volume", + "MA5 as ma5", + "MA10 as ma10", + "MA20 as ma20", + "MA30 as ma30", + "均线交叉 as ma_cross", + "DIF as dif", + "DEA as dea", + "MACD as macd", + ] data = self.db_market_data.query_market_data_by_symbol_bar( symbol, bar, fields, start=last_date, end=end_date, table_name=table_name ) if data is not None and len(data) > 0: data = pd.DataFrame(data) data.sort_values(by="date_time", inplace=True) + data = self.metrics_calculation.calculate_percentile_indicators( + data=data, + window_size=50, + price_column="close", + percentiles=[(0.1, "10")], + ) latest_row = data.iloc[-1] - if (latest_row["ma5"] is None or - latest_row["ma10"] is None or - latest_row["ma20"] is None or - latest_row["dif"] is None or - latest_row["macd"] is None): + if ( + latest_row["ma5"] is None + or latest_row["ma10"] is None + or latest_row["ma20"] is None + or latest_row["dif"] is None + or latest_row["macd"] is None + ): return None return latest_row else: return None - def update_data(self, data: pd.DataFrame): """ 更新数据 @@ -902,12 +977,15 @@ class MaBreakStatistics: :param data: 数据 :return: 更新后的数据 """ - data["date_time"] = data["date_time"].apply(lambda x: self.change_date_format(x)) - data["timestamp"] = data["date_time"].apply(lambda x: transform_date_time_to_timestamp(x)) - metrics_calculation = MetricsCalculation() - data = metrics_calculation.ma5102030(data) + data["date_time"] = data["date_time"].apply( + lambda x: self.change_date_format(x) + ) + data["timestamp"] = data["date_time"].apply( + lambda x: transform_date_time_to_timestamp(x) + ) + data = self.metrics_calculation.ma5102030(data) return data - + def change_date_format(self, date_text: str): # 将20210104这种格式,替换为2021-01-04的格式 if len(date_text) == 8: @@ -920,7 +998,23 @@ class MaBreakStatistics: strategy_name: str = "全均线策略", row: pd.Series = None, behavior: str = "buy", + buy_price: float = None, + window_100_valleys_median: float = None, ): + # 如果行为是卖出,则判断是否根据止损价格卖出 + # 止损价格 = 买入价格 * (1 - window_100_valleys_median) + # window_100_valleys_median为100日下跌波谷幅度中位数 + # 当前价格 < 止损价格,则卖出 + if ( + behavior == "sell" + and buy_price is not None + and window_100_valleys_median is not None + ): + current_price = float(row["close"]) + if current_price < buy_price: + loss_ratio = (buy_price - current_price) / buy_price + if loss_ratio > window_100_valleys_median: + return True strategy_config = self.main_strategy.get(strategy_name, None) if strategy_config is None: logger.error(f"策略{strategy_name}不存在") @@ -953,21 +1047,56 @@ class MaBreakStatistics: long_period_condition_list.append("macd>0") if len(long_period_condition_list) > 0: if self.buy_by_long_period.get("by_week", False): - long_period_data = self.get_long_period_data(row["symbol"], "1W", date_time) + long_period_data = self.get_long_period_data( + row["symbol"], "1W", date_time + ) if long_period_data is not None: - condition = self.get_judge_result(long_period_data, long_period_condition_list, "and", condition) + condition = self.get_judge_result( + long_period_data, + long_period_condition_list, + "and", + condition, + ) if not condition: - logger.info(f"根据周线指标,{row['symbol']}不满足买入条件") + # 如果周线处于空头条件,但收盘价位于50窗口的低点10分位数,则买入 + if self.buy_by_long_period.get("buy_by_10_percentile", False): + if long_period_data["close_10_low"] == 1: + condition = True + if not condition: + logger.info( + f"根据周线指标,{row['symbol']}不满足买入条件" + ) if self.buy_by_long_period.get("by_month", False): - long_period_data = self.get_long_period_data(row["symbol"], "1M", date_time) + long_period_data = self.get_long_period_data( + row["symbol"], "1M", date_time + ) if long_period_data is not None: - condition = self.get_judge_result(long_period_data, long_period_condition_list, "and", condition) + condition = self.get_judge_result( + long_period_data, + long_period_condition_list, + "and", + condition, + ) + if not condition: - logger.info(f"根据月线指标,{row['symbol']}不满足买入条件") + # 如果月线处于空头条件,但收盘价位于50窗口的低点10分位数,则买入 + if self.buy_by_long_period.get("buy_by_10_percentile", False): + if long_period_data["close_10_low"] == 1: + condition = True + if not condition: + logger.info( + f"根据月线指标,{row['symbol']}不满足买入条件" + ) return condition - - def get_judge_result(self, row: pd.Series, condition_list: list, and_or: str = "and", raw_condition: bool = True): + + def get_judge_result( + self, + row: pd.Series, + condition_list: list, + and_or: str = "and", + raw_condition: bool = True, + ): ma_cross = row["ma_cross"] if pd.isna(ma_cross) or ma_cross is None: ma_cross = "" @@ -985,51 +1114,53 @@ class MaBreakStatistics: macd_dea = float(row["dea"]) macd = float(row["macd"]) if and_or == "and": - for and_condition in condition_list: - if and_condition == "5上穿10": - raw_condition = raw_condition and ("5上穿10" in ma_cross) - elif and_condition == "10上穿20": - raw_condition = raw_condition and ("10上穿20" in ma_cross) - elif and_condition == "20上穿30": - raw_condition = raw_condition and ("20上穿30" in ma_cross) - elif and_condition == "ma5>ma10": - raw_condition = raw_condition and (ma5 > ma10) - elif and_condition == "ma10>ma20": - raw_condition = raw_condition and (ma10 > ma20) - elif and_condition == "ma20>ma30": - raw_condition = raw_condition and (ma20 > ma30) - elif and_condition == "close>ma20": - raw_condition = raw_condition and (close > ma20) - elif and_condition == "volume_pct_chg>0.2" and volume_pct_chg is not None: - raw_condition = raw_condition and (volume_pct_chg > 0.2) - elif and_condition == "macd_diff>0": - raw_condition = raw_condition and (macd_diff > 0) - elif and_condition == "macd_dea>0": - raw_condition = raw_condition and (macd_dea > 0) - elif and_condition == "macd>0": - raw_condition = raw_condition and (macd > 0) - elif and_condition == "10下穿5": - raw_condition = raw_condition and ("10下穿5" in ma_cross) - elif and_condition == "20下穿10": - raw_condition = raw_condition and ("20下穿10" in ma_cross) - elif and_condition == "30下穿20": - raw_condition = raw_condition and ("30下穿20" in ma_cross) - elif and_condition == "ma5ma10": + raw_condition = raw_condition and (ma5 > ma10) + elif and_condition == "ma10>ma20": + raw_condition = raw_condition and (ma10 > ma20) + elif and_condition == "ma20>ma30": + raw_condition = raw_condition and (ma20 > ma30) + elif and_condition == "close>ma20": + raw_condition = raw_condition and (close > ma20) + elif ( + and_condition == "volume_pct_chg>0.2" and volume_pct_chg is not None + ): + raw_condition = raw_condition and (volume_pct_chg > 0.2) + elif and_condition == "macd_diff>0": + raw_condition = raw_condition and (macd_diff > 0) + elif and_condition == "macd_dea>0": + raw_condition = raw_condition and (macd_dea > 0) + elif and_condition == "macd>0": + raw_condition = raw_condition and (macd > 0) + elif and_condition == "10下穿5": + raw_condition = raw_condition and ("10下穿5" in ma_cross) + elif and_condition == "20下穿10": + raw_condition = raw_condition and ("20下穿10" in ma_cross) + elif and_condition == "30下穿20": + raw_condition = raw_condition and ("30下穿20" in ma_cross) + elif and_condition == "ma5 ma30) elif or_condition == "close>ma20": raw_condition = raw_condition or (close > ma20) - elif or_condition == "volume_pct_chg>0.2" and volume_pct_chg is not None: + elif ( + or_condition == "volume_pct_chg>0.2" and volume_pct_chg is not None + ): raw_condition = raw_condition or (volume_pct_chg > 0.2) elif or_condition == "macd_diff>0": raw_condition = raw_condition or (macd_diff > 0) @@ -1078,7 +1211,6 @@ class MaBreakStatistics: pass return raw_condition - def draw_quant_pct_chg_bar_chart( self, data: pd.DataFrame, strategy_name: str = "全均线策略" ): diff --git a/trade_ma_strategy_main.py b/trade_ma_strategy_main.py index dd7c2bc..9ae5822 100644 --- a/trade_ma_strategy_main.py +++ b/trade_ma_strategy_main.py @@ -2,6 +2,7 @@ import core.logger as logging from datetime import datetime from time import sleep import pandas as pd +import os from core.biz.market_data import MarketData from core.trade.ma_break_statistics import MaBreakStatistics from core.db.db_market_data import DBMarketData @@ -33,7 +34,13 @@ class TradeMaStrategyMain: is_binance: bool = False, commission_per_share: float = 0, buy_by_long_period: dict = {"by_week": False, "by_month": False}, - long_period_condition: dict = {"ma5>ma10": False, "ma10>ma20": False, "macd_diff>0": False, "macd>0": False}, + long_period_condition: dict = { + "ma5>ma10": False, + "ma10>ma20": False, + "macd_diff>0": False, + "macd>0": False, + }, + cut_loss_by_valleys_median: bool = False, ): self.ma_break_statistics = MaBreakStatistics( is_us_stock=is_us_stock, @@ -43,6 +50,7 @@ class TradeMaStrategyMain: commission_per_share=commission_per_share, buy_by_long_period=buy_by_long_period, long_period_condition=long_period_condition, + cut_loss_by_valleys_median=cut_loss_by_valleys_median, ) def batch_ma_break_statistics(self): @@ -77,41 +85,199 @@ def test_single_symbol(): ) symbol = "600111.SH" bar = "1D" - ma_break_statistics.trade_simulate(symbol=symbol, bar=bar, strategy_name="均线macd结合策略2") + ma_break_statistics.trade_simulate( + symbol=symbol, bar=bar, strategy_name="均线macd结合策略2" + ) + + +def batch_run_strategy(): + commission_per_share_list = [0, 0.0008] + # cut_loss_by_valleys_median_list = [True, False] + cut_loss_by_valleys_median_list = [False] + buy_by_long_period_list = [ + # {"by_week": True, "by_month": True, "buy_by_10_percentile": True}, + {"by_week": False, "by_month": True, "buy_by_10_percentile": True}, + {"by_week": True, "by_month": False, "buy_by_10_percentile": True}, + {"by_week": False, "by_month": False, "buy_by_10_percentile": False}, + ] + # buy_by_long_period_list = [{"by_week": False, "by_month": False}] + long_period_condition_list = [ + {"ma5>ma10": True, "ma10>ma20": True, "macd_diff>0": True, "macd>0": True}, + {"ma5>ma10": True, "ma10>ma20": False, "macd_diff>0": True, "macd>0": True}, + {"ma5>ma10": False, "ma10>ma20": True, "macd_diff>0": True, "macd>0": True}, + ] + + for commission_per_share in commission_per_share_list: + for cut_loss_by_valleys_median in cut_loss_by_valleys_median_list: + for buy_by_long_period in buy_by_long_period_list: + for long_period_condition in long_period_condition_list: + logger.info( + f"开始计算, 主要参数:commission_per_share: {commission_per_share}, buy_by_long_period: {buy_by_long_period}, long_period_condition: {long_period_condition}" + ) + trade_ma_strategy_main = TradeMaStrategyMain( + is_us_stock=False, + is_astock=False, + is_aindex=True, + is_binance=False, + commission_per_share=commission_per_share, + buy_by_long_period=buy_by_long_period, + long_period_condition=long_period_condition, + cut_loss_by_valleys_median=cut_loss_by_valleys_median, + ) + trade_ma_strategy_main.batch_ma_break_statistics() + + trade_ma_strategy_main = TradeMaStrategyMain( + is_us_stock=False, + is_astock=True, + is_aindex=False, + is_binance=False, + commission_per_share=commission_per_share, + buy_by_long_period=buy_by_long_period, + long_period_condition=long_period_condition, + cut_loss_by_valleys_median=cut_loss_by_valleys_median, + ) + trade_ma_strategy_main.batch_ma_break_statistics() + + +def pickup_data_from_excel(): + main_path = r"./output/trade_sandbox/ma_strategy" + sub_main_paths = ["aindex", "astock"] + fix_sub_path = "no_cut_loss_by_valleys_median" + sub_folder = r"excel/均线macd结合策略2/" + file_feature_name = "with_commission" + original_df_columns = [ + "strategy_name", + "symbol", + "symbol_name", + "bar", + "total_buy_commission", + "total_sell_commission", + "total_commission", + "initial_account_value", + "final_account_value", + "account_value_chg", + "market_pct_chg", + ] + original_df_list = [] + for sub_main_path in sub_main_paths: + logger.info(f"开始读取{sub_main_path}数据") + full_sub_main_path = os.path.join(main_path, sub_main_path, fix_sub_path) + # 读取sub_main_path下的所有文件夹 + folder_list = os.listdir(full_sub_main_path) + for folder in folder_list: + logger.info(f"开始读取{folder}数据") + folder_path = os.path.join(full_sub_main_path, folder) + properties = get_properties_by_folder_name(folder, sub_main_path) + logger.info(f"开始读取{folder}数据") + # 读取folder_path的sub_folder下的所有文件 + sub_folder_path = os.path.join(folder_path, sub_folder) + file_list = os.listdir(sub_folder_path) + for file in file_list: + logger.info(f"开始读取{file}数据") + if file_feature_name in file: + file_path = os.path.join(sub_folder_path, file) + df = pd.read_excel(file_path, sheet_name="资产价值变化") + logger.info(f"开始读取{file}数据") + # 向df添加properties + df = df.assign(**properties) + df = df[list(properties.keys()) + original_df_columns] + # 将df添加到original_df_list + original_df_list.append(df) + final_df = pd.concat(original_df_list) + excel_folder_path = os.path.join(main_path, "aindex_astock_均线macd结合策略2") + os.makedirs(excel_folder_path, exist_ok=True) + excel_file_path = os.path.join( + excel_folder_path, "all_strategy_with_commission.xlsx" + ) + with pd.ExcelWriter(excel_file_path) as writer: + final_df.to_excel( + writer, sheet_name="all_strategy_with_commission", index=False + ) + + +def get_properties_by_folder_name(folder_name: str, symbol_type: str): + properties = {} + sub_properties = folder_name.split("_") + properties["symbol_type"] = symbol_type + properties["buy_by_long_period"] = "no_long_period" + properties["long_period_ma5gtma10"] = False + properties["long_period_ma10gtma20"] = False + properties["long_period_macd_diffgt0"] = False + properties["long_period_macdgt0"] = False + properties["buy_by_long_period_10_percentile"] = False + if "1M" in sub_properties: + properties["buy_by_long_period"] = "1M" + if "1W" in sub_properties: + properties["buy_by_long_period"] = "1W" + if "1W1M" in sub_properties: + properties["buy_by_long_period"] = "1W1M" + if "ma5gtma10" in sub_properties: + properties["long_period_ma5gtma10"] = True + if "ma10gtma20" in sub_properties: + properties["long_period_ma10gtma20"] = True + if "macd_diffgt0" in sub_properties: + properties["long_period_macd_diffgt0"] = True + if "macdgt0" in sub_properties: + properties["long_period_macdgt0"] = True + if "10percentile" in sub_properties: + properties["buy_by_long_period_10_percentile"] = True + return properties + + +def profit_loss_ratio(): + """ + 计算利润损失比 + 公式:盈利/盈利交易次数 : 亏损/亏损交易次数 + """ + folder = r"./output/trade_sandbox/ma_strategy/binance/excel/均线macd结合策略2/" + prefix = ["无交易费用", "有交易费用"] + for prefix in prefix: + excel_file_path = os.path.join( + folder, f"{prefix}_趋势投资_from_201708181600_to_202509020600.xlsx" + ) + df = pd.read_excel(excel_file_path, sheet_name="买卖记录明细") + symbol_list = list(df["symbol"].unique()) + bar_list = list(df["bar"].unique()) + data_list = [] + for symbol in symbol_list: + for bar in bar_list: + df_symbol_bar = df[df["symbol"] == symbol][df["bar"] == bar] + start_date = df_symbol_bar["begin_date_time"].min() + end_date = df_symbol_bar["end_date_time"].max() + profit_df = df_symbol_bar[df_symbol_bar["profit_loss"] > 0] + loss_df = df_symbol_bar[df_symbol_bar["profit_loss"] < 0] + + profit_amount = sum(profit_df["profit_loss"]) + loss_amount = abs(sum(loss_df["profit_loss"])) + + profit_count = len(profit_df) + loss_count = len(loss_df) + + if profit_count == 0 or loss_count == 0: + continue + + profit_loss_ratio = round( + (profit_amount / profit_count) / (loss_amount / loss_count) * 100, 4 + ) + + data_list.append( + { + "币种": symbol, + "交易周期": bar, + "开始时间": start_date, + "结束时间": end_date, + "盈利金额": profit_amount, + "盈利次数": profit_count, + "亏损金额": loss_amount, + "亏损次数": loss_count, + "盈亏比": profit_loss_ratio, + "盈亏比公式": f"盈利金额/盈利次数 : 亏损金额/亏损次数", + } + ) + final_df = pd.DataFrame(data_list) + final_df.to_excel(os.path.join(folder, f"{prefix}时虚拟货币利润损失比.xlsx"), index=False) if __name__ == "__main__": - commission_per_share_list = [0, 0.0008] - buy_by_long_period_list = [{"by_week": True, "by_month": True}, - {"by_week": True, "by_month": False}, - {"by_week": False, "by_month": True}, - {"by_week": False, "by_month": False}] - long_period_condition_list = [{"ma5>ma10": True, "ma10>ma20": True, "macd_diff>0": True, "macd>0": True}, - {"ma5>ma10": True, "ma10>ma20": False, "macd_diff>0": True, "macd>0": True}, - {"ma5>ma10": False, "ma10>ma20": True, "macd_diff>0": True, "macd>0": True}] - - for commission_per_share in commission_per_share_list: - for buy_by_long_period in buy_by_long_period_list: - for long_period_condition in long_period_condition_list: - logger.info(f"开始计算, 主要参数:commission_per_share: {commission_per_share}, buy_by_long_period: {buy_by_long_period}, long_period_condition: {long_period_condition}") - trade_ma_strategy_main = TradeMaStrategyMain( - is_us_stock=False, - is_astock=False, - is_aindex=True, - is_binance=False, - commission_per_share=commission_per_share, - buy_by_long_period=buy_by_long_period, - long_period_condition=long_period_condition, - ) - trade_ma_strategy_main.batch_ma_break_statistics() - - trade_ma_strategy_main = TradeMaStrategyMain( - is_us_stock=False, - is_astock=True, - is_aindex=False, - is_binance=False, - commission_per_share=commission_per_share, - buy_by_long_period=buy_by_long_period, - long_period_condition=long_period_condition, - ) - trade_ma_strategy_main.batch_ma_break_statistics() + # batch_run_strategy() + profit_loss_ratio()