Les réseaux neuronaux et la série de transactions quantifiées de devises numériques ((2)) L'apprentissage en profondeur renforcé pour former les stratégies de trading Bitcoin

Auteur:Le foin, Créé: 2019-07-31 11:13:15, Mis à jour: 2023-10-20 20:10:18

img

1.介绍

L'article précédent explique comment utiliser le réseau LSTM pour prédire le prix du Bitcoinhttps://www.fmz.com/digest-topic/4035Comme indiqué dans l'article, ce n'est qu'un petit projet pour les pratiquants, pour se familiariser avec RNN et pytorch. Cet article explique comment utiliser l'apprentissage renforcé pour former directement les stratégies de transaction. Le modèle d'apprentissage renforcé est un PPO open-source OpenAI, et l'environnement fait référence au style de gym. Le PPO, connu sous le nom d'optimisation de la politique proximale, est une amélioration de l'optimisation de la politique Graident, c'est-à-dire de l'échelle de stratégie. Le gym, également publié par OpenAI, peut interagir avec le réseau de stratégies, donner des commentaires sur l'état et les récompenses de l'environnement actuel, comme l'exercice de l'apprentissage renforcé. Lire cet article nécessite une certaine base d'apprentissage intensif en Python, pytorch et DRL. Mais cela n'a pas d'importance.www.fmz.comNous sommes heureux de vous inviter à nous rejoindre sur le groupe QQ: 863946592.

2.数据和学习参考资料

Les données sur le prix du Bitcoin proviennent de la plateforme de trading quantifié par les inventeurs de FMZ:https://www.quantinfo.com/Tools/View/4.htmlUn article sur le DRL+gym pour entraîner les stratégies de trading:https://towardsdatascience.com/visualizing-stock-trading-agents-using-matplotlib-and-gym-584c992bc6d4Voici quelques exemples de pytorch à l'entrée:https://github.com/yunjey/pytorch-tutorialCet article utilise directement une courte mise en œuvre du modèle LSTM-PPO:https://github.com/seungeunrho/minimalRL/blob/master/ppo-lstm.pyL'article sur le PPO:https://zhuanlan.zhihu.com/p/38185553Plus d'articles sur le DRL:https://www.zhihu.com/people/flood-sung/postsEn ce qui concerne le gymnase, cet article n'a pas besoin d'être installé, mais l'apprentissage par renforcement est très courant:https://gym.openai.com/

3.LSTM-PPO

Une explication approfondie sur le PPO peut être apprise à partir des références ci-dessus, qui ne sont qu'une simple introduction à la notion. Dans la dernière phase, le réseau LSTM n'a fait que prédire un prix, et comment réaliser une transaction d'achat et de vente en fonction de ce prix prédit, il est naturel de penser que l'action d'achat et de vente n'est-elle pas plus directe?

Le code source de LSTM-PPO est donné ci-dessous et peut être compris en combinant les informations ci-dessus:


import time
import requests
import json
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical
from itertools import count

#模型的超参数
learning_rate = 0.0005
gamma         = 0.98
lmbda         = 0.95
eps_clip      = 0.1
K_epoch       = 3

device = torch.device('cpu') # 也可以改为GPU版本

class PPO(nn.Module):
    def __init__(self, state_size, action_size):
        super(PPO, self).__init__()
        self.data = []
        self.fc1   = nn.Linear(state_size,10)
        self.lstm  = nn.LSTM(10,10)
        self.fc_pi = nn.Linear(10,action_size)
        self.fc_v  = nn.Linear(10,1)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

    def pi(self, x, hidden):
        #输出各个动作的概率,由于是LSTM网络还要包含hidden层的信息,可以参考上一期文章
        x = F.relu(self.fc1(x))
        x = x.view(-1, 1, 10)
        x, lstm_hidden = self.lstm(x, hidden)
        x = self.fc_pi(x)
        prob = F.softmax(x, dim=2)
        return prob, lstm_hidden
    
    def v(self, x, hidden):
        #价值函数,用于评价当前局面的好坏,所以只有一个输出
        x = F.relu(self.fc1(x))
        x = x.view(-1, 1, 10)
        x, lstm_hidden = self.lstm(x, hidden)
        v = self.fc_v(x)
        return v
      
    def put_data(self, transition):
        self.data.append(transition)
        
    def make_batch(self):
        #准备训练数据
        s_lst, a_lst, r_lst, s_prime_lst, prob_a_lst, hidden_lst, done_lst = [], [], [], [], [], [], []
        for transition in self.data:
            s, a, r, s_prime, prob_a, hidden, done = transition
            s_lst.append(s)
            a_lst.append([a])
            r_lst.append([r])
            s_prime_lst.append(s_prime)
            prob_a_lst.append([prob_a])
            hidden_lst.append(hidden)
            done_mask = 0 if done else 1
            done_lst.append([done_mask])
            
        s,a,r,s_prime,done_mask,prob_a = torch.tensor(s_lst, dtype=torch.float), torch.tensor(a_lst), \
                                         torch.tensor(r_lst), torch.tensor(s_prime_lst, dtype=torch.float), \
                                         torch.tensor(done_lst, dtype=torch.float), torch.tensor(prob_a_lst)
        self.data = []
        return s,a,r,s_prime, done_mask, prob_a, hidden_lst[0]
        
    def train_net(self):
        s,a,r,s_prime,done_mask, prob_a, (h1,h2) = self.make_batch()
        first_hidden = (h1.detach(), h2.detach())

        for i in range(K_epoch):
            v_prime = self.v(s_prime, first_hidden).squeeze(1)
            td_target = r + gamma * v_prime * done_mask
            v_s = self.v(s, first_hidden).squeeze(1)
            delta = td_target - v_s
            delta = delta.detach().numpy()
            
            advantage_lst = []
            advantage = 0.0
            for item in delta[::-1]:
                advantage = gamma * lmbda * advantage + item[0]
                advantage_lst.append([advantage])
            advantage_lst.reverse()
            advantage = torch.tensor(advantage_lst, dtype=torch.float)

            pi, _ = self.pi(s, first_hidden)
            pi_a = pi.squeeze(1).gather(1,a)
            ratio = torch.exp(torch.log(pi_a) - torch.log(prob_a))  # a/b == log(exp(a)-exp(b))

            surr1 = ratio * advantage
            surr2 = torch.clamp(ratio, 1-eps_clip, 1+eps_clip) * advantage
            loss = -torch.min(surr1, surr2) + F.smooth_l1_loss(v_s, td_target.detach()) #同时训练了价值网络和决策网络

            self.optimizer.zero_grad()
            loss.mean().backward(retain_graph=True)
            self.optimizer.step()

4.比特币回测环境

Simulant le format du gym, il y a une méthode d'initialisation de réinitialisation, step input action, qui renvoie le résultat comme ((l'état suivant, l'action de gain, ou la fin, des informations supplémentaires), l'ensemble de l'environnement de réinitialisation est également de 60 lignes, qui peut être modifiée par vous-même pour une version plus complexe, le code spécifique:

class BitcoinTradingEnv:
    def __init__(self, df, commission=0.00075,  initial_balance=10000, initial_stocks=1, all_data = False, sample_length= 500):
        self.initial_stocks = initial_stocks #初始的比特币数量
        self.initial_balance = initial_balance #初始的资产
        self.current_time = 0 #回测的时间位置
        self.commission = commission #易手续费
        self.done = False #回测是否结束
        self.df = df
        self.norm_df = 100*(self.df/self.df.shift(1)-1).fillna(0) #标准化方法,简单的收益率标准化
        self.mode = all_data # 是否为抽样回测模式
        self.sample_length = 500 # 抽样长度
        
    def reset(self):
        self.balance = self.initial_balance
        self.stocks = self.initial_stocks
        self.last_profit = 0
        
        if self.mode:
            self.start = 0
            self.end = self.df.shape[0]-1
        else:
            self.start = np.random.randint(0,self.df.shape[0]-self.sample_length)
            self.end = self.start + self.sample_length
            
        self.initial_value = self.initial_balance + self.initial_stocks*self.df.iloc[self.start,4]
        self.stocks_value = self.initial_stocks*self.df.iloc[self.start,4]
        self.stocks_pct = self.stocks_value/self.initial_value
        self.value = self.initial_value
        
        self.current_time = self.start
        return np.concatenate([self.norm_df[['o','h','l','c','v']].iloc[self.start].values , [self.balance/10000, self.stocks/1]])
    
    def step(self, action):
        #action即策略采取的动作,这里将更新账户和计算reward
        done = False
        if action == 0: #持有
            pass
        elif action == 1: #买入
            buy_value = self.balance*0.5
            if buy_value > 1: #余钱不足,不操作账户
                self.balance -= buy_value
                self.stocks += (1-self.commission)*buy_value/self.df.iloc[self.current_time,4]
        elif action == 2: #卖出
            sell_amount = self.stocks*0.5
            if sell_amount > 0.0001:
                self.stocks -= sell_amount
                self.balance += (1-self.commission)*sell_amount*self.df.iloc[self.current_time,4]
                
        self.current_time += 1
        if self.current_time == self.end:
            done = True
        self.value = self.balance + self.stocks*self.df.iloc[self.current_time,4]
        self.stocks_value = self.stocks*self.df.iloc[self.current_time,4]
        self.stocks_pct = self.stocks_value/self.value
        if self.value < 0.1*self.initial_value:
            done = True
            
        profit = self.value - (self.initial_balance+self.initial_stocks*self.df.iloc[self.current_time,4])
        reward = profit - self.last_profit # 每回合的reward是新增收益
        self.last_profit = profit
        next_state = np.concatenate([self.norm_df[['o','h','l','c','v']].iloc[self.current_time].values , [self.balance/10000, self.stocks/1]])
        return (next_state, reward, done, profit)

5.几个值得注意的细节

Pourquoi les comptes initiaux ont-ils des pièces?

La formule pour calculer les gains de l'environnement de retouche est la suivante: les gains actuels = la valeur actuelle du compte - la valeur actuelle du compte initial. Cela signifie que si le prix du bitcoin baisse et que la stratégie effectue une opération de vente de monnaie, même si la valeur totale du compte diminue, elle devrait en fait être récompensée par la stratégie. Si le temps de retouche est long, le compte initial peut avoir peu d'impact, mais un impact important au début.

Pourquoi les gens font des essais pendant l'entraînement?

Le volume total de données est supérieur à 10 000 lignes K. Si le volume complet est exécuté en une seule boucle à chaque fois, il faut beaucoup de temps et la stratégie est identique à chaque fois. Il peut être plus facile de s'adapter.

Comment faire sans monnaie ou sans argent?

Dans l'environnement de retouche, cette situation n'est pas prise en compte, si les pièces ont été vendues ou n'atteignent pas le volume minimum, alors exécuter une opération de vente est en fait équivalent à ne pas exécuter d'opération, si le prix baisse, selon le calcul du bénéfice relatif, la stratégie est toujours basée sur une récompense positive. L'effet de cette situation est que lorsque la stratégie juge que le marché est en baisse et que le solde de compte ne peut pas être vendu, il n'est pas possible de distinguer l'action de vente et l'action de non-opération, mais cela n'a aucune influence sur le jugement de la stratégie elle-même sur le marché.

Pourquoi est-il nécessaire de réinitialiser les informations des comptes?

Le modèle PPO possède un réseau de valeurs pour évaluer la valeur de l'état actuel. Il est évident que si la stratégie décide que le prix va monter, l'état entier n'a une valeur positive que lorsque le compte actuel détient le bitcoin, et vice versa.

Dans quelles circonstances reviendrait-il sans fonctionner?

Lorsque les bénéfices de l'achat et de la vente ne sont pas couverts par les frais de traitement, il est préférable de revenir sans action. Bien que la description précédente utilise à plusieurs reprises la stratégie pour déterminer la tendance des prix, pour plus de facilité, le modèle PPO ne fait pas de prévision du marché, mais fournit seulement la probabilité de trois mouvements.

6.数据的获取和训练

Comme dans l'article précédent, la manière et le format de l'obtention des données sont les suivants:

resp = requests.get('https://www.quantinfo.com/API/m/chart/history?symbol=BTC_USD_BITFINEX&resolution=60&from=1525622626&to=1561607596')
data = resp.json()
df = pd.DataFrame(data,columns = ['t','o','h','l','c','v'])
df.index = df['t']
df = df.dropna()
df = df.astype(np.float32)

Comme j'utilisais un réseau LSTM et que la formation prenait beaucoup de temps, j'ai changé de version du GPU, qui est environ 3 fois plus rapide.

env = BitcoinTradingEnv(df)
model = PPO()

total_profit = 0 #记录总收益
profit_list = [] #记录每次训练收益
for n_epi in range(10000):
    hidden = (torch.zeros([1, 1, 32], dtype=torch.float).to(device), torch.zeros([1, 1, 32], dtype=torch.float).to(device))
    s = env.reset()
    done = False
    buy_action = 0
    sell_action = 0
    while not done:
        h_input = hidden
        prob, hidden = model.pi(torch.from_numpy(s).float().to(device), h_input)
        prob = prob.view(-1)
        m = Categorical(prob)
        a = m.sample().item()
        if a==1:
            buy_action += 1
        if a==2:
            sell_action += 1
        s_prime, r, done, profit = env.step(a)

        model.put_data((s, a, r/10.0, s_prime, prob[a].item(), h_input, done))
        s = s_prime

    model.train_net()
    profit_list.append(profit)
    total_profit += profit
    if n_epi%10==0:
        print("# of episode :{:<5}, profit : {:<8.1f}, buy :{:<3}, sell :{:<3}, total profit: {:<20.1f}".format(n_epi, profit, buy_action, sell_action, total_profit))

7.训练结果和分析

Après une longue attente:img

En général, la première moitié de l'année a été marquée par une longue baisse, tandis que la seconde moitié a été marquée par une forte reprise.img

Il y a beaucoup d'opérations d'achat avant l'entraînement et pratiquement pas de tour de profits. À la mi-entraînement, les opérations d'achat diminuent progressivement, la probabilité de profit est de plus en plus grande, mais il y a de fortes chances de perte.img

Les résultats sont les suivants:img

La stratégie s'est rapidement débarrassée de la situation où les gains antérieurs étaient négatifs, mais les fluctuations étaient importantes, et les gains n'ont augmenté rapidement qu'après 10 000 tours, ce qui est généralement difficile à former.

Une fois la formation terminée, le modèle passe à l'exécution de toutes les données pour voir comment elles se sont déroulées, en enregistrant la valeur marchande totale des comptes, le nombre de bitcoins détenus, le pourcentage de valeur des bitcoins, les bénéfices totaux. La première est la valeur marchande totale, les bénéfices totaux et autres sont exclus:img
La valeur marchande totale a augmenté lentement lors des premières marchés baissiers, et a suivi la hausse lors des derniers marchés haussiers, mais il y a eu des pertes progressives.

Enfin, regardez le taux de détention, l'axe gauche du graphique est le taux de détention, l'axe droit est le marché, vous pouvez juger de manière préliminaire que le modèle est surfait, la fréquence de détention est faible lors de la première période de marché baissier, la fréquence de détention est élevée au bas du marché.img

8.测试数据分析

Le graphique montre que le prix a chuté de 13 000 $ au début à plus de 9 000 $ aujourd'hui, ce qui est un grand test pour le modèle.img

Le premier est le résultat final, un gain relatif, une performance médiocre, mais pas de perte.img

Si l'on regarde les positions, on peut supposer que le modèle a tendance à revenir en arrière après une chute rapide, comme acheter ou vendre, la volatilité du marché Bitcoin a été faible ces derniers temps, et le modèle a toujours été dans un état de stock vide.img

9.总结

L'article utilise la méthode d'apprentissage renforcé en profondeur (PPO) pour former un robot de trading automatique de bitcoins et en tire quelques conclusions. Le temps étant limité, le modèle a encore des points à améliorer, bienvenue à la discussion. La plus grande leçon est que la méthode de normalisation des données est bonne, et non pas de mise à l'échelle, sinon le modèle se souvient rapidement des relations entre les prix et le marché, et tombe dans le piège.

Les articles précédents présentent: Les inventeurs de FMZ ont partagé certaines stratégies publiques sur leur plateforme de quantification:https://zhuanlan.zhihu.com/p/64961672Le cours de transaction quantitative de la monnaie numérique de NetEase Cloud Classroom, qui coûte seulement 20 dollars:https://study.163.com/course/courseMain.htm?courseId=1006074239&share=2&shareId=400000000602076Une stratégie de haute fréquence que j'ai dévoilée et qui a été très rentable:https://www.fmz.com/bbs-topic/1211


Relationnée

Plus de

Je vous en prie.Pourquoi voulez-vous inverser les images des résultats? Pourquoi votre revenu diminue-t-il toujours quand le dollar augmente?

détectiveprofit = self.value - (self.initial_balance + self.initial_stocks * self.df.iloc[self.current_time,4]) Il y a un bug dans le résultat de l'analyse Il devrait être: profit = self.value - (self.initial_balance + self.initial_stocks * self.df.iloc[self.start,4])

détectiveprofit = self.value - (self.initial_balance+self.initial_stocks*self.df.iloc[self.current_time,4]) Il y a un bug dans le résultat de l'analyse Il devrait être: profit = self.value - (self.initial_balance+self.initial_stocks*self.df.iloc[self.start,4])

Il a été arrêté.C'est plus fort que la première édition.

Le projet de loiIl est trop fort!

Je suis désolée.Les vaches du dieu de l'herbe!

Le foinLe GPU Je ne sais pas. Le code de l'appareil est le code de l'ordinateur. classe PPO ((nn.Module): Définition __init__(self): super (PPO, self).__init (en anglais seulement) Je ne sais pas si c'est vrai. Je ne sais pas. Ligneur (8,64) LSTM est un mot anglais qui désigne la langue officielle de l'Allemagne. Ligneur ((32,3) Ligneur ((32,1) L'optimisation de l'apprentissage par l'utilisateur est basée sur l'optimisation de l'apprentissage par l'utilisateur. Déf pi (self, x, hidden): x = F.relu ((self.fc1 ((x)) x est égal à x.view ((-1, 1, 64) x, lstm_hidden = self. lstm ((x, caché)) x = self. fc_pi (x) Prob = F.softmax (x, dim = 2) Je ne sais pas comment faire. Je ne sais pas. def v ((self, x, hidden): x = F.relu ((self.fc1 ((x)) x est égal à x.view ((-1, 1, 64) x, lstm_hidden = self. lstm (x, caché) v = self. fc_v ((x) retourner v Je ne sais pas. Déf put_data ((self, transition): Les données de l'appareil sont enregistrées dans les données de l'appareil. Je ne sais pas. Déf faire_batch ((self): s_lst, a_lst, r_lst, s_prime_lst, prob_a_lst, hidden_lst, done_lst = [], [], [], [], [], [], [] Pour transition in self.data: s, a, r, s_prime, prob_a, caché, fait = transition Je ne sais pas. s_lst.append (s) a_lst.append (([a]) R_lst.append (([r]) s_prime_lst.append (en anglais seulement) Prob_a_lst.append (en anglais seulement) Je ne sais pas comment faire. done_mask = 0 si fait autre 1 Je ne sais pas comment faire. Je ne sais pas. s,a,r,s_prime,done_mask,prob_a = torch.tensor ((s_lst,dtype=torch.float).to ((device), torch.tensor ((a_lst).to ((device).to ((device), \ torch.tensor ((r_lst).to ((device), torch.tensor ((s_prime_lst, dtype=torch.float).to ((device), \ torch.tensor ((done_lst, dtype=torch.float).to ((appareil), torch.tensor ((prob_a_lst).to ((appareil) est un appareil de type torch.tensor (done_lst, dtype=torch.float). Je ne sais pas si c'est vrai. retour s, a, r, s_prime, done_mask, prob_a, hidden_lst[0] Je ne sais pas. Déf train_net ((self): s, a, r, s_prime, done_mask, prob_a, (h1, h2) = self.make_batch ()) first_hidden = (h1.to ((device).detach ((), h2.to ((device).detach (()) Pour i in range ((K_epoch): v_prime = self.v ((s_prime, first_hidden).squeeze))) td_target = r + gamma * v_prime * done_mask v_s = self.v ((s, first_hidden).squeeze ((1)) delta = td_target - v_s est égal à delta = delta.cpu (().detach (().numpy (()) L'avantage est le fait que vous ne pouvez pas utiliser le système. avantage = 0.0 pour item in delta [:: - 1]: avantage = gamma * lmbda * avantage + article [0] L'avantage est que vous pouvez utiliser les informations fournies par les utilisateurs. avantage_lst.reverse (en anglais) avantage = torch.tensor ((advantage_lst, dtype=torch.float).to ((dispositif) pi, _ = self. pi ((s, first_hidden) est le nombre d'éléments cachés. pi_a = pi.squeeze ((1).gather ((1, a) est le nombre de points de base de pi. Ratio = torch.exp ((torch.log ((pi_a) - torch.log ((prob_a)) # a/b == log ((exp ((a) -exp ((b)) surr1 = ratio * avantage surr2 = torch.clamp ((ratio, 1-eps_clip, 1+eps_clip) * avantage Loss = -torch.min ((surr1, surr2) + F.smooth_l1_loss ((v_s, td_target.detach)) Je ne suis pas d'accord avec vous. Le graphe de l'écran est en train de se décomposer. Je ne suis pas d'accord avec toi. Je ne sais pas.