纳什博弈论与隐马尔可夫滤模型在交易中的应用·进阶篇
(2/3)·从纳什均衡公式到隐马尔可夫状态滤,少有人把这两套数学工具有机接进 MQL5 实战链路
◍ 用高斯隐马尔可夫集成平均状态出信号
这段 Python 训练逻辑把 10 状态高斯 HMM 跑了多轮交叉验证,每轮用 20% 样本做测试集(random_state=i 控制可复现),n_iter=10000 且 tol=1e-6 保证收敛精度,min_covar=1e-3 防止协方差矩阵退化。 每轮 fit 完把转移矩阵、均值、协方差存进三维数组,再用 model.predict 对全量特征打状态标签;状态序号为偶数给 +1 多单信号、奇数给 -1 空单信号,信号滞后一期乘收益得策略收益。 最后对所有轮次的矩阵按轴求平均,得到 average_transition_matrix 等稳定参数,并把平均状态四舍五入作为 df['average_state']。用 (1+returns).cumprod() 画出市场与策略净值曲线,存成 average_strategy_returns_XXX.png。 外汇与贵金属杠杆高,HMM 平均信号仅代表历史样本的统计倾向,实盘前务必在 MT5 用对应品种复算转移矩阵并做样本外验证。
model = hmm.GaussianHMM(n_components=class="num">10, covariance_type="full", n_iter=class="num">10000, tol=class="num">1e-6, min_covar=class="num">1e-3) X_train, X_test = train_test_split(scaled_features, test_size=class="num">0.2, random_state=i) model.fit(X_train) # Save the transition matrix, emission means, and covariances transition_matrices[:, :, i] = model.transmat_ means_matrices[i, :, :] = model.means_ covariance_matrices[i, :, :, :] = model.covars_ # State prediction states = model.predict(scaled_features) state_predictions[:, i] = states # Generate signals and calculate strategy returns for this model df[&class="macro">#x27;state&class="macro">#x27;] = states df[&class="macro">#x27;signal&class="macro">#x27;] = class="num">0 for j in range(class="num">10): df.loc[df[&class="macro">#x27;state&class="macro">#x27;] == j, &class="macro">#x27;signal&class="macro">#x27;] = class="num">1 if j % class="num">2 == class="num">0 else -class="num">1 df[&class="macro">#x27;strategy_returns&class="macro">#x27;] = df[&class="macro">#x27;returns&class="macro">#x27;] * df[&class="macro">#x27;signal&class="macro">#x27;].shift(class="num">1) strategy_returns[:, i] = df[&class="macro">#x27;strategy_returns&class="macro">#x27;].values # Average of matrices average_transition_matrix = transition_matrices.mean(axis=class="num">2) average_means_matrix = means_matrices.mean(axis=class="num">0) average_covariance_matrix = covariance_matrices.mean(axis=class="num">0) # Save the average matrices in the output file in appropriate format print("Average Transition Matrix:") for i, row in enumerate(average_transition_matrix): for j, val in enumerate(row): print(f"average_transition_matrix[{i}][{j}] = {val:.8f};") print("\nAverage Means Matrix:") for i, row in enumerate(average_means_matrix): for j, val in enumerate(row): print(f"average_means_matrix[{i}][{j}] = {val:.8f};") print("\nAverage Covariance Matrix:") for i in range(class="num">10): # For each state for j in range(class="num">3): # For each row of the covariance matrix for k in range(class="num">3): # For each column of the covariance matrix print(f"average_covariance_matrix[{i}][{j}][{k}] = {average_covariance_matrix[i, j, k]:.8e};") # Average of state predictions and strategy returns average_states = np.round(state_predictions.mean(axis=class="num">1)).astype(class="type">int) average_strategy_returns = strategy_returns.mean(axis=class="num">1) # Store the average results in the original dataframe df[&class="macro">#x27;average_state&class="macro">#x27;] = average_states df[&class="macro">#x27;average_strategy_returns&class="macro">#x27;] = average_strategy_returns # Calculate cumulative returns class="kw">using the average strategy df[&class="macro">#x27;cumulative_market_returns&class="macro">#x27;] = (class="num">1 + df[&class="macro">#x27;returns&class="macro">#x27;]).cumprod() df[&class="macro">#x27;cumulative_strategy_returns&class="macro">#x27;] = (class="num">1 + df[&class="macro">#x27;average_strategy_returns&class="macro">#x27;]).cumprod() # Plot cumulative returns(training) plt.figure(figsize=(class="num">7, class="num">6)) plt.plot(df.index, df[&class="macro">#x27;cumulative_market_returns&class="macro">#x27;], label=&class="macro">#x27;Market Returns&class="macro">#x27;) plt.plot(df.index, df[&class="macro">#x27;cumulative_strategy_returns&class="macro">#x27;], label=&class="macro">#x27;Strategy Returns(Average)&class="macro">#x27;) plt.title(&class="macro">#x27;Cumulative Returns with Average Strategy&class="macro">#x27;) plt.xlabel(&class="macro">#x27;Date&class="macro">#x27;) plt.ylabel(&class="macro">#x27;Cumulative Returns&class="macro">#x27;) plt.legend() plt.grid(True) plt.savefig(f&class="macro">#x27;average_strategy_returns_{symbol}.png&class="macro">#x27;) plt.close() # Additional plots for averages fig, (ax1, ax2, ax3) = plt.subplots(class="num">3, class="num">1, figsize=(class="num">12, class="num">15), sharex=True) # Plot closing price and average HMM states
「把隐马尔可夫状态映射成收益图」
这段脚本把训练好的 HMM 平均状态直接画到价格序列上:用 viridis 色阶给收盘价散点上色,颜色越亮代表模型判定的平均状态编号越高,左轴图同时叠了收盘线与状态散点,肉眼能扫出哪些价位区间对应哪种隐状态。 下方三幅子图分别给出每日收益(蓝为市场、红为策略平均,透明度 0.5)、累计收益曲线,以及各平均状态在历史样本里的收益求和——代码里写死 range(10),也就是默认拆 10 个隐状态,state_returns 把每个状态的 returns 加总,正负一眼可辨。 图存成 average_returns_{symbol}.png 与 average_bars_{symbol}.png,柱图还逐根标了 f'{height:.4f}' 的四位小数,并画了 y=0 红线做盈亏分界参考。 近期数据走同一套 scaler 再预测:df_recent 用训练集的 scaler.transform 而非 fit,避免未来函数;循环里每个 i 用不同 random_state 做 train_test_split 重训 GaussianHMM(n_components=10, tol=1e-4),把 recent_states 填回 df_recent['state'],后续按 10 个状态编号生成 signal。外汇与贵金属行情受杠杆与跳空影响,这类状态映射仅作概率参考,实盘前务必在 MT5 用历史品种复跑核对。
ax1.plot(df.index, df[&class="macro">#x27;close&class="macro">#x27;], label=&class="macro">#x27;Closing Price&class="macro">#x27;) scatter = ax1.scatter(df.index, df[&class="macro">#x27;close&class="macro">#x27;], c=df[&class="macro">#x27;average_state&class="macro">#x27;], cmap=&class="macro">#x27;viridis&class="macro">#x27;, s=class="num">30, label=&class="macro">#x27;Average HMM States&class="macro">#x27;) ax1.set_ylabel(&class="macro">#x27;Price&class="macro">#x27;) ax1.set_title(&class="macro">#x27;Closing Price and Average HMM States&class="macro">#x27;) ax1.legend(loc=&class="macro">#x27;upper left&class="macro">#x27;) # Add class="type">class="kw">color bar for states cbar = plt.colorbar(scatter, ax=ax1) cbar.set_label(&class="macro">#x27;Average HMM State&class="macro">#x27;) # Plot returns ax2.bar(df.index, df[&class="macro">#x27;returns&class="macro">#x27;], label=&class="macro">#x27;Market Returns&class="macro">#x27;, alpha=class="num">0.5, class="type">class="kw">color=&class="macro">#x27;blue&class="macro">#x27;) ax2.bar(df.index, df[&class="macro">#x27;average_strategy_returns&class="macro">#x27;], label=&class="macro">#x27;Average Strategy Returns&class="macro">#x27;, alpha=class="num">0.5, class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;) ax2.set_ylabel(&class="macro">#x27;Return&class="macro">#x27;) ax2.set_title(&class="macro">#x27;Daily Returns&class="macro">#x27;) ax2.legend(loc=&class="macro">#x27;upper left&class="macro">#x27;) # Plot cumulative returns ax3.plot(df.index, df[&class="macro">#x27;cumulative_market_returns&class="macro">#x27;], label=&class="macro">#x27;Cumulative Market Returns&class="macro">#x27;) ax3.plot(df.index, df[&class="macro">#x27;cumulative_strategy_returns&class="macro">#x27;], label=&class="macro">#x27;Cumulative Average Strategy Returns&class="macro">#x27;) ax3.set_ylabel(&class="macro">#x27;Cumulative Return&class="macro">#x27;) ax3.set_title(&class="macro">#x27;Cumulative Returns&class="macro">#x27;) ax3.legend(loc=&class="macro">#x27;upper left&class="macro">#x27;) # Adjust layout plt.tight_layout() plt.xlabel(&class="macro">#x27;Date&class="macro">#x27;) # Save figure plt.savefig(f&class="macro">#x27;average_returns_{symbol}.png&class="macro">#x27;) plt.close() # Calculate cumulative returns for each average state state_returns = {} for state in range(class="num">10): # Assuming class="num">10 states state_returns[state] = df[df[&class="macro">#x27;average_state&class="macro">#x27;] == state][&class="macro">#x27;returns&class="macro">#x27;].sum() # Create lists for states and their cumulative returns states = list(state_returns.keys()) returns = list(state_returns.values()) # Create bar chart plt.figure(figsize=(class="num">7, class="num">6)) bars = plt.bar(states, returns) # Customize chart plt.title(&class="macro">#x27;Cumulative Returns by Average HMM State&class="macro">#x27;, fontsize=class="num">7) plt.xlabel(&class="macro">#x27;State&class="macro">#x27;, fontsize=class="num">7) plt.ylabel(&class="macro">#x27;Cumulative Return&class="macro">#x27;, fontsize=class="num">7) plt.xticks(states) # Add value labels above each bar for bar in bars: height = bar.get_height() plt.text(bar.get_x() + bar.get_width()/class="num">2., height, f&class="macro">#x27;{height:.4f}&class="macro">#x27;, ha=&class="macro">#x27;center&class="macro">#x27;, va=&class="macro">#x27;bottom&class="macro">#x27;) # Add horizontal line at y=class="num">0 for reference plt.axhline(y=class="num">0, class="type">class="kw">color=&class="macro">#x27;r&class="macro">#x27;, linestyle=&class="macro">#x27;-&class="macro">#x27;, linewidth=class="num">0.5) # Adjust layout and save chart plt.tight_layout() plt.savefig(f&class="macro">#x27;average_bars_{symbol}.png&class="macro">#x27;) plt.close() # Get recent data to test the model df_recent = get_mt5_data(symbol, timeframe, end_date, current_date) df_recent = calculate_features(df_recent) # Apply the same scaler to recent data scaled_recent_features = scaler.transform(df_recent[[&class="macro">#x27;returns&class="macro">#x27;, &class="macro">#x27;volatility&class="macro">#x27;, &class="macro">#x27;trend&class="macro">#x27;]].values) # Lists to store the results of each model for recent data recent_state_predictions = np.zeros((scaled_recent_features.shape[class="num">0], n_models)) recent_strategy_returns = np.zeros((scaled_recent_features.shape[class="num">0], n_models)) # Apply the trained model to recent data for i in range(n_models): model = hmm.GaussianHMM(n_components=class="num">10, covariance_type="full", n_iter=class="num">10000, tol=class="num">1e-4, min_covar=class="num">1e-3) X_train, X_test = train_test_split(scaled_features, test_size=class="num">0.2, random_state=i) model.fit(X_train) recent_states = model.predict(scaled_recent_features) recent_state_predictions[:, i] = recent_states df_recent[&class="macro">#x27;state&class="macro">#x27;] = recent_states df_recent[&class="macro">#x27;signal&class="macro">#x27;] = class="num">0 for j in range(class="num">10):
隐状态标签与平均转移矩阵的实际落地
把隐藏马尔可夫跑出来的 10 个状态映射到可读标签,是回测之后最该补的一步。代码里用 state_returns[state] 是否大于 0 来判定该状态倾向「Uptrend」还是「Downtrend」,没出现的状态直接标成 Not present,这样你打开终端不会对着 State 7 发懵。 近期样本的平均策略收益是这样攒出来的:recent_strategy_returns 按列(不同模型或不同 seed)取 mean,得到 average_recent_strategy_returns,再用 (1 + average_strategy_returns).cumprod() 画累计曲线。图存成 average_recent_strategy_returns_{symbol}.png,市场收益和策略收益叠在同一张 7×6 的图上,肉眼能比出斜率差。 转移矩阵给的是 10×10 的平均结果,例如状态 0 留在自身的概率仅 0.1574,跳去状态 2 的概率 0.1679 反而最高;状态 9 跳去状态 3 为 0.1360、去状态 4 为 0.1295。外汇与贵金属属高风险品种,这些概率只描述历史样本内的状态游走倾向,换周期可能漂移。 取 H4 级别、2020-01-01 到 2023-12-31 的行情做上述统计,跑完记得 mt5.shutdown() 释放终端连接,否则下一个脚本容易卡在登录态。
df_recent.loc[df_recent[&class="macro">#x27;state&class="macro">#x27;] == j, &class="macro">#x27;signal&class="macro">#x27;] = class="num">1 if j % class="num">2 == class="num">0 else -class="num">1 df_recent[&class="macro">#x27;strategy_returns&class="macro">#x27;] = df_recent[&class="macro">#x27;returns&class="macro">#x27;] * df_recent[&class="macro">#x27;signal&class="macro">#x27;].shift(class="num">1) recent_strategy_returns[:, i] = df_recent[&class="macro">#x27;strategy_returns&class="macro">#x27;].values # Average of state predictions and strategy returns for recent data average_recent_states = np.round(recent_state_predictions.mean(axis=class="num">1)).astype(class="type">int) average_recent_strategy_returns = recent_strategy_returns.mean(axis=class="num">1) # Store the average results in the recent dataframe df_recent[&class="macro">#x27;average_state&class="macro">#x27;] = average_recent_states df_recent[&class="macro">#x27;average_strategy_returns&class="macro">#x27;] = average_recent_strategy_returns # Calculate cumulative returns class="kw">using the average strategy on recent data df_recent[&class="macro">#x27;cumulative_market_returns&class="macro">#x27;] = (class="num">1 + df_recent[&class="macro">#x27;returns&class="macro">#x27;]).cumprod() df_recent[&class="macro">#x27;cumulative_strategy_returns&class="macro">#x27;] = (class="num">1 + df_recent[&class="macro">#x27;average_strategy_returns&class="macro">#x27;]).cumprod() # Plot cumulative returns(recent test) plt.figure(figsize=(class="num">7, class="num">6)) plt.plot(df_recent.index, df_recent[&class="macro">#x27;cumulative_market_returns&class="macro">#x27;], label=&class="macro">#x27;Market Returns&class="macro">#x27;) plt.plot(df_recent.index, df_recent[&class="macro">#x27;cumulative_strategy_returns&class="macro">#x27;], label=&class="macro">#x27;Strategy Returns(Average)&class="macro">#x27;) plt.title(&class="macro">#x27;Cumulative Returns with Average Strategy(Recent Data)&class="macro">#x27;) plt.xlabel(&class="macro">#x27;Date&class="macro">#x27;) plt.ylabel(&class="macro">#x27;Cumulative Returns&class="macro">#x27;) plt.legend() plt.grid(True) plt.savefig(f&class="macro">#x27;average_recent_strategy_returns_{symbol}.png&class="macro">#x27;) plt.close() # Close MetaTrader class="num">5 mt5.shutdown() # Assign descriptive names to the hidden states state_labels = {} for state in range(class="num">10): # Assuming class="num">10 states if state in df[&class="macro">#x27;average_state&class="macro">#x27;].unique(): label = f"State {state}: " # You can customize this description based on your observations if state_returns[state] > class="num">0: label += "Uptrend" else: label += "Downtrend" state_labels[state] = label else: state_labels[state] = f"State {state}: Not present" # Print the states and their descriptive labels print("\nDescription of Hidden States:") for state, label in state_labels.items(): print(f"{label} (State ID: {state})") # Close MetaTrader class="num">5 connection mt5.shutdown() # Finally, close the output file sys.stdout.close() sys.stdout = sys.__stdout__ timeframe = mt5.TIMEFRAME_H4 start_date = "class="num">2020-class="num">01-class="num">01" end_date = "class="num">2023-class="num">12-class="num">31" average_transition_matrix[class="num">0][class="num">0] = class="num">0.15741321; average_transition_matrix[class="num">0][class="num">1] = class="num">0.07086962; average_transition_matrix[class="num">0][class="num">2] = class="num">0.16785905; average_transition_matrix[class="num">0][class="num">3] = class="num">0.08792403; average_transition_matrix[class="num">0][class="num">4] = class="num">0.11101073; average_transition_matrix[class="num">0][class="num">5] = class="num">0.05415263; average_transition_matrix[class="num">0][class="num">6] = class="num">0.08019415; average_transition_matrix[class="num">9][class="num">3] = class="num">0.13599698; average_transition_matrix[class="num">9][class="num">4] = class="num">0.12947508; average_transition_matrix[class="num">9][class="num">5] = class="num">0.06385211; average_transition_matrix[class="num">9][class="num">6] = class="num">0.09042617;
◍ 隐状态参数矩阵的实际读数
模型跑完之后落地的不是图表,而是三组冷冰冰的数组:转移概率、均值、协方差。以第 9 号隐状态为例,转移到自身的概率是 0.04559230,转到第 7 状态为 0.16088280,转到第 8 状态为 0.06588065——说明该状态具备一定持续性,但向外跳转的倾向更散。 均值矩阵里,第 9 状态对三个特征维度的中心读数分别是 -0.16057267、1.17106076、0.18531821;对比第 8 状态的 -0.38270747、0.86916742、-0.58792329,能看出两者虽同属极端波动区,但方向权重并不一致。外汇与贵金属杠杆高,这类读数只描述历史分布,不预示下一根 K 线。 协方差矩阵第 9 状态对角项给出 5.47457383、0.46522710、1.44717234,非对角项里 [0][2] 与 [2][0] 都是 0.25678465,[1][2] 为 -0.28825769。正负交叉说明特征间既有同向放大也有互斥,直接拿来当开仓信号会过拟合。 状态 0 被标为 Not present(不存在态),状态 1 是 Downtrend(下跌趋势态)。在 MT5 里把这两类 ID 打印出来,对照 EURUSD 的 H1 分段,能验证标签和实际波段是否对得上。
average_transition_matrix[class="num">9][class="num">7] = class="num">0.16088280; average_transition_matrix[class="num">9][class="num">8] = class="num">0.06588065; average_transition_matrix[class="num">9][class="num">9] = class="num">0.04559230; average_means_matrix[class="num">9][class="num">0] = -class="num">0.16057267; average_means_matrix[class="num">9][class="num">1] = class="num">1.17106076; average_means_matrix[class="num">9][class="num">2] = class="num">0.18531821; average_covariance_matrix[class="num">9][class="num">0][class="num">0] = class="num">5.47457383; average_covariance_matrix[class="num">9][class="num">1][class="num">2] = -class="num">0.28825769;
「状态机里的趋势片段与矩阵离线解析」
这套隐藏马尔可夫式的状态切分里,ID 2~8 构成了主行情脉络:2、4、5、6、8 被标为 Uptrend,3 与 7 是 Downtrend,9 则直接 Not present。也就是说在已观测样本里,上行状态占了 5/7,下行仅 2 段,状态 9 从未触发——拿去跑 EURUSD 这类高杠杆品种时,要清楚这种不对称本身就可能让反转概率被低估,外汇和贵金属杠杆风险极高。
上面的 Python 片段不是 MT5 终端里跑的,而是把专家顾问导出的矩阵文件离线扒出来重排格式用的。read_file 先判存在性再 try 读盘,避免路径写错直接崩;parse_matrix 用正则抓 名称[i][j]=数值 的二维赋值,parse_covariance_matrix 则多抓一层 k 变成三维协方差块。
format_matrix 的入口先判空返回 { };,否则按 key 排序拼换行。is_3d 为 True 时才走三层嵌套:先写 { ,再对 j 循环把每个 k 序列用 8 位科学计数法 :.8e 串成子数组。你可以在本地用这段把 EA 日志里的 average_covariance_matrix 重排,再贴回 MT5 做可视化验证。
class="kw">import re class="kw">import os def read_file(filename): if not os.path.exists(filename): print(f"Error: The file {filename} does not exist.") class="kw">return None try: with open(filename, "r") as file: class="kw">return file.read() except Exception as e: print(f"Error reading the file: {str(e)}") class="kw">return None def parse_matrix(file_content, matrix_name): pattern = rf"{matrix_name}\[(\d+)\]\[(\d+)\]\s*=\s*([-+]?(?:\d*\.\d+|\d+)(?:e[-+]?\d+)?)" matches = re.findall(pattern, file_content) matrix = {} for match in matches: i, j, value = class="type">int(match[class="num">0]), class="type">int(match[class="num">1]), class="type">float(match[class="num">2]) if i not in matrix: matrix[i] = {} matrix[i][j] = value class="kw">return matrix def parse_covariance_matrix(file_content): pattern = r"average_covariance_matrix\[(\d+)\]\[(\d+)\]\[(\d+)\]\s*=\s*([-+]?(?:\d*\.\d+|\d+)(?:e[-+]?\d+)?)" matches = re.findall(pattern, file_content) matrix = {} for match in matches: i, j, k, value = class="type">int(match[class="num">0]), class="type">int(match[class="num">1]), class="type">int(match[class="num">2]), class="type">float(match[class="num">3]) if i not in matrix: matrix[i] = {} if j not in matrix[i]: matrix[i][j] = {} matrix[i][j][k] = value class="kw">return matrix def format_matrix(matrix, is_3d=False): if not matrix: class="kw">return "{ };" formatted = "{\n" for i in sorted(matrix.keys()): if is_3d: formatted += " { " for j in sorted(matrix[i].keys()): formatted += "{" + ", ".join(f"{matrix[i][j][k]:.8e}" for k in sorted(matrix[i][j].keys())) + "}" if j < max(matrix[i].keys()): formatted += ",\n " formatted += "}" else:
把隐藏马尔可夫矩阵落盘成可读文本
上面这段 Python 脚本干的事很直接:读取 USDJPY_output.txt 里由 MT5 导出的平均转移矩阵、均值矩阵和协方差矩阵,重新排版后写进 formatted_matrices.txt。它先打印输入文件字节数和前 200 字符,方便你确认没读错文件。 parse_matrix 与 parse_covariance_matrix 分别抽取三类矩阵,随后 format_matrix 按 8 位小数格式化,协方差矩阵因是三维结构多传一个 is_3d=True 参数。脚本跑完会在控制台输出各矩阵元素数量,并把输出文件前 20 行回显出来供肉眼校验。 从实际落盘结果看,转移矩阵首行十个状态概率分别为 0.15741321、0.07086962、0.16785905、0.08792403、0.11101073、0.05415263、0.08019415、0.12333382、0.09794255、0.04930020,总和逼近 1。外汇与贵金属属高风险品种,这类概率分布只反映历史样本特征,后续状态切换倾向需结合实时行情验证。 在 MT5 里跑完 HMM 测算后,用这套脚本转格式比手动抄表稳得多;改 input_filename 就能批次处理其他货币对输出。
formatted += " {" + ", ".join(f"{matrix[i][j]:.8f}" for j in sorted(matrix[i].keys())) + "}"
if i < max(matrix.keys()):
formatted += ","
formatted += "\n"
formatted += " };"
class="kw">return formatted
def main():
input_filename = "USDJPY_output.txt"
output_filename = "formatted_matrices.txt"
content = read_file(input_filename)
if content is None:
class="kw">return
print(f"Input file size: {len(content)} bytes")
print("First class="num">200 characters of the file:")
print(content[:class="num">200])
transition_matrix = parse_matrix(content, "average_transition_matrix")
means_matrix = parse_matrix(content, "average_means_matrix")
covariance_matrix = parse_covariance_matrix(content)
print(f"\nElements found in the transition matrix: {len(transition_matrix)}")
print(f"Elements found in the means matrix: {len(means_matrix)}")
print(f"Elements found in the covariance matrix: {len(covariance_matrix)}")
output = "Transition Matrix:\n"
output += format_matrix(transition_matrix)
output += "\n\nMeans Matrix:\n"
output += format_matrix(means_matrix)
output += "\n\nCovariance Matrix:\n"
output += format_matrix(covariance_matrix, is_3d=True)
try:
with open(output_filename, "w") as outfile:
outfile.write(output)
print(f"\nFormatted matrices saved in &class="macro">#x27;{output_filename}&class="macro">#x27;")
except Exception as e:
print(f"Error writing the output file: {str(e)}")
print(f"\nFirst lines of the output file &class="macro">#x27;{output_filename}&class="macro">#x27;:")
output_content = read_file(output_filename)
if output_content:
print("\n".join(output_content.split("\n")[:class="num">20])) # Display the first class="num">20 lines
if __name__ == "__main__":
main()