利用机器学习增强股票交易性能

image-20230711185631294

在当今快速发展的金融市场中,机器学习技术的集成已成为优化交易决策和管理风险的强大工具。ML 算法有助于分析大量数据、识别复杂模式并提供见解。

实施交易策略要求交易者仔细控制和监控市场和多个参数,例如水平、入场价格、止损和止盈价格、围绕这些水平的价格变动、头寸规模和风险管理。管理如此复杂的变量非常耗时,对精神和情感要求很高。

个人交易者亏损的主要原因之一不是市场,这已经不是什么秘密了。事实上,这是交易者自己和他们在做出交易决策时的情绪。很难找到失败的交易者的确切百分比,但根a验,这个数字高于 80%,有些人认为它是 95%。100个个体交易者中有95个在亏损!

因此,我想出了一个通过使用经过训练的机器学习算法来自动化决策过程的想法。这种方法允许交易者利用计算分析的力量来计算交易结果的概率,并做出客观和数据驱动的决策。

让我们要点该方法的主要优点:

  • 不受交易者情绪影响的客观和数据驱动决策
  • 提高效率和交易结果
  • 一致性和纪律性
  • 策略的回测和优化

机器学习和交易:一般方法

我的方法涉及:

  1. 在每个交易日结束时分析交易活动,并确定与所有可用美股策略一致的相关交易模型。
  2. 记录这些交易模型,并避免主动监控价格走势或在未来几天立即采取行动。
  3. 通过合并附加功能来增强记录的交易模型。
  4. 利用机器学习算法来计算有利交易结果的概率,并确定适当的风险管理措施。
  5. 如果计算的概率超过预定阈值,则根据交易模型启动交易头寸。

此策略意味着使用历史数据训练的使用分类模型,涉及多个阶段:

  1. 从1960年到2023年(63年)约53000种金融工具的数据收集
  2. 特征工程和将某种交易策略改编为 Python 代码
  3. 构建供 ML 使用的综合数据集
  4. 分类模型开发
  5. 评估模型
  6. 回溯测试和实验
  7. 交易模拟

让我们更详细地了解项目的每个部分,提供全面的概述以及展示分析过程和关键步骤执行的代码片段。

工具和技术

简而言之,我在项目中使用的主要数据分析工具列表:

  1. 努比
  2. 熊猫
  3. XGBoost
  4. Scikit-Learn
  5. Matplotlib
  6. 朱皮特笔记本

数据采集

为了有效地实现既定结果,全面和高质量的数据收集至关重要。该项目的这一步涉及持续收集相关的金融市场数据。

数据分为两个主要部分:

  1. 金融工具的技术数据(开盘价、最高价、最低价、收盘价)
  2. 反映公司绝大多数财务状况的工具发行人的基本面数据

对于第一部分,OHLC数据,我使用了基本面分析库,这是免费源代码。

但是,对于第二部分,即基本数据,我通过购买他们的服务选择了财务建模准备API。做出这一决定是因为从免费来源找到全面可靠的数据是一项挑战。通过使用他们的API,我能够访问广泛的基本财务信息,例如公司财务报表,关键比率和其他重要数据。

首先,我从数据提供者那里获得了有关所有可用资产的信息。

图表不言自明。股票类型以75.7%的可用资产占主导地位,数据集包括来自全球各种交易所的资产,包括纳斯达克,PNK,纽约证券交易所,XETRA等。我的主要兴趣在于评估该策略在美国市场的表现。

特征工程

在本节中,我用 Python 代码实现了交易策略,允许识别过去 63 年(1960-2023 年)所有 53,000 种工具的相关交易模型。识别和记录交易,包括其各自的结果和所有必要的特征。这个阶段的结果是创建了全面的熊猫数据帧,包括 2,758,931 笔交易、它们的结果和功能。

我将特征工程部分分为四个主要部分:

  1. 将交易策略转换为 Python 代码并从数据中检索所有相关交易
  2. 技术指标 特点工程
  3. 基本功能工程
  4. 生成全面的数据集,以便使用并行计算进行分析

将交易策略转换为 Python 代码

我在项目中使用的策略称为“突破级别之上”。它侧重于识别资产价格超过显着阻力位并继续在其上方盘整的突破。

交易者寻找强劲的看涨势头和持续的买入压力,以确认突破并在之前的阻力位建立新的支撑位。通过在价格稳定在突破水平上方时进入交易,交易者的目标是抓住上升势头并驾驭趋势以获得潜在利润。

阻力位

阻力位是由于做市商或“大玩家”的抛售压力而停止资产向上运动的价格水平。据信,这些实体由于其庞大的交易量或机构地位而对市场具有重大控制权。他们战略性地设置阻力位,以限制价格上涨并保持对市场动态的控制。

我的交易策略基于这样一个概念,即阻力位是根据价格高点构建的,不能被突破或至少在一段时间内(回溯期)。

对于这项任务,我开发了一个函数,该函数使用价格数据搜索指定数据帧内的所有相关级别。

def find_resistance_levels(df, lookback_period, min_volume): “”“ 根据给定的阻力周期和最小交易量查找数据帧中的长期阻力水平。参数---------- df : 熊猫.数据帧 包含 OHLC(开盘-盘高-盘低-收盘)数据的数据帧。resistance_period : int 查找最大高点时要考虑的前几天(行)数。min_volume : int 每天以美元为单位的最低交易量阈值。-------熊猫返回。数据帧 包含长期阻力水平的数据帧。“”“ resistance_levels = [] resistance_levels_df = pd。DataFrame() # 向后循环遍历数据帧,从最后一行开始,转到第一行。对于范围中的索引(len(df) - 1, -1, -1): # 获取当前索引处的行。row = df.iloc[index] # 确定查找最大高点时要查看的行窗口。如果索引 - lookback_period < 0: window_start = 0 否则: window_start = 索引 - lookback_period # 在窗口内找到最大高点。max_high = df[“high”][window_start:index].max() # 提取用于计算平均交易量的行子集。如果索引 - 10 <= 0: df_mean_vol = df.iloc[:index + 20] 否则: df_mean_vol = df.iloc[index - 10:index] # 检查当前行的高点是否大于或等于最大高点 # 以及子集的平均音量是否不低于最小音量阈值。如果 row[“high”] >= max_high 而不是 (df_mean_vol[“volume”] < min_volume).any(): row_dict = {“Level_Price”: row[“high”], “Formation_Date”: df.index[index]} resistance_levels.append(row_dict) # 如果有任何长期阻力位 # 创建阻力位的数据框并返回它。如果 len(resistance_levels) != 0: resistance_levels_df = PD。DataFrame(resistance_levels).sort_values(by='Formation_Date') resistance_levels_df.set_index('Formation_Date', inplace=True, drop=False) resistance_levels_df.index = resistance_levels_df.index.astype('datetime64[ns]') resistance_levels_df.drop(“Formation_Date”, axis=1, inplace=True) else: # 如果没有长期阻力位 # 返回一个空的数据帧。resistance_levels_df = pd。数据帧()

返回resistance_levels_df

例如,对于摩根大通的股票功能,已经找到了349个对应于1993年至2021年的初始参数水平。

突破模型(交易)

找到并存储水平后,我们需要找到高于这些水平的所有可能的突破日并添加相应的参数。

为此,我定义了一个函数,该函数遍历resistance_levels数据帧中的每个水平,并检查一天的开盘价是否低于某个水平,收盘价是否高于某个水平,并且该水平以前从未被突破过。如果满足条件并检测到突破,该函数将计算交易的其他技术参数,并将它们添加到结果数据帧中,例如:

  1. 工具的符号
  2. 仪器类型
  3. 交易资产的交易所简称
  4. 入门价格
  5. 止损价格
  6. 几种风险比率的止盈价格
  7. 水平的价格增量
  8. 传播
  9. 开盘价、最高价、最低价、收盘价、调整后收盘价和突破日交易量
  10. 级别价格,年龄和成立日期
  11. 回溯期
  12. 前 5 天的平均真实范围

该函数的结果是一个数据帧,其中包含 39 个突破模型,其中包含从 1993 年到 2021 年的相应参数。

突破模型结果和二元标签

为了获得交易结果,我开发了一个功能,可以扫描指定entry_window内的入场点并验证它们是否被激活。它还标识这些入口点的出现,以及它们相应的日期和持续时间。同样在此阶段,该函数已生成将用于训练机器学习算法的二进制标签。

def get_model_results(df, breakout_models, entry_window, risk_ratios): “”“ 基于突破模型和数据帧获取模型结果。参数---------- df : 熊猫.数据帧 包含 OHLC(开盘-高-盘-最低价-收盘价)数据的数据帧。breakout_models:熊猫。数据帧 包含突破模型的数据帧。entry_window : int 入口窗口持续时间。risk_ratios : 列出获利与止损比率列表 熊猫-------回报。数据帧 包含模型结果的数据帧。“”“ # 遍历索引的每一行breakout_models,breakout_models.iterrows() 中的行: # 在给定的入场窗口中搜索进入交易的点 df_entrypoint_search = df[df.index > index][:entry_window] entry_point = df_entrypoint_search[(df_entrypoint_search[”low“] <= row[”Entry_Price“]) & (df_entrypoint_search['high'] >= row[”Entry_Price“])].head(1) # 在breakout_models数据存储模型的年龄 数据帧 breakout_models.loc[index, “Model_Age”] = len(df[df.index <= index]) # 如果在入口窗口中找到入口点,如果 len(entry_point) == 1: # 在breakout_models中存储入口点的详细信息 数据帧 breakout_models.loc[index, “Entry_Point_Triggered”] = 1 breakout_models.loc[index, “Entry_Point_Date”] = entry_point.index[0].date() breakout_models.loc[index, “Entry_Point_Duration”] = (entry_point.index[0] - index).days breakout_models.loc[index, “Entry_Point_Day_of_Week”] = entry_point.index[0].dayofweek + 1 breakout_models.loc[index, “Entry_Point_Week”] = entry_point.index[0].week breakout_models.loc[index, “Entry_Point_Month”] = entry_point.index[0].month # 从入场点开始搜索交易结果 df_trade_result = df[df.index >= entry_point.index[0]] # 搜索触发止损的点 止损 = df_trade_result[df_trade_result[“低”] <= 行[“Stoploss_Price”]].head(1) # 存储日期和变量中的止损持续时间 sl_date = stoploss.index[0].date() 如果不是 stoploss.empty else none sl_duration = (stoploss.index - index).days if not stoploss.empty else None # 遍历risk_ratios以查找risk_ratios比率的止盈点: # 搜索触发给定风险比率的止盈点 止盈 = df_trade_result[df_trade_result[“高”] >= 行[f“Takeprofit_{ratio}/1_Price”]].head(1) # 存储日期和持续时间变量中的止盈 tp_date = 止盈.index[0].date() 如果不是止盈.空 否则 无 tp_duration = (止盈.索引 - 索引).天 如果不是止盈.空 否则 无 # 如果同时找到止损和止盈点 如果 sl_date 和 tp_date: # 如果在止盈之前发生止损,如果 sl_date <= tp_date: # 存储止损详细信息并将止盈指示为未触发 breakout_models.loc[index, [ f“Stoploss_{ratio}/1_Triggered”, f“Stoploss_{ratio}/1_Date”, f“Stoploss_{ratio}/1_Duration”, f“Takeprofit_{ratio}/1_Triggered”, f“Takeprofit_{ratio}/1_Date”, f“Takeprofit_{ratio}/1_Duration” ]]= [1, sl_date, sl_duration, 0, np.nan, np.nan] # 如果止盈发生在止损之前,否则: # 存储止盈详细信息并将止损指示为未触发 breakout_models.loc[index, [ f“Stoploss_{ratio}/1_Triggered”, f“Stoploss_{ratio}/1_Date”, f“Stoploss_{ratio}/1_Duration”, f“Takeprofit_{ratio}/1_Triggered”, f“Takeprofit_{ratio}/1_Date”, f“Takeprofit_{ratio}/1_Duration”

]] = [0, 例如, 例如南, 1, tp_date, tp_duration]

技术指标特点

尽管交易策略主要侧重于分析价格走势,而不使用SMA,HMA或EMA等技术指标,但作为数据科学家,我的目标是探索将它们纳入项目的潜在好处。通过从价格数据中计算一组指标,我将研究它们的预测能力,并评估它们是否可以提高机器学习算法的性能。

换句话说,我正在检查一个假设,即项目的交易策略不需要技术指标

将要计算的技术指标列表:

  1. 变化率
  2. 船体移动平均线 (HMA)
  3. 易于移动 (EOM)
  4. 科波克曲线
  5. 力指数
  6. 鳄鱼振荡器
  7. 枢轴点
  8. 百分比价格振荡器 (PPO)
  9. 威廉姆斯分形
  10. 相对活力指数 (RVI)
  11. 三重指数移动平均线 (TEMA)
  12. 去趋势价格振荡器 (DPO)
  13. 平衡量比率 (OBV_Ratio)
  14. 埃勒斯·费舍尔变换
  15. 费舍尔变换
  16. 力量平衡
  17. 简单移动平均线 (SMA)
  18. 标准偏差
  19. 布林带
  20. 与平均值的距离

# 定义函数来计算技术指标def rate_of_change(df, n, high_column='high'): “”“ :p aram df: 包含价格数据的数据帧 :p aram n: ROC 计算的周期数 :p aram high_column: 最高价的列名(默认:”最高价“) :return: ROC 值作为数据帧列 ”“” delta = df[high_column].diff(n) prev_price = df[high_column].shift(n) df['ROC'] = (delta / prev_price) * 100 返回 dfdef hull_moving_average(df, n, close_column='收盘'): “”“ :p aram df: 包含收盘价数据的数据帧 :p aram n:计算赫尔移动平均线的周期 :p aram close_column:收盘价的列名(默认:”收盘价“) :return: 船体移动平均值作为数据帧列 ”“” wma_half_n = 2 * df[close_column].rolling(window=n // 2).mean() wma_full_n = df[close_column].rolling(window=n).mean() weighted_wma = wma_half_n - wma_full_n df['HMA'] = weighted_wma.rolling(window=int(n ** 0.5)).mean() 返回 dfdef ease_of_movement(df, n=1, high_column='high', low_column='low', volume_column='volume'): “”“ :p aram df:包含高、低和交易量数据的数据帧 :p aram n:计算移动难易度的周期(默认值为 1) :p aram high_column:最高价的列名称(默认:”最高价“) :p阿拉姆low_column: 最低价的列名称(默认:“低”) :p aram volume_column:卷的列名(默认:“卷”) :return:数据帧列“”的移动便利性值 列“”“ distance_moved = ((df[high_column] + df[low_column]) / 2) - ((df[high_column].shift(1) + df[low_column].shift(1)) / 2) box_ratio = (df[volume_column] / 1e6) / (df[high_column] - df[low_column]) EOM = distance_moved / box_ratio df['EOM'] = eom.rolling(window=n).mean()

返回DF

基本特征

关于基本特征的一般想法是为每个交易找到公司的某个财务报告,并使用报告中的数据来训练 ML 分类算法。通过在训练数据中包含这些基本特征,算法可以学习公司的财务业绩与交易结果之间的模式和关系。

以下是收集的 132 个指标中的 6 个列表:

  1. 收入增长:增加销售额
  2. 毛利增长:盈利能力增长
  3. 息税前利润增长:营业利润率提高
  4. 营业利润增长:营业利润增加
  5. 净收入增长:更高的盈利能力
  6. 每股收益增长:每股收益上升

以下代码是添加资产负债表报表的示例:

def add_balance_sheet_history(breakout_models_with_results, balance_sheet_df): “”“ 向分组讨论模型添加资产负债表历史记录功能。参数---------- breakout_models_with_results:数据帧 具有现有结果的突破模型。balance_sheet_df:数据帧 资产负债表数据帧。返回-------数据帧 具有添加的资产负债表历史记录功能的突破模型。“”“ # 如果不是 balance_sheet_df.empty 且不是 breakout_models_with_results.empty,则检查两个数据框是否都不为空: # 迭代索引中的每一行,breakout_models_with_results.iterrows() 中的行breakout_models_with_results: # 从当前行索引之前或等于balance_sheet_df获取最新的资产负债表报告 past_balance_report = balance_sheet_df[ balance_sheet_df.index <= 索引 ].tail(1) # 将前缀”Balance_Sheet_“添加到past_balance_report past_balance_report 中的列名称 = past_balance_report.add_prefix('Balance_Sheet_') # 如果找到过去的资产负债表报表,如果不是 past_balance_report.empty: # 使用资产负债表报表中的值更新 breakout_models_with_results 中的相关列 past_balance_report.columns: breakout_models_with_results.loc[ 索引, 列 ] = past_balance_report[列].值[0] # 如果未找到过去的资产负债表报表,则其他: # 将 past_balance_report 中的列的值设置为“无”。列: breakout_models_with_results.loc[ 索引,列 ] = 无

返回breakout_models_with_results

构建用于机器学习的综合数据集

在本节中,我使用并行计算为所有交易工具生成了一个全面的数据集。数据集是通过使用特定函数处理资产列表而创建的。这些函数旨在加载和操作金融工具数据。生成的数据被合并到数据帧中,并追加到共享列表中。

为了简化处理,资产列表被分成更小的块,每个块由一个 CPU 核心(在我的例子中为 24 个)处理。每个进程在其分配的资产块上执行必要的功能。然后收集结果并组合以形成一个整体数据帧。

def assets_process(assets_list, number_of_processes): “”“ 使用多个进程并行处理资产列表。参数:assets_list(列表):要处理的资产列表。number_of_processes(整数):用于并行处理的进程数。返回: pd.数据帧:包含已处理结果的整体数据帧。“”“ # 获取可用 CPU 内核数 cpu_count = mp.cpu_count() # 将进程数限制为 CPU 核心数 n_processes = min(number_of_processes, cpu_count) # 如果number_of_processes超过 CPU 内核数,则打印消息 number_of_processes > cpu_count: print(f'进程数上限为最大可用 CPU 内核数: {n_processes}') print() else: print(f“Number of processes: {n_processes}”) print() # 将股票列表分成“number_of_processes”部分 x = list(divide_list_into_chunks(assets_list, n_processes)) # 打印有关处理斯托克斯的信息 print(f“处理金融工具总数: {len(assets_list)}”) # 创建管理器对象管理器 = 管理器() # 使用管理器创建共享列表 result_list = manager.list() # 创建进程列表以运行 'test_m1' 函数并将共享列表作为附加参数进程 = [mp.进程(目标=generate_comprehensive_df, args=(symbol, result_list)) 对于 x 中的符号] 如果 name == “main”: # 启动进程中进程的每个进程: process.start() # 等待每个进程完成 进程中的进程: 进程.join() 如果 len(result_list) == 0: 返回 pd。DataFrame() # 所有进程完成后,从共享列表中收集数据并创建一个整体数据帧 overall_dataframe = pd.concat([result for _, result in result_list], axis=0) overall_dataframe[“Model_Date”] = overall_dataframe.index overall_dataframe.insert(0, “Model_Date”, overall_dataframe.pop(“Model_Date”)) overall_dataframe.reset_index(drop=True, inplace=True) # 返回overall_dataframe

返回overall_dataframe

生成的 DataFrame 包含 2,758,931 行,代表各种交易模型。磁盘上数据帧的总大小为 8.78 GB。

综合数据帧 .info()

DF 的每一行包含:

  • 与交易策略参数对应的交易。
  • 交易品种名称、工具类型和交易所名称。
  • 开盘价、最高价、最低价、收盘价和交易量。
  • 92项技术指标。
  • 53 交易参数。
  • 不同风险比率的交易结果。(带日期的二进制标签)
  • 132 基本指标

收集的数据探索

若要深入了解收集的数据帧并了解重要指标的分布,执行探索性分析是有益的。在这方面,我开发了一个函数来分析收集的数据帧并生成可视化效果,以提供对重要指标的见解。

此函数创建了条形图,说明了不同风险比率的盈利交易与总交易的年度比率。盈亏平衡阈值也被计算并显示在图表上。

此外,使用线性回归分析将趋势线拟合到数据中,以可视化任何潜在趋势。对年度盈利交易总数执行了相同的过程,提供了进一步的统计信息。这些可视化具有趋势线和盈亏平衡阈值,可以全面了解所分析指标的分布和趋势。

函数的结果(红色条 — 无利可图年份,绿色 — 盈利年份)

经进一步研究,我观察到2000年后风险比率有明显下降的趋势。这促使我调查根本原因。在分析年平均止损大小后,我发现在 1999 年之后,这个比率显着飙升了 1000%。

年平均绝对止损大小

因此,我想出了一个想法来确定我的策略中止损计算的修正因子。我再次重建了全面的数据帧。看看结果:

左 — 更正前,右 — 更正后

通过简单地添加基本修正因子,1999年后利润率明显提高,右侧图上的“红条”略少。

当然,确保更正与战略中的风险管理规则保持一致至关重要。因此,对交易头寸规模进行了一些调整,但总体而言,修正是成功的,并且符合风险管理原则。

我必须注意到,只有通过观察数据并进行非常基本的更正,我才能在应用任何机器学习技术之前改进交易策略。真棒!

构建机器学习模型

我为自己设定了一个目标,通过使用 ML 算法消除“红条”。换句话说,我很好奇是否有可能使所有交易年都盈利。让我们讨论一下我以这种方式面临的问题。

算法选择

我花了数百小时回溯测试和评估这些算法,最终随机森林分类器表现最好。

虽然总的来说,我测试了:

  1. XGBoost 分类器
  2. 线性 SVC
  3. K 最近邻分类器
  4. 支持矢量分类器
  5. 随机森林分类器
  6. Sci-Kit Learn Map对我帮助很大。

选择正确的估算器

这是我的培训功能:

def clf_train( df, train_period_start, train_period_end, test_period_start, drop_list, tp, n_estimators, max_depth, random_state, criteria, min_samples_leaf, verbose, class_weight, min_samples_split): “”“ 使用随机森林分类器执行分类。参数: - df:包含输入数据集的数据帧。- train_period_start:培训期的开始日期。- train_period_end:培训期的结束日期。- test_period_start:测试期的开始日期。- drop_list:要从数据集中删除的列的列表。- tp:目标变量列名。- n_estimators:随机森林中的树木数量。- max_depth:每棵树的最大深度。- random_state:用于随机数生成的种子。- 标准:衡量拆分质量的函数。- min_samples_leaf:叶节点所需的最小样本数。- 详细:日志记录的详细级别。- class_weight:与不平衡数据的类关联的权重。- min_samples_split:拆分内部节点所需的最小样本数。返回: - y_probs:每个类的预测概率。 - y_preds:预测的类标签。- y_test:真正的类标签。- clf:经过训练的分类器。- X_train:训练数据特征。- y_train:训练数据目标变量。- X_test:测试数据功能。- y_test:测试数据目标变量。- y_probs_positive:正类的预测概率。 - y_probs_negative:负类的预测概率。 “”“ # 准备数据集进行建模 df_X = make_train(df, train_period_start, train_period_end, test_period_start) # 准备特征矩阵 (X) 和目标变量 (y) X = df_X.drop(drop_list, axis=1) y = df_X[tp] # 将数据集拆分为训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, random_state=0, shuffle=True) # 如果需要特征缩放,请取消注释以下行 # 缩放器 = 标准缩放器() # X_train = scaler.fit_transform(X_train) # X_test = 缩放器.transform(X_test) # 使用指定参数实例化随机森林分类器 clf = 随机森林分类器( n_estimators=n_estimators, max_depth=max_depth, random_state=random_state, n_jobs=-1, 标准=标准, min_samples_leaf=min_samples_leaf, 详细=详细, class_weight=class_weight, min_samples_split=min_samples_split ) # 将分类器拟合到训练数据 clf.fit(X_train, y_train) # 获取特征重要性分数 feature_importance = clf.feature_importances_ # 对特征重要性分数进行排序并获取排序索引 sorted_indices = np.argsort(feature_importance)[::-1] # 按重要性降序获取特征名称 feature_names = X.columns[sorted_indices] # 使用分类器对测试集进行预测 y_preds = clf.predict(X_test) # 使用分类器计算正类的预测概率 y_probs = clf.predict_proba(X_test) y_probs_positive = y_probs[:, 1] y_probs_negative = y_probs[:, 0] # 返回预测概率、预测值、真值、分类器、训练数据、测试数据、# 正概率、负概率、训练数据长度和特征名称返回(y_probs、y_preds、y_test、clf、X_train、y_train、X_test、y_test、y_probs_positive、y_probs_negative

)

阶级失衡

为了防止模型超过其中一个类,我必须开发自己的函数来实现 50/50 类比。我知道随机森林clf有自己的内置函数来平衡类,但测试表明我的方法给出了更好的结果。

def make_50x50(df): “”“ 通过随机抽取较大类的子集来平衡数据集,以实现 50-50 的比率。参数:df(数据帧):包含数据集的输入数据帧。返回:数据帧:平衡的数据集。“”“ # 根据目标变量将数据集分成两个类 df_class_A = df[df[tp] == 1] df_class_B = df[df[tp] == 0] # 获取两个类的大小 size_class_A = df_class_A.shape[0] size_class_B = df_class_B.shape[0] # 如果size_class_A > size_class_B,则随机抽取较大类数据帧的子集以平衡比率: df_class_A = df_class_A.sample(size_class_B, random_state=42) 否则: df_class_B = df_class_B.sample(size_class_A, random_state=42) # 连接两个平衡类数据帧 df_50x50 = pd.concat([df_class_A, df_class_B]) # 打乱串联数据帧的行 df_50x50 = df_50x50.sample(frac=1, random_state=42)

返回df_50x50

数据泄露

在时间序列分析中解决数据泄漏问题非常重要。就我而言,算法从标记数据中学习,例如是否触发了止盈。但是,由于止盈事件的发生需要时间,因此在训练时避免未来的数据泄漏至关重要。

为了解决这个问题,我创建了自定义训练/测试拆分函数。这些函数根据特定日期筛选数据。训练数据集被构造为仅包含测试期开始之前的数据,确保止盈或止损事件的标签不包含未来信息。

换句话说,训练数据集中的任何数据都不能包含来自测试数据集的任何事件。

def make_train(df, train_period_start, train_period_end, test_period_start): “”“ 通过应用筛选器和平衡类来准备训练数据集。参数:df(数据帧):包含原始数据的输入数据帧。train_period_start (str):训练期的开始日期。train_period_end (str):训练期的结束日期。test_period_start (str):测试期的开始日期。返回:数据帧:已处理的训练数据集。“”“ # 根据日期范围过滤数据 df_train = df[df.index >= train_period_start] df_train = df_train[df_train.index < train_period_end] df_train = df_train[df_train[tp_date] < str(test_period_start)] df_train = df_train[df_train[sl_date] < str(test_period_start)] # 根据类型和交易所过滤数据 df_train = df_train[df_train['类型'] == '股票'] df_train = df_train[df_train['交易所'].isin(['纽约证券交易所'])] df_train = df_train[df_train['Level_Age'] >= 4] # 余额类 df_train = make_50x50(df_train) 返回df_traindef make_test(df, test_period_start, test_period_end): “”“ 通过应用筛选器来选择相关数据来准备测试数据集。参数:df(数据帧):包含原始数据的输入数据帧。test_period_start (str):测试期的开始日期。test_period_end (str):测试期的结束日期。返回:数据帧:已处理的测试数据集。“”“ # 根据日期范围筛选数据 df_test = df[df.index >= test_period_start] df_test = df_test[df_test.index < test_period_end] df_test = df_test[df_test['类型'] == '股票'] df_test = df_test[df_test['交易所'].isin(['NYSE', 'NASDAQ'])]

返回df_test

功能选择

为了确定数据集广泛的 250 个特征中最有价值的特征,我采用了几种技术,包括:

  1. 功能重要性评分
  2. 相关分析
  3. 动手测试

在这些方法中,实践测试被证明是最有效的。

通过大量测试,我发现所有92个技术指标(ROC,HMA,EOM,e.t.c.)都没有为分类任务贡献任何额外的价值。这一发现强化了这样一种观念,即对于我的项目的交易策略,只关注围绕水平和基本指标的价格变动至关重要。

老实说,我真的很佩服我通过使用ML算法证明了我的理论。

过拟合

对于任何机器学习任务来说,这是一个非常普遍的问题,当算法在训练和过度拟合时非常了解数据,而在看不见的数据上,它很难很好地泛化。

就我而言,通过应用这些技术解决了问题:

  1. 正则化以降低 clf 的复杂性(max_depth、min_samples_leaf、min_samples_split)。
  2. 训练/测试拆分为单独的训练和测试期以评估性能。
  3. 具有交叉验证功能的大量评估指标

模型评估

长话短说,这些是我用来评估模型的指标:

  1. 准确性(训练):模型对训练数据的准确性。
  2. 准确性(测试):模型对测试数据的准确性。
  3. 精度:精度分数,用于衡量模型正确识别阳性实例的能力。
  4. 召回率:召回率分数,用于衡量模型正确识别所有阳性实例的能力。
  5. F1 分数:F1 分数,它是精度和召回率的调和平均值,提供模型性能的平衡度量。
  6. ROC AUC:接收器工作特征曲线 (ROC AUC) 下的面积,用于衡量模型区分正负实例的能力。
  7. PR AUC:精确召回曲线下面积 (PR AUC),用于衡量不同分类阈值的精度和召回率之间的权衡。
  8. 特异性:也称为真阴性率,它衡量模型正确识别负实例的能力。
  9. 混淆矩阵:是一个表,通过显示真阳性、真阴性、假阳性和假阴性预测的计数来汇总分类模型的性能,从而允许评估模型的准确性和错误率。

我开发了一个函数来计算和绘制这些指标。

def model_evaluation(df_test, df_train, y_test, y_preds, y_probs_positive, X_train, y_train, new_threshold, show_evaluation): “”“ 此函数通过显示评估指标和绘图来评估模型。计算的指标包括准确性、精度、召回率、F1 分数、ROC AUC、PR AUC 和特异性(真阴性率)。生成的图包括 ROC 曲线、精度-召回率曲线和混淆矩阵。参数:df_test(数据帧):测试数据集。df_train(数据帧):训练数据集。y_test(类似数组):测试数据集的实际标签。y_preds(类似数组):模型预测的标签。y_probs_positive(类似数组):正类的预测概率。 X_train(类似数组):训练数据集的特征。y_train(类似数组):训练数据集的标签。new_threshold(浮点):基于概率预测类标签的阈值。show_evaluation(布尔值,可选):如果为 True,则该函数将打印评估指标和绘图。默认值为 True。返回:包含训练数据的准确性、测试数据的准确性、精度、召回率、F1 分数、ROC AUC、PR AUC 和特异性(真阴性率)的元组。所有值均以百分比表示。“”“ # 通过将预测概率与新阈值进行比较来生成预测类 y_preds = (y_probs[:, 1] >= new_threshold).astype(int) # 计算指标并创建绘图 accuracy_train = round(clf.score(X_train, y_train) * 100, 2) # 训练数据的准确性分数 accuracy_test = round(accuracy_score(y_test, y_preds) * 100, 2) # 测试数据精度的准确度分数 精度 = round(precision_score(y_test, y_preds) * 100, 2) # 精度分数召回率 = 轮次(recall_score(y_test, y_preds) * 100, 2) # 召回率 f1 = 轮次(f1_score(y_test, y_preds) * 100, 2) # F1 分数 test_proportion = len(df_test) / (len(df_test) + len(df_train)) * 100 # 测试数据比例 train_proportion = len(df_train) / (len(df_test) + len(df_train)) * 100 # 训练数据precision_list比例, recall_list,阈值 = precision_recall_curve(y_test == 1, y_probs_positive) # 获取精度、召回率和阈值 auc_pr = auc(recall_list, precision_list) # 精度-召回率曲线下的面积 # 计算 ROC 曲线和 AUC fpr, tpr, 阈值 = roc_curve(y_test, y_probs_positive) roc_auc = roc_auc_score(y_test, y_probs_positive) # 创建混淆矩阵显示 cm_normalized = confusion_matrix(y_test, y_preds, normalize='pred') cm_normalized_display = ConfusionMatrixDisplay(cm_normalized) cm = confusion_matrix(y_test, y_preds) cm_display = ConfusionMatrixDisplay(cm) # 计算特异性(也称为真阴性率) tn, fp, fn, tp = confusion_matrix(y_test, y_preds).ravel() 特异性 = tn / (tn+fp) * 100 如果show_evaluation: print(f“Accuracy score (train): {accuracy_train:.2f}%”) print(f“Accuracy score (test): {accuracy_test:.2f}%“) print(f”Precision score: {precision:.2f}%“) print(f”Recall (True Positive Rate): {recall:.2f}%“) print(f”Specificity (True Negative Rate): {specificity:.2f}%“) print(f”F1 score: {f1:.2f}%“) print(f”Training data size: {len(df_train):,}“) print(f”Testing data size: {len(df_test):,}“) print(f”Test/Train ratio: {len(df_test):,}/{len(df_train):,} ({test_proportion:.2f}%/{train_proportion:.2f}%)“) print(f”ROC AUC: {roc_auc:.3f}“) print(f”PR AUC: {auc_pr:.3f}“) # 在同一图中绘制 ROC 曲线、精度召回率曲线和混淆矩阵 图, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2, figsize=(12, 12), dpi=300) ax1.plot(fpr, tpr, label='ROC curve (AUC = {:.3f})'.format(roc_auc)) ax1.set_xlabel('假阳性率') ax1.set_ylabel('真阳性率') ax1.set_title('受试者工作特性 (ROC) 曲线')ax1.legend(loc='best') ax1.grid(visible=True) ax1.set_axisbelow(True) ax2.plot(recall_list, precision_list, label='AUC-PR = {:.3f}'.format(auc_pr)) ax2.set_xlabel(“召回”) ax2.set_ylabel(“精度”) ax2.set_title(“精度-召回率曲线”) ax2.legend(loc='best') ax2.grid(visible=True) ax2.set_axisbelow(True) cm_normalized_display.plot(ax=ax3, cmap='Blues') ax3.set_xlabel('Predict label') ax3.set_ylabel('True label') ax3.set_title('Confusion Matrix Normalized')ax3.grid(visible=False) cm_display.plot(ax=ax4, cmap='Blues') ax4.set_xlabel('Predict label') ax4.set_ylabel('True label') ax4.set_title('Confusion Matrix') ax4.grid(visible=False) plt.tight_layout() # 调整子图之间的间距 plt.show();

返回accuracy_train、accuracy_test、精密度、召回率、F1、roc_auc、auc_pr、特异性

模型评估指标示例

ROC, 召回曲线和混淆矩阵

交易绩效评估

此外,我还开发了一种专门针对交易角度的定制评估方法。

它侧重于使用交易模型执行的交易(ML 交易)与不依赖模型执行的交易(非 ML 交易)相比。该报告提供了关键交易指标,包括 ML 交易数量、ML 利润率、非 ML 交易数量、非 ML 利润率以及通过合并模型实现的利润率提高。

模型性能年度报告的一部分

培训、回测和实验

在我的方法中,我采用滑动窗口技术,训练测试分为 20 年的培训,1 年的测试。

滑动窗口技术

这意味着我在 20 年的时间段内训练分类器,并在随后的 1 年期间评估其性能。此外,为了模拟真实的交易条件,我遵循年度培训方法。这意味着我每年使用可用数据训练一次模型,直到那时为止。通过每年更新和重新训练模型,我的目标是捕捉金融市场的动态和演变模式,使模型能够适应不断变化的市场条件并随着时间的推移提高其性能。

# 设置年度testperiod_start_date的开始和结束日期 = '1965-01-01'period_end_date = '2022-01-01'# 将字符串日期转换为日期时间 objectsperiod_start_date = pd.to_datetime(period_start_date)period_end_date = pd.to_datetime(period_end_date)# 设置训练和测试窗口的长度 yearstrain_window_length_years = 20test_window_length_years = 1risk_ratio = 3 # 风险比率值# 函数调用来计算目标值及其各自的日期tp, sl, tp_date, sl_date = target(risk_ratio)# 初始化用于存储评估指标的空列表和用于总体结果的空数据帧,predictionsevaluation_metrics = []evaluation_df = pd。DataFrame()overall_results_df = pd.DataFrame()df_preds_overall = pd.DataFrame()# 设置训练窗口和测试窗口之间的间隔长度,yearsgap_length_years = 1# 在指定时间段内每年循环用于范围(period_start_date.年、period_end_date.年 - train_window_length_years + 1)中的train_year: # 设置每个训练窗口的开始和结束日期 train_period_start = pd.to_datetime(f'{train_year}-01-01') train_period_end = pd.to_datetime(f'{train_year + train_window_length_years - 1}-12-31') # 循环在给定训练test_year窗口的测试窗口中,每年在范围(train_period_end.year + gap_length_years、train_period_end.year + gap_length_years + test_window_length_years)的测试窗口中: clear_output(等待=True) # 清除每个测试年份的输出 # 设置每个测试窗口的开始和结束日期 test_period_start = pd.to_datetime(f'{test_year}-01-01') test_period_end = pd.to_datetime(f'{test_year + test_window_length_years - 1}-12-31') # 如果测试期的开始时间晚于指定的结束日期,test_period_start > period_end_date则在以下情况下退出循环: 中断 # 为分类器设置超参数 random_state = 0 n_estimators = 500 max_depth = 4 标准 = “基尼” class_weight = 无 min_samples_leaf = 2 min_samples_split = 2

详细 = 0

成果和成就

经过广泛的实验和微调,我在交易模型上取得了令人印象深刻的结果。下面的年度报告展示了该模型在过去37年的表现。

利润率提高

该模型实现了值得称赞的平均利润率 64.53%,风险比为 3 比 1,而非 ML 比率为 38,03%。这表明在此期间平均改善了26.51%。

1985年至2022年模型交易模拟年度报告

但最重要的成就是该模型实现了所有 37 年的盈利,风险比为 3 比 1。

左侧为非 ML 交易,右侧为 ML 交易

通过风险管理进行交易模拟

为了评估模型在不同时间段的表现,并使用风险管理评估账户的增长,我开发了几个函数来模拟交易场景。这些功能使我能够检查帐户随时间变化的轨迹。

有趣的是,将风险值增加到初始存款的 1.7%,很明显,只有风险比率为 3、5 和 15 比 1 的策略才能维持并达到模拟期的结束。而风险比为10和30比1的策略到2016年耗尽了存款,无法继续。这进一步强调了在交易中实施风险管理原则的至关重要性。

当你冒太多风险时会发生什么

交易结果的分布

我应该提到的另一件有趣的事情是交易结果的分布,例如止盈和止损持续时间。

让我们看看其他指标:

梅特西斯分布

这里第一个有趣的方面是平均真实范围(ATR5)的分布。表现出选择波动性较低的股票的趋势。平均真实范围是衡量价格波动性的指标,表示股票最高价和最低价之间的平均范围。

第二个方面是水平价格的分布,或者基本上是指工具的价格。选择的绝大多数股票都位于 4 美元以下。这是进一步调查的良好开端。

限制

虽然该模型显示出令人印象深刻的结果,但从我的角度来看仍然存在一些局限性:

  1. 无实时测试:该模型缺乏实时交易条件下的测试,影响了其在实时市场动态下的表现。
  2. 缺乏多元化:该模型主要关注价格变动和基本指标,忽略了行业多样性和地理风险等因素。这可能会使战略面临部门或地理风险。
  3. 对历史数据的依赖:该模型依赖于历史数据,可能无法准确反映受政治或经济危机等不可预测事件影响的未来市场状况。
  4. 可能的过度拟合:尽管努力防止过度拟合,但仍然存在一定程度的过度拟合,从而限制了模型泛化到新数据的能力。
  5. 关注低价股票:该模型表现出对低价股票的偏好,可能会限制其对高价或蓝筹股的有效性,并使其面临与低价股票相关的风险。

未来方向

潜在的未来方向

  1. 实时交易实施:随着模型的发展并表现出稳健的性能,未来的方向可能是将其过渡到实时交易。通过利用实时市场数据,该模型可以实时生成预测,使其能够做出自动交易决策。这一进步将有助于开发一个能够根据模型预测执行交易的全自动交易系统。
  2. 新闻和情绪分析的整合:未来发展的一个有希望的途径是将新闻和情绪分析整合到模型中。通过利用 GPT-3 等高级语言模型,可以在包含新闻、谣言和其他相关信息的大量文本数据上训练该模型。这种集成将使模型能够捕获和分析新闻事件对金融工具的影响,增强其预测能力。
  3. 多模态分析:为了增强模型对市场动态的理解,未来的迭代可以探索多模态分析。这种方法将涉及整合来自不同来源的信息,例如传统数据、新闻、社交媒体情绪和热门话题。通过综合这些不同的输入,该模型可以更全面地了解市场情绪,并有可能提高其预测价格走势的准确性。
  4. 模型细化:模型的持续细化对于优化其性能至关重要。这可能涉及探索替代机器学习和深度学习模型、微调超参数和完善特征工程技术。通过不断迭代和改进模型架构,可以提高其预测准确性。

结论

总之,我的项目展示了机器学习在开发基于技术特征和基本指标的交易策略方面的潜力。该模型已经显示出有希望的结果,实现了有利可图的结果,并且在利润率方面优于非ML交易方法。但是,有几个领域需要进一步关注和完善。整合新闻和情绪分析,探索多模态分析,并结合实时测试,将增强模型的预测能力和对不断变化的市场条件的适应性。

此外,解决诸如缺乏多样化、依赖历史数据、特征选择有限、潜在过度拟合、专注于低价股票以及该过程耗时等限制将有助于制定更稳健、更可靠的交易策略。随着不断发展和改进,该项目有可能大幅改善交易实践,并有助于金融市场上更明智的投资决策。

你可能感兴趣的:(java,后端,架构)