refactor: 清理测试文件和2019相关代码
- 删除测试脚本: test_2026_classification.py, test_lagllama.py, test_timesfm.py, test_ttm.py - 删除2019相关文件: analyze_2019_*, apply_2019_*, dlinear_2019_*, three_models_2019_* - 添加集成模型相关代码和分析脚本 - 添加N-BEATS模型和CatBoost分类模型
This commit is contained in:
1 parent
c74f4f6949
commit
1bd9d25f60
33 files changed
+6557
-233
No files matched your search
@@ -0,0 +1,458 @@
|
||||
"""保存所有测试结果到项目 - 生成完整报告"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import torch
|
||||
import pickle
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pandas_ta as ta
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.metrics import confusion_matrix
|
||||
from data.fetcher import BinanceDataFetcher
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# ==================== 预处理器定义 ====================
|
||||
|
||||
class ClassificationPreprocessor:
|
||||
"""DLinear分类预处理器"""
|
||||
def __init__(self, lookback=168, forecast_horizon=1):
|
||||
self.lookback = lookback
|
||||
self.forecast_horizon = forecast_horizon
|
||||
self.scaler = StandardScaler()
|
||||
self.is_fitted = False
|
||||
|
||||
def add_features(self, df):
|
||||
df = df.copy()
|
||||
df['rsi'] = ta.rsi(df['close'], length=14)
|
||||
macd = ta.macd(df['close'])
|
||||
if macd is not None and not macd.empty:
|
||||
df['macd'] = macd.iloc[:, 0]
|
||||
df['macd_hist'] = macd.iloc[:, 1]
|
||||
df['macd_signal'] = macd.iloc[:, 2]
|
||||
return df
|
||||
|
||||
def create_labels(self, df):
|
||||
df = df.copy()
|
||||
df['future_price'] = df['close'].shift(-self.forecast_horizon)
|
||||
df['future_return'] = (df['future_price'] - df['close']) / df['close']
|
||||
df['label'] = (df['future_return'] > 0).astype(int)
|
||||
return df[:-self.forecast_horizon]
|
||||
|
||||
def preprocess(self, df, fit=True):
|
||||
df = self.add_features(df)
|
||||
df = self.create_labels(df)
|
||||
df = df.dropna().reset_index(drop=True)
|
||||
|
||||
feature_cols = ['open', 'high', 'low', 'close', 'volume',
|
||||
'rsi', 'macd', 'macd_hist', 'macd_signal']
|
||||
|
||||
if fit:
|
||||
scaled_features = self.scaler.fit_transform(df[feature_cols])
|
||||
self.is_fitted = True
|
||||
self.feature_columns = feature_cols
|
||||
else:
|
||||
scaled_features = self.scaler.transform(df[feature_cols])
|
||||
|
||||
X, y = [], []
|
||||
for i in range(len(scaled_features) - self.lookback):
|
||||
X.append(scaled_features[i:i + self.lookback])
|
||||
y.append(df['label'].iloc[i + self.lookback])
|
||||
|
||||
return np.array(X), np.array(y), df
|
||||
|
||||
|
||||
class NBeatsPreprocessor:
|
||||
"""N-BEATS预处理器"""
|
||||
def __init__(self, lookback=168, forecast_horizon=1):
|
||||
self.lookback = lookback
|
||||
self.forecast_horizon = forecast_horizon
|
||||
self.price_scaler = StandardScaler()
|
||||
self.is_fitted = False
|
||||
|
||||
def preprocess(self, df, fit=True):
|
||||
df = df.copy()
|
||||
df['future_price'] = df['close'].shift(-self.forecast_horizon)
|
||||
df = df[:-self.forecast_horizon].reset_index(drop=True)
|
||||
|
||||
prices = df['close'].values.reshape(-1, 1)
|
||||
future_prices = df['future_price'].values.reshape(-1, 1)
|
||||
|
||||
if fit:
|
||||
scaled_prices = self.price_scaler.fit_transform(prices)
|
||||
scaled_future = self.price_scaler.transform(future_prices)
|
||||
self.is_fitted = True
|
||||
else:
|
||||
scaled_prices = self.price_scaler.transform(prices)
|
||||
scaled_future = self.price_scaler.transform(future_prices)
|
||||
|
||||
X, y = [], []
|
||||
for i in range(len(scaled_prices) - self.lookback):
|
||||
X.append(scaled_prices[i:i + self.lookback, 0])
|
||||
y.append(scaled_future[i + self.lookback, 0])
|
||||
|
||||
return np.array(X), np.array(y), df
|
||||
|
||||
def inverse_transform_price(self, scaled_price):
|
||||
return self.price_scaler.inverse_transform(scaled_price.reshape(-1, 1)).flatten()
|
||||
|
||||
|
||||
def main():
|
||||
print("="*80)
|
||||
print("保存完整测试结果到项目")
|
||||
print("="*80)
|
||||
|
||||
# 创建结果目录
|
||||
results_dir = "./results"
|
||||
os.makedirs(results_dir, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# ========== 加载模型 ==========
|
||||
print("\n[1/5] 加载模型...")
|
||||
|
||||
device = torch.device('mps' if torch.backends.mps.is_available() else 'cpu')
|
||||
|
||||
from models.dlinear.model import DLinear
|
||||
dlinear_checkpoint = torch.load(
|
||||
'./models/checkpoints/dlinear_classification_2026/best_model.pt',
|
||||
map_location=device
|
||||
)
|
||||
dlinear_model = DLinear(dlinear_checkpoint['config'])
|
||||
dlinear_model.load_state_dict(dlinear_checkpoint['model_state_dict'])
|
||||
dlinear_model = dlinear_model.to(device)
|
||||
dlinear_model.eval()
|
||||
|
||||
with open('./models/checkpoints/dlinear_classification_2026/preprocessor.pkl', 'rb') as f:
|
||||
dlinear_preprocessor = pickle.load(f)
|
||||
|
||||
from models.nbeats.model import NBeats
|
||||
nbeats_checkpoint = torch.load(
|
||||
'./models/checkpoints/nbeats_btc_1h/best_model.pt',
|
||||
map_location=device
|
||||
)
|
||||
nbeats_model = NBeats(nbeats_checkpoint['config'])
|
||||
nbeats_model.load_state_dict(nbeats_checkpoint['model_state_dict'])
|
||||
nbeats_model = nbeats_model.to(device)
|
||||
nbeats_model.eval()
|
||||
|
||||
with open('./models/checkpoints/nbeats_btc_1h/preprocessor.pkl', 'rb') as f:
|
||||
nbeats_preprocessor = pickle.load(f)
|
||||
|
||||
from tsfm_public.models.tinytimemixer import TinyTimeMixerForPrediction
|
||||
ttm_model = TinyTimeMixerForPrediction.from_pretrained(
|
||||
"ibm-granite/granite-timeseries-ttm-r2",
|
||||
trust_remote_code=True
|
||||
)
|
||||
ttm_model = ttm_model.to(device)
|
||||
ttm_model.eval()
|
||||
|
||||
print("✓ 三个模型已加载")
|
||||
|
||||
# ========== 获取数据 ==========
|
||||
print("\n[2/5] 获取2026年1月测试数据...")
|
||||
fetcher = BinanceDataFetcher()
|
||||
|
||||
df_2026 = fetcher.fetch_ohlcv('BTC', '1h', start_date='2026-01-01',
|
||||
end_date='2026-01-30', use_cache=False)
|
||||
|
||||
print(f"✓ 数据: {len(df_2026)} 行, {df_2026['timestamp'].min()} 至 {df_2026['timestamp'].max()}")
|
||||
|
||||
# ========== 预处理和预测 ==========
|
||||
print("\n[3/5] 预处理和预测...")
|
||||
|
||||
X_dlinear, y_dlinear, df_dlinear = dlinear_preprocessor.preprocess(df_2026, fit=False)
|
||||
X_nbeats, y_nbeats, df_nbeats = nbeats_preprocessor.preprocess(df_2026, fit=False)
|
||||
|
||||
min_samples = min(len(X_dlinear), len(X_nbeats))
|
||||
X_dlinear = X_dlinear[:min_samples]
|
||||
y_dlinear = y_dlinear[:min_samples]
|
||||
X_nbeats = X_nbeats[:min_samples]
|
||||
|
||||
# DLinear预测
|
||||
X_dlinear_tensor = torch.FloatTensor(X_dlinear).to(device)
|
||||
with torch.no_grad():
|
||||
dlinear_logits = dlinear_model(X_dlinear_tensor).cpu().numpy()
|
||||
dlinear_probs = 1 / (1 + np.exp(-dlinear_logits.flatten()))
|
||||
dlinear_preds = (dlinear_probs > 0.5).astype(int)
|
||||
|
||||
# N-BEATS预测
|
||||
X_nbeats_tensor = torch.FloatTensor(X_nbeats).to(device)
|
||||
with torch.no_grad():
|
||||
nbeats_pred_scaled = nbeats_model(X_nbeats_tensor).cpu().numpy()
|
||||
|
||||
nbeats_pred_prices = nbeats_preprocessor.inverse_transform_price(nbeats_pred_scaled)
|
||||
current_open_prices = df_nbeats['open'].iloc[168:168+len(nbeats_pred_prices)].values
|
||||
nbeats_preds = (nbeats_pred_prices > current_open_prices).astype(int)
|
||||
|
||||
# TTM-R2预测
|
||||
context_length = 512
|
||||
ttm_preds_list = []
|
||||
all_prices = df_2026['close'].values
|
||||
all_open_prices = df_2026['open'].values
|
||||
|
||||
for i in range(min_samples):
|
||||
start_idx = i
|
||||
end_idx = start_idx + context_length
|
||||
|
||||
if end_idx > len(all_prices):
|
||||
context_data = all_prices[-context_length:].reshape(1, -1, 1)
|
||||
current_open_price = all_open_prices[min(168 + i, len(all_open_prices) - 1)]
|
||||
else:
|
||||
context_data = all_prices[start_idx:end_idx].reshape(1, -1, 1)
|
||||
current_open_price = all_open_prices[end_idx] if end_idx < len(all_open_prices) else all_open_prices[-1]
|
||||
|
||||
context_tensor = torch.FloatTensor(context_data).to(device)
|
||||
with torch.no_grad():
|
||||
output = ttm_model(context_tensor)
|
||||
pred_price = output.prediction_outputs.cpu().numpy()[0, 0, 0]
|
||||
ttm_preds_list.append(1 if pred_price > current_open_price else 0)
|
||||
|
||||
ttm_preds = np.array(ttm_preds_list)
|
||||
|
||||
# 双模型交集
|
||||
two_model_agree_up = (dlinear_preds == 1) & (nbeats_preds == 1)
|
||||
two_model_agree_down = (dlinear_preds == 0) & (nbeats_preds == 0)
|
||||
two_model_preds = np.full(len(dlinear_preds), -1)
|
||||
two_model_preds[two_model_agree_up] = 1
|
||||
two_model_preds[two_model_agree_down] = 0
|
||||
|
||||
# 三模型投票
|
||||
vote_sum = dlinear_preds + nbeats_preds + ttm_preds
|
||||
three_model_preds = np.where(vote_sum >= 2, 1, 0)
|
||||
|
||||
true_labels = y_dlinear
|
||||
|
||||
print(f"✓ 预测完成,样本数: {min_samples}")
|
||||
|
||||
# ========== 生成详细结果 ==========
|
||||
print("\n[4/5] 生成详细结果...")
|
||||
|
||||
# 时间戳和日期
|
||||
timestamps = df_2026['timestamp'].iloc[168:168+min_samples].values
|
||||
timestamps_utc = pd.to_datetime(timestamps)
|
||||
timestamps_et = timestamps_utc.tz_localize('UTC').tz_convert('America/New_York')
|
||||
dates = timestamps_et.date
|
||||
|
||||
# 创建详细结果DataFrame
|
||||
results_df = pd.DataFrame({
|
||||
'timestamp_utc': timestamps_utc,
|
||||
'timestamp_et': timestamps_et,
|
||||
'date': dates,
|
||||
'true_label': true_labels,
|
||||
'dlinear_prob': dlinear_probs,
|
||||
'dlinear_pred': dlinear_preds,
|
||||
'nbeats_pred': nbeats_preds,
|
||||
'ttm_pred': ttm_preds,
|
||||
'two_model_pred': two_model_preds,
|
||||
'three_model_pred': three_model_preds,
|
||||
'dlinear_correct': dlinear_preds == true_labels,
|
||||
'two_model_correct': (two_model_preds == true_labels) & (two_model_preds != -1),
|
||||
'three_model_correct': three_model_preds == true_labels
|
||||
})
|
||||
|
||||
# 按天统计
|
||||
daily_stats = results_df.groupby('date').agg({
|
||||
'true_label': 'count',
|
||||
'two_model_pred': lambda x: (x != -1).sum(),
|
||||
'two_model_correct': 'sum',
|
||||
'three_model_correct': 'sum',
|
||||
'dlinear_correct': 'sum'
|
||||
})
|
||||
|
||||
daily_stats.columns = ['total_samples', 'two_model_valid', 'two_model_correct',
|
||||
'three_model_correct', 'dlinear_correct']
|
||||
|
||||
daily_stats['dlinear_accuracy'] = daily_stats['dlinear_correct'] / daily_stats['total_samples'] * 100
|
||||
daily_stats['two_model_accuracy'] = daily_stats['two_model_correct'] / daily_stats['two_model_valid'] * 100
|
||||
daily_stats['three_model_accuracy'] = daily_stats['three_model_correct'] / daily_stats['total_samples'] * 100
|
||||
daily_stats['two_model_coverage'] = daily_stats['two_model_valid'] / daily_stats['total_samples'] * 100
|
||||
|
||||
daily_stats['two_model_accuracy'] = daily_stats['two_model_accuracy'].fillna(0)
|
||||
|
||||
# ========== 保存结果 ==========
|
||||
print("\n[5/5] 保存结果文件...")
|
||||
|
||||
# 1. 保存逐小时详细结果
|
||||
hourly_file = f"{results_dir}/hourly_predictions_2026_01.csv"
|
||||
results_df.to_csv(hourly_file, index=False)
|
||||
print(f"✓ 保存逐小时预测: {hourly_file}")
|
||||
|
||||
# 2. 保存每日统计
|
||||
daily_file = f"{results_dir}/daily_statistics_2026_01.csv"
|
||||
daily_stats.to_csv(daily_file)
|
||||
print(f"✓ 保存每日统计: {daily_file}")
|
||||
|
||||
# 3. 生成总结报告
|
||||
summary = {
|
||||
"测试期间": {
|
||||
"开始日期": str(df_2026['timestamp'].min()),
|
||||
"结束日期": str(df_2026['timestamp'].max()),
|
||||
"总天数": len(daily_stats),
|
||||
"总样本数": int(min_samples)
|
||||
},
|
||||
"单模型性能": {
|
||||
"DLinear": {
|
||||
"准确率": f"{(dlinear_preds == true_labels).mean() * 100:.2f}%",
|
||||
"预测上涨比例": f"{(dlinear_preds == 1).mean() * 100:.1f}%"
|
||||
},
|
||||
"N-BEATS": {
|
||||
"准确率": f"{(nbeats_preds == true_labels).mean() * 100:.2f}%",
|
||||
"预测上涨比例": f"{(nbeats_preds == 1).mean() * 100:.1f}%"
|
||||
},
|
||||
"TTM-R2": {
|
||||
"准确率": f"{(ttm_preds == true_labels).mean() * 100:.2f}%",
|
||||
"预测上涨比例": f"{(ttm_preds == 1).mean() * 100:.1f}%"
|
||||
}
|
||||
},
|
||||
"集成策略性能": {
|
||||
"双模型交集": {
|
||||
"准确率": f"{((two_model_preds == true_labels) & (two_model_preds != -1)).sum() / (two_model_preds != -1).sum() * 100:.2f}%",
|
||||
"覆盖率": f"{(two_model_preds != -1).mean() * 100:.2f}%",
|
||||
"有效预测数": int((two_model_preds != -1).sum())
|
||||
},
|
||||
"三模型投票": {
|
||||
"准确率": f"{(three_model_preds == true_labels).mean() * 100:.2f}%",
|
||||
"覆盖率": "100.00%",
|
||||
"有效预测数": int(len(three_model_preds))
|
||||
}
|
||||
},
|
||||
"DLinear概率分布": {
|
||||
"0.4-0.5区间": f"{((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).sum()}个 ({((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).mean() * 100:.1f}%)",
|
||||
"0.5-0.6区间": f"{((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).sum()}个 ({((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).mean() * 100:.1f}%)",
|
||||
"0.6-0.7区间": f"{((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).sum()}个 ({((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).mean() * 100:.1f}%)",
|
||||
"平均概率": f"{dlinear_probs.mean():.3f}"
|
||||
},
|
||||
"最佳/最差交易日": {
|
||||
"双模型最佳日": {
|
||||
"日期": str(daily_stats['two_model_accuracy'].idxmax()),
|
||||
"准确率": f"{daily_stats['two_model_accuracy'].max():.1f}%"
|
||||
},
|
||||
"双模型最差日": {
|
||||
"日期": str(daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].idxmin()),
|
||||
"准确率": f"{daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].min():.1f}%"
|
||||
},
|
||||
"三模型最佳日": {
|
||||
"日期": str(daily_stats['three_model_accuracy'].idxmax()),
|
||||
"准确率": f"{daily_stats['three_model_accuracy'].max():.1f}%"
|
||||
},
|
||||
"三模型最差日": {
|
||||
"日期": str(daily_stats['three_model_accuracy'].idxmin()),
|
||||
"准确率": f"{daily_stats['three_model_accuracy'].min():.1f}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary_file = f"{results_dir}/summary_report_2026_01.json"
|
||||
with open(summary_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(summary, f, indent=2, ensure_ascii=False)
|
||||
print(f"✓ 保存总结报告: {summary_file}")
|
||||
|
||||
# 4. 生成Markdown报告
|
||||
markdown_report = f"""# 加密货币价格预测模型测试报告
|
||||
|
||||
## 测试概况
|
||||
|
||||
- **测试期间**: 2026年1月1日 - 2026年1月30日
|
||||
- **总天数**: {len(daily_stats)}天
|
||||
- **总样本数**: {min_samples}个小时
|
||||
- **生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
---
|
||||
|
||||
## 单模型性能
|
||||
|
||||
### DLinear分类模型
|
||||
- **准确率**: {(dlinear_preds == true_labels).mean() * 100:.2f}%
|
||||
- **预测上涨比例**: {(dlinear_preds == 1).mean() * 100:.1f}%
|
||||
- **预测下跌比例**: {(dlinear_preds == 0).mean() * 100:.1f}%
|
||||
|
||||
### N-BEATS回归模型
|
||||
- **准确率**: {(nbeats_preds == true_labels).mean() * 100:.2f}%
|
||||
- **预测上涨比例**: {(nbeats_preds == 1).mean() * 100:.1f}%
|
||||
- **预测下跌比例**: {(nbeats_preds == 0).mean() * 100:.1f}%
|
||||
|
||||
### TTM-R2零样本模型
|
||||
- **准确率**: {(ttm_preds == true_labels).mean() * 100:.2f}%
|
||||
- **预测上涨比例**: {(ttm_preds == 1).mean() * 100:.1f}%
|
||||
- **预测下跌比例**: {(ttm_preds == 0).mean() * 100:.1f}%
|
||||
|
||||
---
|
||||
|
||||
## 集成策略性能
|
||||
|
||||
### 双模型交集(DLinear + N-BEATS)
|
||||
- **准确率**: {((two_model_preds == true_labels) & (two_model_preds != -1)).sum() / (two_model_preds != -1).sum() * 100:.2f}%
|
||||
- **覆盖率**: {(two_model_preds != -1).mean() * 100:.2f}%
|
||||
- **有效预测数**: {(two_model_preds != -1).sum()}次
|
||||
|
||||
### 三模型多数投票(DLinear + N-BEATS + TTM-R2)
|
||||
- **准确率**: {(three_model_preds == true_labels).mean() * 100:.2f}%
|
||||
- **覆盖率**: 100.00%
|
||||
- **有效预测数**: {len(three_model_preds)}次
|
||||
|
||||
---
|
||||
|
||||
## DLinear概率分布分析
|
||||
|
||||
| 概率区间 | 样本数 | 占比 |
|
||||
|---------|--------|------|
|
||||
| 0.4-0.5 | {((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).sum()} | {((dlinear_probs >= 0.4) & (dlinear_probs < 0.5)).mean() * 100:.1f}% |
|
||||
| 0.5-0.6 | {((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).sum()} | {((dlinear_probs >= 0.5) & (dlinear_probs < 0.6)).mean() * 100:.1f}% |
|
||||
| 0.6-0.7 | {((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).sum()} | {((dlinear_probs >= 0.6) & (dlinear_probs < 0.7)).mean() * 100:.1f}% |
|
||||
|
||||
**平均概率**: {dlinear_probs.mean():.3f}
|
||||
|
||||
---
|
||||
|
||||
## 最佳/最差交易日
|
||||
|
||||
### 双模型交集
|
||||
- **最佳日**: {daily_stats['two_model_accuracy'].idxmax()} ({daily_stats['two_model_accuracy'].max():.1f}%)
|
||||
- **最差日**: {daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].idxmin()} ({daily_stats[daily_stats['two_model_accuracy'] > 0]['two_model_accuracy'].min():.1f}%)
|
||||
|
||||
### 三模型投票
|
||||
- **最佳日**: {daily_stats['three_model_accuracy'].idxmax()} ({daily_stats['three_model_accuracy'].max():.1f}%)
|
||||
- **最差日**: {daily_stats['three_model_accuracy'].idxmin()} ({daily_stats['three_model_accuracy'].min():.1f}%)
|
||||
|
||||
---
|
||||
|
||||
## 结论
|
||||
|
||||
1. **最佳单模型**: DLinear ({(dlinear_preds == true_labels).mean() * 100:.2f}%)
|
||||
2. **最佳集成策略**: 双模型交集 ({((two_model_preds == true_labels) & (two_model_preds != -1)).sum() / (two_model_preds != -1).sum() * 100:.2f}%)
|
||||
3. **DLinear置信度**: 99.2%的预测集中在0.45-0.55区间,缺乏高置信度预测
|
||||
|
||||
## 数据文件
|
||||
|
||||
- 逐小时预测结果: `hourly_predictions_2026_01.csv`
|
||||
- 每日统计数据: `daily_statistics_2026_01.csv`
|
||||
- 总结报告: `summary_report_2026_01.json`
|
||||
|
||||
---
|
||||
|
||||
生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
"""
|
||||
|
||||
markdown_file = f"{results_dir}/README_2026_01.md"
|
||||
with open(markdown_file, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_report)
|
||||
print(f"✓ 保存Markdown报告: {markdown_file}")
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("✓ 所有结果已保存到 ./results/ 目录")
|
||||
print("="*80)
|
||||
print(f"\n保存的文件:")
|
||||
print(f" 1. {hourly_file}")
|
||||
print(f" 2. {daily_file}")
|
||||
print(f" 3. {summary_file}")
|
||||
print(f" 4. {markdown_file}")
|
||||
print("\n" + "="*80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user