From 69aff37c2475c45541c8ee280f79d634530f9c17 Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Mon, 25 Aug 2025 16:58:38 +0800 Subject: [PATCH] optimize chart output --- .../ma_break_statistics.cpython-312.pyc | Bin 24055 -> 32078 bytes core/trade/ma_break_statistics.py | 227 +++++++++++++++--- trade_ma_strategy_main.py | 1 + 3 files changed, 201 insertions(+), 27 deletions(-) diff --git a/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc b/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc index e04149372c9c4ae873a625a83ef1afae7b1c54cd..10d11b4e25b2f9655dbfc25afca7b8ebef12343c 100644 GIT binary patch delta 10748 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 ZHj&p;UiuL-n~K;PO?2Hg3Q=40e*gyRYrOyf diff --git a/core/trade/ma_break_statistics.py b/core/trade/ma_break_statistics.py index 319541d..1a4a155 100644 --- a/core/trade/ma_break_statistics.py +++ b/core/trade/ma_break_statistics.py @@ -93,10 +93,13 @@ class MaBreakStatistics: f"{symbol} {bar} 的市场价格变化, {market_data_pct_chg.get('pct_chg', 0)}%" ) market_data_pct_chg_list.append(market_data_pct_chg) + if len(ma_break_market_data_list) > 0: ma_break_market_data = pd.concat(ma_break_market_data_list) market_data_pct_chg_df = pd.DataFrame(market_data_pct_chg_list) # 依据symbol和bar分组,统计每个symbol和bar的pct_chg的max, min, mean, std, median, count + ma_break_market_data.sort_values(by="begin_timestamp", ascending=True, inplace=True) + ma_break_market_data.reset_index(drop=True, inplace=True) pct_chg_df = ( ma_break_market_data.groupby(["symbol", "bar"])["pct_chg"] .agg( @@ -127,7 +130,7 @@ class MaBreakStatistics: symbol_bar_data = ma_break_market_data[ (ma_break_market_data["symbol"] == symbol) & (ma_break_market_data["bar"] == bar) - ] + ].copy() # 创建副本避免SettingWithCopyWarning if len(symbol_bar_data) > 0: symbol_bar_data.sort_values( by="end_timestamp", ascending=True, inplace=True @@ -136,6 +139,14 @@ class MaBreakStatistics: symbol_bar_data["pct_chg_total"] = symbol_bar_data[ "pct_chg_total" ].cumprod() + + # 将更新后的pct_chg_total数据同步更新到ma_break_market_data的对应数据行中 + for idx, row in symbol_bar_data.iterrows(): + mask = (ma_break_market_data["symbol"] == symbol) & \ + (ma_break_market_data["bar"] == bar) & \ + (ma_break_market_data["end_timestamp"] == row["end_timestamp"]) + ma_break_market_data.loc[mask, "pct_chg_total"] = row["pct_chg_total"] + last_pct_chg_total = symbol_bar_data["pct_chg_total"].iloc[-1] last_pct_chg_total = (last_pct_chg_total - 1) * 100 pct_chg_df.loc[ @@ -208,7 +219,9 @@ class MaBreakStatistics: writer, sheet_name="买卖时间间隔统计", index=False ) - chart_dict = self.draw_pct_chg_mean_chart(pct_chg_df, strategy_name) + chart_dict = self.draw_quant_pct_chg_bar_chart(pct_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, strategy_name) self.output_chart_to_excel(output_file_path, chart_dict) return pct_chg_df else: @@ -370,13 +383,17 @@ class MaBreakStatistics: if len(ma_break_market_data_pair_list) > 0: ma_break_market_data = pd.DataFrame(ma_break_market_data_pair_list) + # sort by end_timestamp + ma_break_market_data.sort_values(by="begin_timestamp", ascending=True, inplace=True) + ma_break_market_data.reset_index(drop=True, inplace=True) logger.info( f"获取{symbol} {bar} 的买卖记录明细成功, 买卖次数: {len(ma_break_market_data)}" ) - # 将market_data(最后一条数据的close - 第一条数据的open) / 第一条数据的open * 100 + # 量化期间,市场的波动率: + # ma_break_market_data(最后一条数据的end_close - 第一条数据的begin_close) / 第一条数据的begin_close * 100 pct_chg = ( - (market_data["close"].iloc[-1] - market_data["open"].iloc[0]) - / market_data["open"].iloc[0] + (ma_break_market_data["end_close"].iloc[-1] - ma_break_market_data["begin_close"].iloc[0]) + / ma_break_market_data["begin_close"].iloc[0] * 100 ) pct_chg = round(pct_chg, 4) @@ -511,7 +528,7 @@ class MaBreakStatistics: pass return condition - def draw_pct_chg_mean_chart( + def draw_quant_pct_chg_bar_chart( self, data: pd.DataFrame, strategy_name: str = "全均线策略" ): """ @@ -527,48 +544,204 @@ class MaBreakStatistics: plt.rcParams["font.size"] = 11 # 设置字体大小 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 chart_dict = {} - column_name_dict = {"pct_chg_total": "涨跌总和", "pct_chg_mean": "涨跌均值"} + column_name_dict = {"pct_chg_total": "量化策略涨跌", "pct_chg_mean": "量化策略涨跌均值"} for column_name, column_name_text in column_name_dict.items(): for bar in data["bar"].unique(): bar_data = data[data["bar"] == bar].copy() # 一次筛选即可 if bar_data.empty: continue bar_data.rename(columns={column_name: column_name_text}, inplace=True) + if column_name == "pct_chg_total": + bar_data.rename(columns={"market_pct_chg": "市场自然涨跌"}, inplace=True) # 可选:按均值排序 bar_data.sort_values(by=column_name_text, ascending=False, inplace=True) bar_data.reset_index(drop=True, inplace=True) - plt.figure(figsize=(10, 6)) - ax = sns.barplot( - x="symbol", y=column_name_text, data=bar_data, palette="Blues_d" - ) - plt.title(f"{bar}趋势{column_name_text}(%)") - plt.xlabel("symbol") - plt.ylabel(column_name_text) - plt.xticks(rotation=45, ha="right") + # 如果column_name_text是"量化策略涨跌",则柱状图,同时绘制量化策略涨跌与市场自然涨跌的柱状图,并绘制在同一个图表中 + if column_name == "pct_chg_total": + plt.figure(figsize=(12, 7)) + + # 设置x轴位置,为并列柱状图做准备 + x = np.arange(len(bar_data)) + width = 0.35 # 柱状图宽度 + + # 确保symbol列是字符串类型,避免matplotlib警告 + bar_data["symbol"] = bar_data["symbol"].astype(str) + + # 绘制量化策略涨跌柱状图(蓝色渐变色) + bars1 = plt.bar(x - width/2, bar_data[column_name_text], width, + label=column_name_text, + color=plt.cm.Blues(np.linspace(0.6, 0.9, len(bar_data)))) + + # 绘制市场自然涨跌柱状图(绿色渐变色) + bars2 = plt.bar(x + width/2, bar_data["市场自然涨跌"], width, + label="市场自然涨跌", + color=plt.cm.Greens(np.linspace(0.6, 0.9, len(bar_data)))) + + # 设置图表标题和标签 + plt.title(f"{bar}趋势{column_name_text}与市场自然涨跌对比(%)", fontsize=14, fontweight='bold') + plt.xlabel("Symbol", fontsize=12) + plt.ylabel("涨跌幅(%)", fontsize=12) + plt.xticks(x, bar_data['symbol'], rotation=45, ha='right') + plt.legend() + plt.grid(True, alpha=0.3) + + # 在量化策略涨跌柱状图上方添加数值标签 + for i, (bar1, value1) in enumerate(zip(bars1, bar_data[column_name_text])): + plt.text(bar1.get_x() + bar1.get_width()/2, value1 + (0.01 if value1 >= 0 else -0.01), + f'{value1:.3f}%', ha='center', va='bottom' if value1 >= 0 else 'top', + fontsize=9, fontweight='bold', color='darkblue') + + # 在市场自然涨跌柱状图上方添加数值标签 + for i, (bar2, value2) in enumerate(zip(bars2, bar_data["市场自然涨跌"])): + plt.text(bar2.get_x() + bar2.get_width()/2, value2 + (0.01 if value2 >= 0 else -0.01), + f'{value2:.3f}%', ha='center', va='bottom' if value2 >= 0 else 'top', + fontsize=9, fontweight='bold', color='darkgreen') - # 在柱状图上添加数值标签 - for i, v in enumerate(bar_data[column_name_text]): - ax.text( - i, - v, - f"{v:.3f}", - ha="center", - va="bottom", - fontsize=10, - fontweight="bold", + else: + plt.figure(figsize=(10, 6)) + + # 确保symbol列是字符串类型,避免matplotlib警告 + bar_data["symbol"] = bar_data["symbol"].astype(str) + + ax = sns.barplot( + x="symbol", y=column_name_text, data=bar_data, palette="Blues_d" ) + plt.title(f"{bar}趋势{column_name_text}(%)") + plt.xlabel("symbol") + plt.ylabel(column_name_text) + plt.xticks(rotation=45, ha="right") + + # 在柱状图上添加数值标签 + for i, v in enumerate(bar_data[column_name_text]): + ax.text( + i, + v, + f"{v:.3f}", + ha="center", + va="bottom", + fontsize=10, + fontweight="bold", + ) plt.tight_layout() save_path = os.path.join( self.stats_chart_dir, - f"{bar}_ma_break_{column_name}_{strategy_name}.png", + f"{bar}_bar_chart_{column_name}_{strategy_name}.png", ) plt.savefig(save_path, dpi=150) plt.close() - sheet_name = f"{bar}_趋势{column_name_text}分布图表_{strategy_name}" + sheet_name = f"{bar}_趋势{column_name_text}柱状图_{strategy_name}" + chart_dict[sheet_name] = save_path + return chart_dict + + def draw_quant_line_chart(self, data: pd.DataFrame, strategy_name: str = "全均线策略"): + """ + 根据量化策略买卖明细记录,绘制量化策略涨跌与市场自然涨跌的折线图 + :param data: 量化策略买卖明细记录 + :param strategy_name: 策略名称 + :return: None + """ + symbols = data["symbol"].unique() + bars = data["bar"].unique() + chart_dict = {} + for symbol in symbols: + for bar in bars: + symbol_bar_data = data[(data["symbol"] == symbol) & (data["bar"] == bar)] + if symbol_bar_data.empty: + continue + + # 获取第一行数据作为基准 + first_row = symbol_bar_data.iloc[0].copy() + + # 创建初始化行,设置基准值 + init_row = first_row.copy() + init_row.loc["pct_chg_total"] = 1.0 # 量化策略初始值为1 + init_row.loc["end_timestamp"] = first_row["begin_timestamp"] + init_row.loc["end_date_time"] = first_row["begin_date_time"] + init_row.loc["end_close"] = first_row["begin_close"] + init_row.loc["end_ma5"] = first_row["begin_ma5"] + init_row.loc["end_ma10"] = first_row["begin_ma10"] + init_row.loc["end_ma20"] = first_row["begin_ma20"] + init_row.loc["end_ma30"] = first_row["begin_ma30"] + init_row.loc["end_macd_diff"] = first_row["begin_macd_diff"] + init_row.loc["end_macd_dea"] = first_row["begin_macd_dea"] + init_row.loc["end_macd"] = first_row["begin_macd"] + init_row.loc["pct_chg"] = 0 + init_row.loc["interval_seconds"] = 0 + init_row.loc["interval_minutes"] = 0 + init_row.loc["interval_hours"] = 0 + init_row.loc["interval_days"] = 0 + + # 将初始化行添加到数据开头 + symbol_bar_data = pd.concat([pd.DataFrame([init_row]), symbol_bar_data]) + symbol_bar_data.sort_values(by="end_timestamp", ascending=True, inplace=True) + symbol_bar_data.reset_index(drop=True, inplace=True) + + # 确保时间列是datetime类型,避免matplotlib警告 + symbol_bar_data["end_date_time"] = pd.to_datetime(symbol_bar_data["end_date_time"]) + + # 计算市场价位归一化数据(相对于初始价格) + symbol_bar_data["end_close_to_1"] = symbol_bar_data["end_close"] / init_row["end_close"] + symbol_bar_data["end_close_to_1"] = symbol_bar_data["end_close_to_1"].round(4) + + # 绘制折线图 + plt.figure(figsize=(12, 7)) + + # 绘制量化策略涨跌线(蓝色) + plt.plot(symbol_bar_data["end_date_time"], symbol_bar_data["pct_chg_total"], + label="量化策略涨跌", color='blue', linewidth=2, marker='o', markersize=4) + + # 绘制市场自然涨跌线(绿色) + plt.plot(symbol_bar_data["end_date_time"], symbol_bar_data["end_close_to_1"], + label="市场自然涨跌", color='green', linewidth=2, marker='s', markersize=4) + + plt.title(f"{symbol} {bar} 量化与市场折线图_{strategy_name}", + fontsize=14, fontweight='bold') + plt.xlabel("时间", fontsize=12) + plt.ylabel("涨跌变化", fontsize=12) + plt.legend(fontsize=11) + plt.grid(True, alpha=0.3) + + # 设置x轴标签,避免matplotlib警告 + # 选择合适的时间间隔显示标签,避免过于密集 + if len(symbol_bar_data) > 30: + # 如果数据点较多,选择间隔显示,但确保第一条和最后一条始终显示 + step = max(1, len(symbol_bar_data) // 30) + + # 创建标签索引列表,确保包含首尾数据 + label_indices = [0] # 第一条 + + # 添加中间间隔的标签 + for i in range(step, len(symbol_bar_data) - 1, step): + label_indices.append(i) + + # 添加最后一条(如果还没有包含的话) + if len(symbol_bar_data) - 1 not in label_indices: + label_indices.append(len(symbol_bar_data) - 1) + + # 设置x轴标签 + plt.xticks(symbol_bar_data["end_date_time"].iloc[label_indices], + symbol_bar_data["end_date_time"].iloc[label_indices].dt.strftime('%m-%d %H:%M'), + rotation=45, ha='right') + else: + # 如果数据点较少,全部显示 + plt.xticks(symbol_bar_data["end_date_time"], + symbol_bar_data["end_date_time"].dt.strftime('%m-%d %H:%M'), + rotation=45, ha='right') + + plt.tight_layout() + + save_path = os.path.join( + self.stats_chart_dir, + f"{symbol}_{bar}_line_chart_{strategy_name}.png", + ) + plt.savefig(save_path, dpi=150, bbox_inches='tight') + plt.close() + + sheet_name = f"{symbol}_{bar}_折线图_{strategy_name}" chart_dict[sheet_name] = save_path return chart_dict diff --git a/trade_ma_strategy_main.py b/trade_ma_strategy_main.py index 01174a6..7b815b8 100644 --- a/trade_ma_strategy_main.py +++ b/trade_ma_strategy_main.py @@ -38,6 +38,7 @@ class TradeMaStrategyMain: for strategy_name, strategy_info in strategy_dict.items(): pct_chg_df = self.ma_break_statistics.batch_statistics(strategy_name=strategy_name) pct_chg_df_list.append(pct_chg_df) + pct_chg_df = pd.concat(pct_chg_df_list) def statistics_pct_chg(self, pct_chg_df: pd.DataFrame):