# 数字货币配对交易策略详解

Author: 小草, Created: 2024-07-05 16:23:42, Updated: 2024-07-12 09:19:11

## 数字货币配对交易策略详解

### 准备数据

#### 引入相应库

import requests
from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import requests, zipfile, io
%matplotlib inline


#### 获取所有正在交易的交易对

Info = requests.get('https://fapi.binance.com/fapi/v1/exchangeInfo')
b_symbols = [s['symbol'] for s in Info.json()['symbols'] if s['contractType'] == 'PERPETUAL' and s['status'] == 'TRADING' and s['quoteAsset'] == 'USDT']
b_symbols = list(filter(lambda x: x[-4:] == 'USDT', [s.split('_')[0] for s in b_symbols]))
b_symbols = [x[:-4] for x in b_symbols]
print(b_symbols) # 获取所有的正在交易的交易对


#### 下载K线的函数

GetKlines函数的主要功能是从币安交易所获取指定交易对永续合约的历史K线数据，并将这些数据存储在一个Pandas DataFrame中。K线数据包括开盘价、最高价、最低价、收盘价、成交量等信息。这次我们主要用到收盘价的数据。

def GetKlines(symbol='BTCUSDT',start='2020-8-10',end='2024-7-01',period='1h',base='fapi',v = 'v1'):
Klines = []
start_time = int(time.mktime(datetime.strptime(start, "%Y-%m-%d").timetuple()))*1000 + 8*60*60*1000
end_time =  min(int(time.mktime(datetime.strptime(end, "%Y-%m-%d").timetuple()))*1000 + 8*60*60*1000,time.time()*1000)
intervel_map = {'m':60*1000,'h':60*60*1000,'d':24*60*60*1000}
while start_time < end_time:
time.sleep(0.3)
mid_time = start_time+1000*int(period[:-1])*intervel_map[period[-1]]
url = 'https://'+base+'.binance.com/'+base+'/'+v+'/klines?symbol=%s&interval=%s&startTime=%s&endTime=%s&limit=1000'%(symbol,period,start_time,mid_time)
res = requests.get(url)
res_list = res.json()
if type(res_list) == list and len(res_list) > 0:
start_time = res_list[-1][0]+int(period[:-1])*intervel_map[period[-1]]
Klines += res_list
if type(res_list) == list and len(res_list) == 0:
start_time = start_time+1000*int(period[:-1])*intervel_map[period[-1]]
if mid_time >= end_time:
break
df.index = pd.to_datetime(df.time,unit='ms')
return df


#### 下载数据

start_date = '2024-04-01'
end_date   = '2024-07-05'
period = '1h'
df_dict = {}

for symbol in b_symbols:
print(symbol)
if symbol in df_dict.keys():
continue
df_s = GetKlines(symbol=symbol+'USDT',start=start_date,end=end_date,period=period)
if not df_s.empty:
df_dict[symbol] = df_s
df_close = pd.DataFrame(index=pd.date_range(start=start_date, end=end_date, freq=period),columns=df_dict.keys())
for symbol in symbols:
df_close[symbol] = df_dict[symbol].close
df_close = df_close.dropna(how='all')


### 回测引擎

class Exchange:
self.initial_balance = initial_balance #初始的资产
self.fee = fee
self.account = {'USDT':{'realised_profit':0, 'unrealised_profit':0, 'total':initial_balance,
'fee':0, 'leverage':0, 'hold':0, 'long':0, 'short':0}}
self.account[symbol] = {'amount':0, 'hold_price':0, 'value':0, 'price':0, 'realised_profit':0,'unrealised_profit':0,'fee':0}

def Trade(self, symbol, direction, price, amount):
cover_amount = 0 if direction*self.account[symbol]['amount'] >=0 else min(abs(self.account[symbol]['amount']), amount)
open_amount = amount - cover_amount
self.account['USDT']['realised_profit'] -= price*amount*self.fee #扣除手续费
self.account['USDT']['fee'] += price*amount*self.fee
self.account[symbol]['fee'] += price*amount*self.fee
if cover_amount > 0: #先平仓
self.account['USDT']['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount  #利润
self.account[symbol]['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount
self.account[symbol]['amount'] -= -direction*cover_amount
self.account[symbol]['hold_price'] = 0 if self.account[symbol]['amount'] == 0 else self.account[symbol]['hold_price']
if open_amount > 0:
total_cost = self.account[symbol]['hold_price']*direction*self.account[symbol]['amount'] + price*open_amount
total_amount = direction*self.account[symbol]['amount']+open_amount

self.account[symbol]['hold_price'] = total_cost/total_amount
self.account[symbol]['amount'] += direction*open_amount

def Sell(self, symbol, price, amount):

def Update(self, close_price): #对资产进行更新
self.account['USDT']['unrealised_profit'] = 0
self.account['USDT']['hold'] = 0
self.account['USDT']['long'] = 0
self.account['USDT']['short'] = 0
if not np.isnan(close_price[symbol]):
self.account[symbol]['unrealised_profit'] = (close_price[symbol] - self.account[symbol]['hold_price'])*self.account[symbol]['amount']
self.account[symbol]['price'] = close_price[symbol]
self.account[symbol]['value'] = self.account[symbol]['amount']*close_price[symbol]
if self.account[symbol]['amount'] > 0:
self.account['USDT']['long'] += self.account[symbol]['value']
if self.account[symbol]['amount'] < 0:
self.account['USDT']['short'] += self.account[symbol]['value']
self.account['USDT']['hold'] += abs(self.account[symbol]['value'])
self.account['USDT']['unrealised_profit'] += self.account[symbol]['unrealised_profit']
self.account['USDT']['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + self.account['USDT']['unrealised_profit'],6)
self.account['USDT']['leverage'] = round(self.account['USDT']['hold']/self.account['USDT']['total'],3)


### 相关性分析筛选币种

• 1 表示完全正相关，两个变量总是同步变化。一个变量增加时，另一个变量也按比例增加。越接近1代表相关性越强。
• -1 表示完全负相关，两个变量总是反向变化。越接近-1代表负相关性越强。
• 0 表示无线性相关，两个变量之间没有直线关系。

[ \rho_{X,Y} = \frac{\text{cov}(X,Y)}{\sigma_X \sigma_Y} ]

• ( \rho_{X,Y} ) 是变量 ( X ) 和 ( Y ) 的皮尔森相关系数。
• ( \text{cov}(X,Y) ) 是 ( X ) 和 ( Y ) 的协方差。
• ( \sigma_X ) 和 ( \sigma_Y ) 分别是 ( X ) 和 ( Y ) 的标准差。

import seaborn as sns
corr = df_close.corr()
plt.figure(figsize=(20, 20))
sns.heatmap(corr, annot=False, cmap='coolwarm', vmin=-1, vmax=1)
plt.title('Correlation Heatmap of Cryptocurrency Closing Prices', fontsize=20);


MANA     SAND     0.996562
ICX      ZIL      0.996000
STORJ    FLOW     0.994193
FLOW     SXP      0.993861
STORJ    SXP      0.993822
IOTA     ZIL      0.993204
SAND     0.993095
KAVA     SAND     0.992303
ZIL      SXP      0.992285
SAND     0.992103
DYDX     ZIL      0.992053
DENT     REEF     0.991789
RDNT     MANTA    0.991690
STMX     STORJ    0.991222
BIGTIME  ACE      0.990987
RDNT     HOOK     0.990718
IOST     GAS      0.990643
ZIL      HOOK     0.990576
MATIC    FLOW     0.990564
MANTA    HOOK     0.990563


corr_pairs = corr.unstack()

# 移除自身相关性（即对角线上的值）
corr_pairs = corr_pairs[corr_pairs != 1]

sorted_corr_pairs = corr_pairs.sort_values(kind="quicksort")

# 提取最相关和最不相关的前20个币种对
most_correlated = sorted_corr_pairs.tail(40)[::-2]

print("最相关的前20个币种对：")
print(most_correlated)



### 回测验证

1. 初始化

• 定义交易对（pair_a = ‘IOTA’, pair_b = ‘ZIL’）。
• 创建一个交易所对象 e，初始余额为10000美元，交易费用为0.02%。
• 计算初始平均价格比率 avg
• 设定一个初始交易价值 value = 1000
2. 迭代处理价格数据

• 遍历每一个时间点的价格数据 df_close
• 计算当前价格比率相对于平均值的偏差 diff
• 根据偏差计算目标交易价值 aim_value，每偏离0.01，交易一个value。并基于当前账户持仓和价格情况决定买卖操作。
• 如果偏差过大，执行卖出 pair_a 和买入 pair_b 操作。
• 如果偏差过小，执行买入 pair_a 和卖出 pair_b 操作。
3. 调整平均值

• 更新平均价格比率 avg，以便反映最新的价格比率。
4. 更新账户和记录

• 更新交易所账户的持仓和余额信息。
• 记录每一步的账户状态（总资产、持有资产、交易费用、多头和空头头寸）到 res_list
5. 结果输出

• res_list 转换为dataframe res，以便进一步分析和展示。
pair_a = 'IOTA'
pair_b = "ZIL"
e = Exchange([pair_a,pair_b], fee=0.0002, initial_balance=10000) #Exchange定义放在评论区
res_list = []
index_list = []
avg = df_close[pair_a][0] / df_close[pair_b][0]
value = 1000
for idx, row in df_close.iterrows():
diff = (row[pair_a] / row[pair_b] - avg)/avg
aim_value = -value * diff / 0.01
if -aim_value + e.account[pair_a]['amount']*row[pair_a] > 0.5*value:
e.Sell(pair_a,row[pair_a],(-aim_value + e.account[pair_a]['amount']*row[pair_a])/row[pair_a])
if -aim_value + e.account[pair_a]['amount']*row[pair_a]  < -0.5*value:
e.Sell(pair_b, row[pair_b],(aim_value + e.account[pair_b]['amount']*row[pair_b])/row[pair_b])
avg = 0.99*avg + 0.01*row[pair_a] / row[pair_b]
index_list.append(idx)
e.Update(row)
res_list.append([e.account['USDT']['total'],e.account['USDT']['hold'],
e.account['USDT']['fee'],e.account['USDT']['long'],e.account['USDT']['short']])
res = pd.DataFrame(data=res_list, columns=['total','hold', 'fee', 'long', 'short'],index = index_list)
res['total'].plot(grid=True);


### 结论

More

77924998 这个值得研究，码源呢？