Die Entwicklung eines Bitcoin-Trading-Robot, der kein Geld verliert

Schriftsteller:Gutes, Erstellt: 2019-06-27 10:58:40, Aktualisiert: 2023-10-30 20:30:00

img

Lassen Sie uns mit Hilfe von Reinforcement Learning aus der KI einen digitalen Währungstransaktionsroboter bauen.

In diesem Tutorial werden wir eine Reinforcement-Learning-Lynx erstellen und anwenden, um zu lernen, wie man einen Bitcoin-Trading-Roboter erstellt. In diesem Tutorial werden wir OpenAI-Gym und PPO-Roboter aus der Stable-baselines-Bibliothek verwenden, eine Ableger der OpenAI-Basis.

Vielen Dank an OpenAI und DeepMind für die Open-Source-Software, die sie in den vergangenen Jahren für Deep-Learning-Forscher bereitgestellt haben.

img

AlphaStar-Ausbildunghttps://deepmind.com/blog/alphastar-mastering-real-time-strategy-game-starcraft-ii/

Obwohl wir nichts beeindruckendes erschaffen werden, ist es immer noch nicht einfach, Bitcoin-Roboter in den täglichen Transaktionen zu integrieren.

Was man so einfach bekommt, hat keinen Wert.

Deshalb müssen wir nicht nur lernen, selbst zu handeln... sondern auch, dass Roboter für uns handeln.

Planung

img

1.为我们的机器人创建gym环境以供其进行机器学习

2.渲染一个简单而优雅的可视化环境

3.训练我们的机器人,使其学习一个可获利的交易策略

Wenn Sie noch nicht vertraut sind, wie man eine Fitnessstudio-Umgebung von Grund auf erstellt, oder wie man diese einfach visualisiert.

Eintritt

在本教程中,我们将使用Zielak生成的Kaggle数据集。如果您想下载源代码,我的Github仓库中会提供,同时也有.csv数据文件。好的,让我们开始吧。

Zuerst müssen wir alle notwendigen Bibliotheken importieren. Stellen Sie sicher, dass Sie alle fehlenden Bibliotheken mit Pip installieren.

import gym
import pandas as pd
import numpy as np
from gym import spaces
from sklearn import preprocessing

Als nächstes erstellen wir unsere Klasse für die Umgebung. Wir müssen eine Pandas-Datenparameter, sowie eine optionale initial_balance und eine lookback_window_size einreichen, die die Anzahl der vergangenen Zeitschritte anzeigen, die der Roboter in jedem Schritt beobachtet hat. Wir setzen die Provision für jede Transaktion als Default 0.075%, also den aktuellen Bitmex-Wechselkurs, und setzen die Stereo-Parameter als Default false, was bedeutet, dass unsere Datenparameter standardmäßig in zufälligen Abschnitten verlaufen.

Wir rufen die Daten auch dropna (()) und reset_index (()) an und entfernen zuerst die Zeilen mit den NaN-Werten und setzen dann den Index mit den Parametern zurück, da wir die Daten entfernt haben.

class BitcoinTradingEnv(gym.Env):
  """A Bitcoin trading environment for OpenAI gym"""
  metadata = {'render.modes': ['live', 'file', 'none']}
  scaler = preprocessing.MinMaxScaler()
  viewer = None
def __init__(self, df, lookback_window_size=50, 
                         commission=0.00075,  
                         initial_balance=10000
                         serial=False):
    super(BitcoinTradingEnv, self).__init__()
self.df = df.dropna().reset_index()
    self.lookback_window_size = lookback_window_size
    self.initial_balance = initial_balance
    self.commission = commission
    self.serial = serial
# Actions of the format Buy 1/10, Sell 3/10, Hold, etc.
    self.action_space = spaces.MultiDiscrete([3, 10])
# Observes the OHCLV values, net worth, and trade history
    self.observation_space = spaces.Box(low=0, high=1, shape=(10, lookback_window_size + 1), dtype=np.float16)

Unser Action_space ist hier als eine Gruppe von 3 Optionen (Kaufen, Verkaufen oder Halten) und eine andere Gruppe von 10 Beträgen (1/10, 2/10, 3/10 usw.) dargestellt.

Unser Observation_space ist definiert als ein kontinuierliches Flussesatz zwischen 0 und 1, dessen Form ((10, lookback_window_size + 1)); + 1 verwendet wird, um die aktuelle Zeitspanne zu berechnen. Für jede Zeitspanne im Fenster werden wir den OHCLV-Wert beobachten. Unser Nettowert entspricht der Menge an BTC, die wir gekauft oder verkauft haben, und der Gesamtbetrag der Dollar, die wir in diesen BTCs ausgegeben oder erhalten haben.

Als nächstes müssen wir eine Reset-Methode schreiben, um die Umgebung zu initialieren.

def reset(self):
  self.balance = self.initial_balance
  self.net_worth = self.initial_balance
  self.btc_held = 0
self._reset_session()
self.account_history = np.repeat([
    [self.net_worth],
    [0],
    [0],
    [0],
    [0]
  ], self.lookback_window_size + 1, axis=1)
self.trades = []
return self._next_observation()

Hier verwenden wir self._reset_session und self._next_observation, die wir noch nicht definiert haben.

Verhandlungsgespräche

img

我们环境的一个重要部分是交易会话的概念。如果我们将这个机器人部署到市场外,我们可能永远不会一次运行它超过几个月。出于这个原因,我们将限制self.df中连续帧数的数量,也就是我们的机器人连续一次能看到的帧数。

In unserer _reset_session-Methode setzen wir zunächst den current_step auf 0 zurück. Als nächstes setzen wir die steps_left auf eine zufällige Zahl zwischen 1 und MAX_TRADING_SESSION, die wir oben definieren.

MAX_TRADING_SESSION = 100000 # ~2个月

Als nächstes müssen wir, wenn wir die Stückzahlen kontinuierlich durchlaufen wollen, die gesamte Stückzahlen durchlaufen, sonst setzen wir frame_start auf einen zufälligen Punkt in self.df und erstellen eine neue Datenstückzahl namens active_df, die nur ein Stückchen von self.df ist und von frame_start zu frame_start + steps_left abgeleitet wird.

def _reset_session(self):
  self.current_step = 0
if self.serial:
    self.steps_left = len(self.df) - self.lookback_window_size - 1
    self.frame_start = self.lookback_window_size
  else:
    self.steps_left = np.random.randint(1, MAX_TRADING_SESSION)
    self.frame_start = np.random.randint(self.lookback_window_size, len(self.df) - self.steps_left)
self.active_df = self.df[self.frame_start - self.lookback_window_size:self.frame_start + self.steps_left]

Eine wichtige Nebenwirkung des Durchschnitts von Datenstücken in Zufallsstücken ist, dass unsere Roboter mehr einzigartige Daten haben werden, die sie für längere Trainings verwenden können. Zum Beispiel, wenn wir nur in einer seriellen Art und Weise durch die Datenstückzahlen (d. h. in der Reihenfolge von 0 bis len (df)) durchqueren, dann haben wir nur so viele einzigartige Datenpunkte wie in der Datenstückzahlen. Unser Beobachtungsraum kann sogar nur eine Anzahl von verschiedenen Zuständen in jedem Zeitschritt annehmen.

Aber durch die zufällige Durchforstung der Datensätze können wir für jede Zeitspanne des ursprünglichen Datensatzes eine Sammlung von bedeutungsvollen Handelsergebnissen erstellen, also eine Kombination aus Handelsverhalten und vorher gesehenem Preisverhalten, um mehr einzigartige Datensätze zu erzeugen.

Bei einer Zeitspanne von 10 nach der Wiederherstellung der Serienumgebung wird unser Roboter immer gleichzeitig im Datensatz laufen, und nach jeder Zeitspanne haben wir drei Optionen: Kaufen, Verkaufen oder Halten. Für jede dieser drei Optionen ist eine weitere Wahl erforderlich: 10%, 20%,... oder 100% der spezifischen Praxis. Dies bedeutet, dass unser Roboter möglicherweise in einem der 10 Zustände von 103 auftreten kann, insgesamt 1030 Situationen.

Zurück in unsere Random-Slice-Umgebung. Wenn die Zeitspanne 10 ist, befindet sich unser Roboter möglicherweise in jeder len (df) Zeitspanne innerhalb der Datenmenge. Angenommen, nach jeder Zeitspanne die gleiche Auswahl zu treffen, bedeutet, dass der Roboter in der gleichen Zeitspanne 10 einen einzigartigen Zustand in 30 Sekunden von jeder len (df) erleben kann.

Obwohl dies für große Datensätze ziemlich laut sein könnte, glaube ich, dass es den Robotern erlaubt werden sollte, mehr von unseren begrenzten Datenmengen zu lernen. Wir durchforsten unsere Testdaten immer noch in einer seriellen Art und Weise, um aktuelle, scheinbar frische Daten in Echtzeit zu erhalten, in der Hoffnung, ein genaueres Verständnis durch die Wirksamkeit der Algorithmen zu erhalten.

Das ist ein sehr schwieriges Problem.

Durch die Beobachtung einer effektiven visuellen Umgebung ist es oft hilfreich, die Art der Funktionen zu verstehen, die unser Roboter verwenden wird. Hier ist zum Beispiel eine Visualisierung des beobachtbaren Raumes mit OpenCV-Rendering.

img

Ein Blick auf die OpenCV-Visualisierung

Jede Zeile im Bild stellt eine Zeile in unserem Observation_space dar. Die ersten vier Zeilen mit ähnlicher Frequenz repräsentieren die rote Zeile für OHCL-Daten, die orange und gelben Punkte darunter für Transaktionen. Die schwankenden blauen Streifen darunter sind die Nettowerte der Roboter, und die leichteren Streifen darunter repräsentieren die Transaktionen der Roboter.

Wenn Sie genau hinschauen, können Sie sogar selbst eine Abbildung erstellen. Unterhalb der Transaktionsmassebar befindet sich eine Morse-ähnliche Oberfläche, die die Transaktionsgeschichte anzeigt. Es sieht so aus, als würde unser Roboter ausreichend aus den Daten in unserem Observation_space lernen können, also lasst uns weitermachen.

  • Es ist wichtig, nur die bisher beobachteten Daten zu erweitern, um eine Überschreitung der Abweichungen zu vermeiden.
def _next_observation(self):
  end = self.current_step + self.lookback_window_size + 1
obs = np.array([
    self.active_df['Open'].values[self.current_step:end],  
    self.active_df['High'].values[self.current_step:end],
    self.active_df['Low'].values[self.current_step:end],
    self.active_df['Close'].values[self.current_step:end],
    self.active_df['Volume_(BTC)'].values[self.current_step:end],])
scaled_history = self.scaler.fit_transform(self.account_history)
obs = np.append(obs, scaled_history[:, -(self.lookback_window_size + 1):], axis=0)
return obs

Handeln

Wir haben unsere Beobachtungsfläche aufgebaut, jetzt ist es Zeit, unsere Ladderfunktionen zu schreiben und dann die von dem Roboter vorgeschriebenen Aktionen auszuführen. Jedes Mal, wenn wir selbst.steps_left == 0 in der aktuellen Handelszeit haben, werden wir unsere BTC verkaufen und_reset_session aufrufen.Andernfalls setzen wir die Belohnung auf den aktuellen Nettowert und setzen sie auf True, wenn unser Geld aus ist.

def step(self, action):
  current_price = self._get_current_price() + 0.01
  self._take_action(action, current_price)
  self.steps_left -= 1
  self.current_step += 1
if self.steps_left == 0:
    self.balance += self.btc_held * current_price
    self.btc_held = 0
    self._reset_session()
obs = self._next_observation()
  reward = self.net_worth
  done = self.net_worth <= 0
return obs, reward, done, {}

Eine Transaktionsaktion ist so einfach wie die Erfassung des aktuellen Preises, die Bestimmung der Aktionen, die ausgeführt werden müssen, sowie die Anzahl der Käufe oder Verkäufe.

def _take_action(self, action, current_price):
  action_type = action[0]
  amount = action[1] / 10
btc_bought = 0
  btc_sold = 0
  cost = 0
  sales = 0
if action_type < 1:
    btc_bought = self.balance / current_price * amount
    cost = btc_bought * current_price * (1 + self.commission)
    self.btc_held += btc_bought
    self.balance -= cost
elif action_type < 2:
    btc_sold = self.btc_held * amount
    sales = btc_sold * current_price  * (1 - self.commission)
    self.btc_held -= btc_sold
    self.balance += sales

最后,在同一方法中,我们会将交易附加到self.trades并更新我们的净值和账户历史。

if btc_sold > 0 or btc_bought > 0:
    self.trades.append({
      'step': self.frame_start+self.current_step,
      'amount': btc_sold if btc_sold > 0 else btc_bought,
      'total': sales if btc_sold > 0 else cost,
      'type': "sell" if btc_sold > 0 else "buy"
    })
self.net_worth = self.balance + self.btc_held * current_price
  self.account_history = np.append(self.account_history, [
    [self.net_worth],
    [btc_bought],
    [cost],
    [btc_sold],
    [sales]
  ], axis=1)

Unsere Roboter können jetzt neue Umgebungen starten, sie schrittweise fertigstellen und Handlungen durchführen, die die Umgebung beeinflussen.

Schauen Sie sich unsere Roboterhandel an

Unsere Rendering kann so einfach sein wie den Aufruf print ((self.net_worth), aber das ist nicht lustig genug. Stattdessen werden wir ein einfaches Diagramm zeichnen, das ein separates Diagramm mit Handelsvolumen und unserem Netto enthält.

我们将从我上一篇文章中获取StockTradingGraph.py中的代码,并重新设计它以适应比特币环境。你可以从我的Github中获取代码。

Die erste Änderung, die wir machen werden, ist die Aktualisierung von self.df zu self.df und das Entfernen aller Aufrufe auf date2num, da unser Datum bereits im Unix-Zeitstempelformat ist. Als nächstes werden wir in unserer Rendering-Methode das Datum-Tag aktualisieren, um ein menschlich lesbares Datum und keine Zahlen zu drucken.

from datetime import datetime

Zuerst importieren wir die Datetime-Datenbank, und dann werden wir die UTC-String aus jedem Zeitfenster und Strftime mit der utcfromtimestampmethod abrufen, um sie in der Form: Y-m-d H:M-Format-String zu formatisieren.

date_labels = np.array([datetime.utcfromtimestamp(x).strftime('%Y-%m-%d %H:%M') for x in self.df['Timestamp'].values[step_range]])

Schließlich haben wir selb.df[Volume] zu selb.df[Volume_(BTC) ] geändert, um unsere Datensätze zu entsprechen, und wenn wir das getan haben, sind wir bereit. Zurück zu unserem BitcoinTradingEnv, können wir jetzt Rendering-Methoden schreiben, um die Grafiken zu zeigen.

def render(self, mode='human', **kwargs):
  if mode == 'human':
    if self.viewer == None:
      self.viewer = BitcoinTradingGraph(self.df,
                                        kwargs.get('title', None))
self.viewer.render(self.frame_start + self.current_step,
                       self.net_worth,
                       self.trades,
                       window_size=self.lookback_window_size)

Wir können jetzt unseren Roboter beim Bitcoin-Handel beobachten.

img

Mit Matplotlib visualisieren wir unsere Roboter-Transaktionen

Die grünen Phantom-Tags stehen für den Kauf von BTC, die roten Phantom-Tags für den Verkauf. Das weiße Label in der oberen rechten Ecke ist der aktuelle Nettowert des Roboters, das untere Recht ist der aktuelle Preis von Bitcoin. Einfach und elegant. Jetzt ist es Zeit, unseren Roboter zu trainieren und zu sehen, wie viel Geld wir verdienen können!

Zeit für das Training

Eine Kritik, die ich in einem früheren Artikel erhielt, war die fehlende Cross-Verification, die Daten nicht in Trainings- und Testsätze aufgeteilt wurden. Das Ziel war es, die Genauigkeit des Endmodells auf neue Daten zu testen, die zuvor noch nie gesehen wurden. Obwohl dies nicht das Thema des Artikels war, war es sehr wichtig.

Eine häufige Form der Cross-Verification beispielsweise ist die sogenannte k-fold-Verification, bei der Sie die Daten in k gleiche Gruppen aufteilen, jeweils eine Gruppe als Testgruppe verwenden und die restlichen Daten als Trainingsgruppe verwenden. Die Zeitreihen-Daten sind jedoch sehr zeitabhängig, was bedeutet, dass spätere Daten sehr von früheren Daten abhängen.

Die gleiche Schwäche trifft auch auf die meisten anderen Cross-Verification-Strategien zu, wenn sie auf Zeitreihendaten angewendet werden. Daher müssen wir nur einen Teil der vollständigen Datensätze als Trainingssätze für Abstufungen von Abstufungen bis zu einigen beliebigen Indizes verwenden und die restlichen Daten als Testsätze verwenden.

slice_point = int(len(df) - 100000)
train_df = df[:slice_point]
test_df = df[slice_point:]

Als nächstes werden wir zwei Umgebungen erstellen, eine für Trainingsdaten und eine für Testdaten, da unsere Umgebung nur so eingerichtet ist, dass sie nur einzelne Datenstücke verarbeitet.

train_env = DummyVecEnv([lambda: BitcoinTradingEnv(train_df, commission=0, serial=False)])
test_env = DummyVecEnv([lambda: BitcoinTradingEnv(test_df, commission=0, serial=True)])

现在,训练我们的模型就像使用我们的环境创建机器人并调用model.learn一样简单。

model = PPO2(MlpPolicy,
             train_env,
             verbose=1, 
             tensorboard_log="./tensorboard/")
model.learn(total_timesteps=50000)

Hier verwenden wir einen Strang-Stepboard, so dass wir unsere Strang-Flow-Diagramme leicht visualisieren und einige quantitative Indikatoren für unsere Roboter sehen können. Hier ist zum Beispiel ein Discounted Rewards-Chart, in dem viele Roboter mehr als 200.000 Zeitschritte haben:

img

Wow, es sieht aus, als wären unsere Roboter sehr profitabel! Unsere besten Roboter sind sogar in der Lage, über 200.000 Schritte hinweg 1000x Balance zu erzielen, wobei der Rest durchschnittlich mindestens 30 Mal erhöht wird!

In diesem Moment wurde mir klar, dass es einen Fehler in der Umgebung gab... und nachdem ich diesen Fehler behoben hatte, gab es eine neue Belohnung:

img

Wie Sie sehen können, machen einige unserer Roboter gut, andere gehen selbst bankrott. Allerdings können gute Roboter höchstens 10 oder sogar 60 Mal den ursprünglichen Bilanz erreichen. Ich muss zugeben, dass alle profitablen Roboter ohne Provisionen ausgebildet und getestet werden, so dass es für unsere Roboter nicht praktikabel ist, echtes Geld zu verdienen.

Lassen Sie uns unsere Roboter in einer Testumgebung testen (mit neuen Daten, die sie noch nie zuvor gesehen haben), um zu sehen, wie sie sich verhalten.

img

Unsere ausgebildeten Roboter scheitern beim Handel mit neuen Testdaten.

Offensichtlich gibt es noch viel zu tun. Indem wir das Modell einfach umschalten, um A2C mit stabiler Basis zu verwenden, anstatt der aktuellen PPO2-Roboter, können wir unsere Leistung auf diesem Datensatz erheblich verbessern. Und schließlich können wir unsere Belohnungsfunktion leicht aktualisieren, wie von Sean O'Gorman empfohlen, so dass wir Belohnungen im Netto erhöhen, anstatt nur einen hohen Netto zu erzielen und dort zu bleiben.

reward = self.net_worth - prev_net_worth

Allein diese beiden Änderungen können die Leistung der Testdatensätze erheblich verbessern, und wie Sie unten sehen werden, können wir endlich auf neue Daten profitieren, die in den Trainingssätzen nicht vorhanden sind.

img

Aber wir können es besser machen. Um diese Ergebnisse zu verbessern, müssen wir unsere Hyperparameter optimieren und unsere Roboter länger trainieren. Es ist Zeit, die GPUs an die Arbeit zu bringen und mit vollem Feuer!

Bis jetzt ist dieser Artikel ein bisschen lang, und wir haben noch viele Details zu berücksichtigen, also wollen wir hier eine Pause machen. Im nächsten Artikel werden wir Bayes-Optimierung verwenden, um die besten Supparameter für unseren Problemraum zu bestimmen und uns auf das Training / Testen mit CUDA auf der GPU vorzubereiten.

Schlussfolgerung

In diesem Artikel beginnen wir mit der Erstellung eines profitablen Bitcoin-Trading-Robot aus dem Nichts mit Hilfe von Reinforcement Learning.

1.使用OpenAI的gym从零开始创建比特币交易环境。

2.使用Matplotlib构建该环境的可视化。

3.使用简单的交叉验证对我们的机器人进行训练和测试。

4.略微调整我们的机器人以实现盈利

Obwohl unsere Handelsroboter nicht so profitabel sind, wie wir es uns erhofft haben, sind wir in die richtige Richtung gegangen. Nächstes Mal werden wir sicherstellen, dass unsere Roboter immer die Märkte schlagen, und wir werden sehen, wie unsere Handelsroboter mit Echtzeitdaten umgehen.


Verwandt

Mehr