下面是一个交易环境,在这个环境中,所有可能的交易策略都可以以一种非常动态的方式进行测试,即使是初学者python程序员也可以创建和回溯测试自己的交易想法,并最终解决他们的疑问。使用环境并实现您自己的想法的唯一先决条件是:对python结构(如for循环和if语句)有基本的了解,对pandas有足够的了解,以便从dataframe的正确列/索引位置获取适当的价格dataframe,可能对面向对象编程有较低层次的理解,这取决于您的策略的复杂性。
收集数据
首先读取数据。在此脚本中,我们使用了binance API,该API允许从交易所中列出的各种加密货币中收集数据。给出的示例是一个单独的脚本,从2019年3月28日到2020年6月1日,读取12种不同加密货币的每种加密货币的价格变化,总共618290个实例的价格,从而产生211 MB的dataframe。在我的机器上,这个从binance API读取的过程大约需要5个小时才能完成,但是通过一个简单的csv写操作,在以后使用脚本时几乎可以立即检索到。
关于数据读取的一些值得注意的事情:
1. 要访问binance API,只需从命令行pip install binance client。其他依赖项安装在其各自的导入行上注释。
2. binance客户端请求API密钥时,这些密钥仅对于将交易订单实时发送到币安交易所是必需的,而对于读取数据则不是必需的。我保留了格式,以防这是任何读者所期望的用例。
3. 在收集了每个代币的数据之后,我们需要让脚本停止运行60秒。这是为了防止binance API因为我们请求太多的数据而把我们踢出去。我目前没有遇到过这种问题,但如果遇到错误,这个时间可能需要增加。
4. 这里包括一个简单的移动平均函数,它展示了如何从价格数据中设计特性并将其添加到dataframe中的示例。这可以是您选择的自定义函数,也可以只是一个在网上找到的通用指示器的函数,如图所示。
5. 卷数据也可用,但在本例中未使用。如果您感兴趣,请将其添加到第32行。
6. csv文件的自定义命名可以在第72行的引号中编辑。
import numpy as np #pip install numpy
from tqdm import tqdm #pip install tqdm
from binance.client import Client #pip install python-binance
import pandas as pd #pip install pandas
from datetime import datetime
import random
SMA_LOW = 40
SMA_HIGH = 150
def compute_sma(data, window):
sma = data.rolling(window=window).mean()
return sma
#select cryptocurrencies you'd like to gather and time interval
ratios = ['BTC','ETH','LTC','XLM','XRP','XMR','TRX','LINK','IOTA','EOS','DASH','ZRX']
START_TIME = '28 Mar, 2019'
END_TIME = '1 Jun, 2020'
api_key=''
api_secret=''
client = Client(api_key=api_key,api_secret=api_secret)
merge = False
for ratio in ratios:
print(f'Gathering data...')
data = client.get_historical_klines(symbol=f'USDT',interval=Client.KLINE_INTERVAL_1MINUTE,start_str=START_TIME,end_str=END_TIME)
cols = ['time','Open','High','Low',f'-USD_close',f'-USD_volume','CloseTime','QuoteAssetVolume','NumberOfTrades','TBBAV','TBQAV','null']
temp_df = pd.DataFrame(data,columns=cols)
temp_df = temp_df[['time',f'-USD_close']]
if merge == False:
df = temp_df
else:
df = pd.merge(df,temp_df,how='inner',on='time')
merge = True
print('complete')
time.sleep(60) #sleep for a bit so the binance api doesn't kick you out for too many data asks
for col in df.columns:
if col != 'time':
df[col] = df[col].astype(np.float64)
for ratio in ratios:
df[f'_'] = compute_sma(df[f'-USD_close'], SMA_LOW)
df[f'_'] = compute_sma(df[f'-USD_close'], SMA_HIGH)
#clip NaNs
df = df[SMA_HIGH:]
df = df.reset_index(drop=True)
#convert binance timestamp to datetime
for i in tqdm(range(len(df))):
df['time'][i] = datetime.fromtimestamp(int(df['time'][i]/1000))
df.to_csv('12-coins-Mar18_Jun20')
太好了,现在我们有了一个庞大的加密货币价格数据集。在进入交易环境之前,让我们创建一个简单的memory 类,这样我们可以存储关于每个回测事件的信息。
Memory
class Memory:
def __init__(self):
self.clear()
def clear(self):
self.actions = []
def add_to_memory(self, new_action):
self.actions.append(new_action)
这个类很容易解释;它就是一个简单的Memory对象,它允许我们附加购买/出售操作,还可以在每周期之后清除Memory。
在我们有了数据和Memory对象,让我们开始进入交易环境。
我们的环境采用两个参数:我们刚刚创建的dataframe以及您希望在回溯测试过程中包含的货币比率(例如“BTC”、“LTC”等)。
我们的环境还有另两个方法:reset()和step()。
这是另一个无需过多解释的项目:它只是将交易环境重置为其默认状态。这样做的目的是在运行多个周期的回测时,我们将在每个周期之后调用env.reset()来重置所有局部变量,例如代币余额,周期dataframe的索引位置 跟踪您的特定策略以及可能对日志记录有用的任何其他信息。环境的默认状态是USD余额为1.0,每种加密货币的余额为0.0。有关reset()方法的一些重要注意事项:
请注意,每次重置都会占用主要dataframe的一小部分,并将其称为“ episode_df”,而不是在回测期间遍历整个600,000多个数据实例。这是该策略将针对每个周期进行回测的时间间隔。可以使用常量MINUTES_PER_EPISODE或DAYS_PER_EPISODE来指定此周期dataframe的持续时间。在此示例中,我们使用1天或1440分钟的周期大小。
在启动环境时(在__init__中),将调用reset()方法。这将在第一次调用Env对象时设置所有环境变量。
Env.step()方法
我将这种方法分为两部分:实施交易策略和计算效果指标。
策略
在此示例中,我没有使用更深入的技术分析讨论中可能遇到的更复杂的交易策略,而是要尝试我们在网上看到的一种常见策略的有效性:移动平均线交叉策略。如果你不熟悉,这个想法的要点是,当一个短窗口移动平均线越过一个较长的窗口移动平均线时买入;当短窗口移动平均线回到较长窗口移动平均线以下时卖出。
现在,我不确定是否有人相信这个策略真的有效(在某些情况下可能是这样),但是为了演示回溯测试环境(并且因为实现更复杂的策略将从修改您自己的想法中去除一些乐趣),让我们考虑一下。如下面的代码所示,我们有两个关键的if语句,这些语句检查移动平均线变化并决定是否记录买入或卖出(或都不记录)。需要熟悉的几个局部变量:money_in和to_buy。这些是字符串变量,用于跟踪您当前持有的资金以及购买开始时资金的去向。输入买入和卖出if语句后,这些变量将相应更新;因此,余额会根据您当前持有的价格进行更新。请注意,每次买卖都将相应的余额乘以TRADING_FEE_MULTIPLIER,在这种情况下为0.99925。该值基于使用BNB令牌(币安交易所可用的最便宜的选择)支付费用时的0.075%币安交易费用,但可以根据您特定交易所的交易费用进行更改。
性能指标
我们将使用两个指标来评估策略在回测间隔内的结果:回测间隔后的资产净值和回测间隔的平均市场变化。您的余额净值是根据您持有的货币的当前价格和您所持货币的金额将您持有的资产转换为美元来计算的。区间的平均市场变化是通过计算事件开始和结束时所有选定货币的价格平均值,然后将这些值相除得出的。基于这些指标,最终目标是以比拥有平均市场份额时要高的净资产来完成本集(显然要比开始时拥有的净资产高)。
DAYS_PER_EPISODE = 1
MINUTES_PER_EPISODE = 1440*DAYS_PER_EPISODE
NUM_EPISODES = 100
TRADING_FEE_MULTIPLIER = .99925 #this is the trading fee on binance VIP level 0 if using BNB to pay fees
class Env:
def __init__(self, ratios, df):
self.ratios = ratios
self.main_df = df
self.reset()
def reset(self):
self.balances = {'USD':1.0}
for ratio in self.ratios:
self.balances[ratio] = .
self.iloc = random.randint(,len(self.main_df)-MINUTES_PER_EPISODE-1)
self.episode_df = self.main_df[self.iloc:self.iloc+MINUTES_PER_EPISODE+2]
self.money_in = 'USD'
self.start_time = self.episode_df['time'].iloc[]
self.end_time = self.episode_df['time'].iloc[-1]
def step(self):
self.iloc+=1
#-------IMPLEMENT STRATEGY HERE--------
if self.money_in == 'USD':
for ratio in self.ratios:
#if low sma crosses above high sma
if self.episode_df[f'_'][self.iloc] > self.episode_df[f'_'][self.iloc] and self.episode_df[f'_'][self.iloc-1] > self.episode_df[f'_'][self.iloc-1]:
self.to_buy = ratio
#buy that ratio (self.to_buy)
self.balances[self.to_buy] = (self.balances['USD']/self.episode_df[f'-USD_close'][self.iloc])*TRADING_FEE_MULTIPLIER
self.balances['USD'] = .
self.buy_price = self.episode_df[f'-USD_close'][self.iloc]
memory.add_to_memory(f'Buy : ')
self.money_in = self.to_buy
break
if self.money_in != 'USD': #can't sell if money_in usd
if self.episode_df[f'_'][self.iloc]
#if high sma crosses below low sma
#sell money_in/USD
self.balances['USD'] = (self.balances[self.money_in]*self.episode_df[f'-USD_close'][self.iloc])*TRADING_FEE_MULTIPLIER
self.balances[self.money_in] = .
self.sell_price = self.episode_df[f'-USD_close'][self.iloc]
memory.add_to_memory(f'Sell : ')
self.money_in = 'USD'
#-------IMPLEMENT STRATEGY HERE--------
#-------CALCULATE PERFORMANCE METRICS HERE-------
#Running net worth
self.net_worth = self.balances['USD']
for ratio in self.ratios:
self.net_worth += self.balances[ratio]*self.episode_df[f'-USD_close'][self.iloc]
#Net_worth had you owned all ratios over episode_df --> 'average_market_change'
self.average_start_price =
self.average_end_price =
for ratio in self.ratios:
self.average_start_price += self.episode_df[f'-USD_close'].iloc[]
self.average_end_price += self.episode_df[f'-USD_close'].iloc[-1]
self.average_start_price /= len(ratios)
self.average_end_price /= len(ratios)
self.average_market_change = self.average_start_price / self.average_end_price
#-------CALCULATE PERFORMANCE METRICS HERE-------
return self.net_worth, self.average_market_change, self.start_time, self.end_time
执行环境
在这里,我们一次一分钟地遍历环境,按照策略的指示进行买卖,并跟踪性能指标以及将其输出到日志的过程。
df = pd.read_csv('12-coins-Mar18_Jun20')
env = Env(ratios, df)
memory = Memory()
net_worth_collect = []
average_market_change_collect = []
for i_episode in range(NUM_EPISODES):
for i in range(len(env.episode_df)-1):
net_worth, average_market_change, start_time, end_time = env.step()
net_worth_collect.append(net_worth)
average_market_change_collect.append(average_market_change)
#log after each episode
print(f'episode: ')
print(memory.actions)
print('\n')
print(f'interval: - ')
print(f'net worth after day(s): ')
print(f'average market change: ')
print('\n')
memory.clear()
env.reset()
#log overall
print(f'net worth average after backtest episodes: ')
#Yes, average of the average market changes
print(f'average, average market change over episodes: ')
我们做得怎么样?
好吧,正如之前预测的那样,由于策略比较简单,效果不是很好。如图所示,我们预计该策略的实施将导致在平均每天增长0.2%的市场中,平均每天净值下降4%(哎呀)。但是,除此之外日志中还显示,一个快速、动态且易于阅读的策略输出,该策略在2019年3月至2020年6月期间随机选择了100多个1天的时间间隔进行了回溯测试,并提供了每笔交易的买入/卖出信息。现在你的问题有了一些答案!
episode: 96
['Buy BTC: 5255.99', 'Sell BTC: 5227.76', 'Buy ETH: 155.86', 'Sell ETH: 155.41', 'Buy LTC: 67.46', 'Sell LTC: 67.45', 'Buy IOTA: 0.3167', 'Sell IOTA: 0.3176', 'Buy IOTA: 0.3167', 'Sell IOTA: 0.3151', 'Buy LTC: 67.67', 'Sell LTC: 67.42', 'Buy XMR: 61.38', 'Sell XMR: 61.59', 'Buy EOS: 4.5225', 'Sell EOS: 4.5076', 'Buy ZRX: 0.2743', 'Sell ZRX: 0.2723', 'Buy ETH: 155.2', 'Sell ETH: 156.98', 'Buy XLM: 0.09605', 'Sell XLM: 0.09597', 'Buy LINK: 0.439', 'Sell LINK: 0.439', 'Buy LINK: 0.4418', 'Sell LINK: 0.4672', 'Buy BTC: 5308.08', 'Sell BTC: 5304.94', 'Buy XLM: 0.09966', 'Sell XLM: 0.09965', 'Buy XMR: 62.29', 'Sell XMR: 62.22', 'Buy TRX: 0.02312', 'Sell TRX: 0.02307', 'Buy LTC: 72.82']
interval: 2019-04-29 12:25:00 - 2019-04-30 12:26:00
net worth after 1 day(s): 1.0221672847272814
average market change: 0.988959335567251
episode: 99
['Buy XRP: 0.30917', 'Sell XRP: 0.31171', 'Buy BTC: 9641.57', 'Sell BTC: 9635.6', 'Buy TRX: 0.02252', 'Sell TRX: 0.02238', 'Buy XLM: 0.08462', 'Sell XLM: 0.08457', 'Buy LINK: 2.2158', 'Sell LINK: 2.211', 'Buy EOS: 4.2811', 'Sell EOS: 4.2527', 'Buy ETH: 212.07', 'Sell ETH: 211.26', 'Buy IOTA: 0.2835', 'Sell IOTA: 0.2833', 'Buy XLM: 0.08375', 'Sell XLM: 0.08317999999999999', 'Buy LTC: 89.2', 'Sell LTC: 89.34', 'Buy XMR: 79.39', 'Sell XMR: 79.15', 'Buy BTC: 9570.01', 'Sell BTC: 9548.48', 'Buy LINK: 2.1819', 'Sell LINK: 2.1595', 'Buy LTC: 89.33', 'Sell LTC: 89.4', 'Buy EOS: 4.1803', 'Sell EOS: 4.1903', 'Buy BTC: 9540.07', 'Sell BTC: 9559.46', 'Buy ETH: 210.83', 'Sell ETH: 208.78', 'Buy LTC: 90.68', 'Sell LTC: 90.39', 'Buy LTC: 90.76', 'Sell LTC: 90.44', 'Buy XRP: 0.31008', 'Sell XRP: 0.30991', 'Buy BTC: 9504.64', 'Sell BTC: 9490.15', 'Buy ETH: 209.7', 'Sell ETH: 209.77', 'Buy IOTA: 0.2853']
interval: 2019-07-28 16:39:00 - 2019-07-29 16:40:00
net worth after 1 day(s): 0.9218801235080804
average market change: 0.9984598063740531
net worth average after 100 backtest episodes: 0.9656146271760888
average, average market change over 100 episodes: 1.0021440510159623
显示的是100集中仅有两期间的输出。为了简洁起见,我选择了一个有利可图的期间(96个)和一个不赚钱的期间(99个)来显示,而实际输出显示所有100期间的结果。不过我们真正关心的不是单个事件的结果,而是问题:“我的策略平均表现如何?”. 这将显示在最终平均事件日志中。
python日志记录包在这里对于运行许多不同策略并登录到单独文件以比较结果的人可能有用。
最终显示日交易是具有非常大的风险。我希望阅读此文的人会发现此工具有助于探索他们的交易思路,使他们能够在将其策略推向市场之前能自己对决策进行探索。对编程和算法交易知识有限的人来说,似乎缺乏资源来做出明智的交易决策;
其他一些想法
在我看来,使用我们在这个例子中的环境是一个很好的方法,可以将动态算法交易介绍给像我以前一样(也许现在还是我现在的自己)对面向对象编程知识有限的人。就我个人而言,我使用这个工具来回溯测试许多不同的策略,这些策略包含了价格数据的更多特性,还允许一些附加功能;即能够将加密货币交易(例如BTC/LTC对等),而不仅仅是加密货币对美元,反之亦然。
-------------------------------------
原文作者:Lee Schmalz
译者:链三丰
-------------------------------------