
Dalam artikel ini, kami akan mencipta dan menggunakan rangka kerja pembelajaran pengukuhan untuk mempelajari cara membuat bot dagangan Bitcoin. Dalam tutorial ini, kami akan menggunakan gim OpenAI dan robot PPO daripada pustaka garis dasar stabil, yang merupakan garpu pustaka garis dasar OpenAI.
Terima kasih banyak kepada OpenAI dan DeepMind kerana menyediakan perisian sumber terbuka kepada penyelidik pembelajaran mendalam sejak beberapa tahun lalu. Jika anda belum melihat pencapaian menakjubkan yang telah mereka buat dengan teknologi seperti AlphaGo, OpenAI Five dan AlphaStar, anda mungkin telah hidup dalam pengasingan sejak tahun lepas, tetapi anda harus menyemaknya.

Latihan AlphaStar https://deepmind.com/blog/alphastar-mastering-real-time-strategy-game-starcraft-ii/
Walaupun kami tidak akan mencipta sesuatu yang mengagumkan, perdagangan robot Bitcoin masih bukan satu tugas yang mudah dalam perdagangan harian. Namun, seperti yang pernah dikatakan oleh Teddy Roosevelt,
Jadi, bukan sahaja belajar berdagang untuk diri sendiri…tetapi juga biarkan robot berdagang untuk kita.

Cipta persekitaran gim untuk robot kami melakukan pembelajaran mesin
Menghasilkan persekitaran visualisasi yang ringkas dan elegan
Latih robot kami untuk mempelajari strategi perdagangan yang menguntungkan
Jika anda masih belum biasa dengan cara mencipta persekitaran gim dari awal, atau cara hanya memaparkan visualisasi persekitaran ini. Sila google artikel seperti ini sebelum meneruskan. Kedua-dua tindakan ini tidak akan sukar walaupun untuk pengaturcara yang paling baru.
Dalam tutorial ini, kami akan menggunakan set data Kaggle yang dijana oleh Zielak. Jika anda ingin memuat turun kod sumber, ia tersedia dalam repositori Github saya, bersama-sama dengan fail data .csv. Baiklah, mari kita mulakan.
Mula-mula, mari kita import semua perpustakaan yang diperlukan. Pastikan anda memasang mana-mana perpustakaan yang anda hilang menggunakan pip.
import gym
import pandas as pd
import numpy as np
from gym import spaces
from sklearn import preprocessing
Seterusnya, mari kita cipta kelas kita untuk alam sekitar. Kita perlu memasukkan bingkai data panda, serta initial_balance pilihan dan lookback_window_size yang akan menentukan berapa banyak langkah masa lalu yang akan diperhatikan oleh robot pada setiap langkah. Kami lalai komisen setiap dagangan kepada 0.075%, kadar semasa pada Bitmex dan lalai parameter bersiri kepada palsu, bermakna bingkai data kami akan dilalui secara rawak secara lalai.
Kami juga memanggil dropna() dan reset_index() pada data, mula-mula untuk mengalih keluar baris dengan nilai NaN dan kemudian untuk menetapkan semula indeks untuk nombor bingkai kerana kami telah menggugurkan data.
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)
Ruang_tindakan kami diwakili di sini sebagai satu set 3 pilihan (beli, jual atau tahan) dan satu set lagi 10 jumlah (1⁄10, 2⁄10, 3⁄10 dll). Apabila memilih tindakan beli, kami akan membeli jumlah * self.balance bernilai BTC. Untuk penjualan, kami akan menjual amaun * harga diri.btc_held bagi BTC. Sudah tentu, tindakan penahanan mengabaikan jumlah dan tidak melakukan apa-apa.
Ruang_pemerhatian kami ditakrifkan sebagai satu set apungan berterusan antara 0 dan 1, dengan bentuk (10, lookback_window_size + 1). + 1 digunakan untuk mengira langkah masa semasa. Untuk setiap langkah dalam tetingkap, kami akan memerhatikan nilai OHCLV. Nilai bersih kami adalah sama dengan jumlah BTC yang dibeli atau dijual, dan jumlah keseluruhan USD yang kami belanjakan atau terima untuk BTC tersebut.
Seterusnya, kita perlu menulis kaedah tetapan semula untuk memulakan persekitaran.
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()
Di sini kita menggunakan diri sendiri._reset_session dan diri._next_observation, kami belum mentakrifkannya lagi. Mari kita tentukan mereka terlebih dahulu.

Bahagian penting dalam persekitaran kita ialah konsep sesi dagangan. Jika kami menggunakan bot ini di luar pasaran, kami mungkin tidak akan menjalankannya lebih daripada beberapa bulan pada satu masa. Atas sebab ini, kami akan mengehadkan bilangan bingkai berturut-turut dalam self.df, iaitu bilangan bingkai yang boleh dilihat oleh robot kami pada satu masa.
Dalam kaedah _reset_session kami, kami mula-mula menetapkan semula current_step kepada 0. Seterusnya, kami akan menetapkan steps_left kepada nombor rawak antara 1 dan MAX_TRADING_SESSION, yang akan kami tentukan di bahagian atas program.
MAX_TRADING_SESSION = 100000 # ~2个月
Seterusnya, jika kita ingin mengulangi bingkai secara berterusan, kita mesti menetapkannya untuk mengulangi seluruh bingkai, jika tidak, kita menetapkan frame_start kepada titik rawak dalam self.df dan mencipta bingkai data baharu yang dipanggil active_df yang hanya diri A daripada df dari frame_start ke frame_start + steps_left.
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]
Kesan sampingan yang penting untuk mengulang bilangan bingkai data dalam penghirisan rawak ialah robot kami akan mempunyai lebih banyak data unik untuk digunakan semasa berlatih untuk masa yang lama. Sebagai contoh, jika kita hanya mengulangi bilangan bingkai data secara bersiri (iaitu mengikut urutan dari 0 hingga len(df)), maka kita hanya akan mempunyai seberapa banyak titik data unik seperti yang terdapat dalam bilangan bingkai data. Ruang pemerhatian kami hanya boleh menerima pakai bilangan keadaan diskret pada setiap langkah masa.
Walau bagaimanapun, dengan melelaran secara rawak pada kepingan set data, kami boleh mencipta set hasil perdagangan yang lebih bermakna untuk setiap langkah masa dalam set data awal, iaitu gabungan tindakan perdagangan dan tindakan harga yang dilihat sebelum ini untuk mencipta set data yang lebih unik. Biar saya jelaskan ini dengan contoh.
Pada langkah masa 10 selepas menetapkan semula persekitaran bersiri, robot kami akan sentiasa berjalan serentak dalam set data dan akan mempunyai 3 pilihan selepas setiap langkah masa: Beli, Jual atau Tahan. Bagi setiap tiga pilihan ini, terdapat pilihan lain: 10%, 20%, … atau 100% daripada jumlah pelaksanaan tertentu. Ini bermakna robot kami boleh menghadapi mana-mana 103 hingga kuasa 10, untuk sejumlah 1030 situasi.
Sekarang kembali ke persekitaran penghirisan rawak kami. Pada langkah masa 10, robot kami mungkin berada pada mana-mana langkah masa len(df) dalam bilangan bingkai data. Dengan mengandaikan pilihan yang sama dibuat selepas setiap langkah masa, ini bermakna robot boleh melalui mana-mana keadaan unik len(df)30 dalam 10 langkah masa yang sama.
Walaupun ini mungkin memperkenalkan bunyi yang besar kepada set data yang besar, saya percaya ia sepatutnya membenarkan robot mengetahui lebih lanjut daripada jumlah data terhad yang kami ada. Kami masih akan berulang melalui data ujian kami secara bersiri untuk mendapatkan data yang paling segar dan kelihatan ‘masa nyata’ untuk mendapatkan pemahaman yang lebih tepat tentang keberkesanan algoritma.
Selalunya berguna untuk mempunyai gambaran keseluruhan visual yang baik tentang persekitaran untuk memahami jenis fungsi yang akan digunakan oleh robot kami. Sebagai contoh, berikut ialah visualisasi ruang yang boleh diperhatikan yang diberikan menggunakan OpenCV.

Pemerhatian persekitaran visualisasi OpenCV
Setiap baris dalam imej mewakili baris dalam ruang_pemerhatian kami. 4 baris pertama garis merah frekuensi serupa mewakili data OHCL, dan titik oren dan kuning betul-betul di bawah mewakili volum. Bar biru yang turun naik di bawah ialah ekuiti bot, manakala bar yang lebih ringan di bawah mewakili dagangan bot.
Jika anda melihat dengan teliti, anda juga boleh membuat carta candlestick anda sendiri. Di bawah bar kelantangan ialah antara muka seperti kod Morse yang menunjukkan sejarah dagangan. Nampaknya bot kami sepatutnya dapat belajar dengan secukupnya daripada data dalam observation_space kami, jadi mari teruskan. Di sini kami akan mentakrifkan kaedah _next_observation di mana kami akan menskalakan data yang diperhatikan dari 0 hingga 1.
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
Sekarang setelah ruang pemerhatian kami disediakan, tiba masanya untuk menulis fungsi langkah kami dan kemudian mengambil tindakan yang ingin dilakukan oleh robot. Setiap kali self.steps_left == 0 untuk sesi dagangan semasa kami, kami akan menjual pegangan BTC kami dan memanggil reset session(). Jika tidak, kami menetapkan ganjaran kepada ekuiti semasa, atau ditetapkan kepada Benar jika kami kehabisan dana.
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, {}
Mengambil tindakan perdagangan adalah semudah mendapatkan harga_semasa, menentukan tindakan yang perlu dilakukan dan jumlah untuk membeli atau menjual. Mari cepat menulis _take_action supaya kita boleh menguji persekitaran kita.
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
Akhir sekali, dalam kaedah yang sama, kami akan menambahkan dagangan kepada self.trades dan mengemas kini ekuiti dan sejarah akaun kami.
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)
Robot kami kini boleh melancarkan persekitaran baharu, melangkah melalui persekitaran itu dan mengambil tindakan yang menjejaskan alam sekitar. Sudah tiba masanya untuk melihat mereka berdagang.
Kaedah render kami boleh semudah memanggil print(self.net_worth) , tetapi itu tidak cukup menarik. Sebaliknya, kami akan melukis carta candlestick ringkas dengan bar volum dan carta berasingan untuk ekuiti kami.
Kami akan mengambil kod dalam StockTradingGraph.py daripada artikel saya sebelum ini dan mengolahnya semula agar sesuai dengan persekitaran Bitcoin. Anda boleh mendapatkan kod dari Github saya.
Perubahan pertama yang akan kita lakukan ialah mengubah diri.df[ ‘Tarikh’] Kemas kini kepada self.df[‘Timestamp’] dan alih keluar semua panggilan ke date2num kerana tarikh kami sudah pun dalam format unix timestamp. Seterusnya, dalam kaedah pemaparan kami, kami akan mengemas kini label tarikh untuk mencetak tarikh yang boleh dibaca manusia dan bukannya nombor.
from datetime import datetime
Mula-mula, kami akan mengimport pustaka datetime, kemudian kami akan menggunakan utcfromtimestampmethod untuk mendapatkan rentetan UTC daripada setiap cap waktu dan strftime untuk menjadikannya rentetan dalam format Y-m-d H:M .
date_labels = np.array([datetime.utcfromtimestamp(x).strftime('%Y-%m-%d %H:%M') for x in self.df['Timestamp'].values[step_range]])
Akhirnya, kita akan menggunakan self.df[‘Volume’] ditukar kepada self.df[‘Volume_(BTC)’] untuk memadankan set data kami dan dengan itu selesai, kami bersedia untuk pergi. Kembali ke BitcoinTradingEnv kami, kami kini boleh menulis kaedah render untuk memaparkan graf.
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)
Lihatlah! Kami kini boleh menonton robot kami berdagang Bitcoin.

Memvisualisasikan dagangan robot kami dengan Matplotlib
Label hantu hijau mewakili pembelian BTC, dan label hantu merah mewakili penjualan. Label putih di sudut kanan atas ialah nilai bersih semasa robot, dan label di sudut kanan bawah ialah harga semasa Bitcoin. Ringkas dan elegan. Sekarang, tiba masanya untuk melatih bot kami dan lihat berapa banyak wang yang boleh kami hasilkan!
Satu kritikan yang saya terima dalam artikel saya sebelum ini ialah kekurangan pengesahan silang dan tidak membahagikan data kepada set latihan dan ujian. Tujuannya adalah untuk menguji ketepatan model akhir pada data baharu yang tidak pernah dilihat sebelum ini. Walaupun ini bukan fokus artikel itu, ia sememangnya penting. Memandangkan kami sedang bekerja dengan data siri masa, kami tidak mempunyai banyak pilihan dalam hal pengesahan silang.
Sebagai contoh, bentuk pengesahan silang biasa dipanggil pengesahan k-lipat, di mana anda membahagikan data kepada k kumpulan yang sama, memisahkan salah satu kumpulan sebagai kumpulan ujian dan menggunakan data yang selebihnya sebagai kumpulan latihan. . Walau bagaimanapun, data siri masa sangat bergantung pada masa, yang bermaksud bahawa data kemudiannya sangat bergantung pada data terdahulu. Jadi k-fold tidak akan berfungsi kerana robot kami akan belajar daripada data masa hadapan sebelum berdagang, yang merupakan kelebihan yang tidak adil.
Kelemahan yang sama digunakan pada kebanyakan strategi pengesahan silang lain apabila digunakan pada data siri masa. Oleh itu, kita hanya perlu menggunakan sebahagian daripada bilangan lengkap bingkai data sebagai set latihan bermula dari permulaan nombor bingkai kepada beberapa indeks sewenang-wenangnya, dan menggunakan data yang selebihnya sebagai set ujian.
slice_point = int(len(df) - 100000)
train_df = df[:slice_point]
test_df = df[slice_point:]
Seterusnya, memandangkan persekitaran kami hanya disediakan untuk mengendalikan satu bingkai data, kami akan mencipta dua persekitaran, satu untuk data latihan dan satu untuk data ujian.
train_env = DummyVecEnv([lambda: BitcoinTradingEnv(train_df, commission=0, serial=False)])
test_env = DummyVecEnv([lambda: BitcoinTradingEnv(test_df, commission=0, serial=True)])
Kini, melatih model kami semudah mencipta robot dengan persekitaran kami dan memanggil model.belajar.
model = PPO2(MlpPolicy,
train_env,
verbose=1,
tensorboard_log="./tensorboard/")
model.learn(total_timesteps=50000)
Di sini, kami menggunakan papan tensor supaya kami boleh memvisualisasikan graf aliran tensor kami dengan mudah dan melihat beberapa metrik kuantitatif tentang robot kami. Sebagai contoh, berikut ialah plot ganjaran terdiskaun untuk banyak robot melebihi 200,000 langkah masa:

Wah, nampaknya bot kami agak menguntungkan! Robot terbaik kami juga mampu mencapai keseimbangan 1000x lebih baik sepanjang 200,000 langkah, dan selebihnya purata sekurang-kurangnya 30x peningkatan!
Pada ketika inilah saya menyedari terdapat pepijat dalam persekitaran… Selepas membetulkannya, berikut ialah peta ganjaran baharu:

Seperti yang anda lihat, beberapa robot kami melakukan kerja yang hebat, dan selebihnya telah muflis dengan sendirinya. Walau bagaimanapun, bot yang berprestasi baik boleh mencapai sehingga 10x atau bahkan 60x baki awal. Saya mesti mengakui bahawa semua bot yang menguntungkan dilatih dan diuji tanpa komisen, jadi adalah tidak realistik untuk bot kami membuat sebarang wang sebenar. Tetapi sekurang-kurangnya kami menemui arahnya!
Mari uji bot kami dalam persekitaran ujian (dengan data baharu yang tidak pernah mereka lihat sebelum ini) dan lihat prestasinya.

Bot terlatih kami menjadi muflis apabila berdagang data ujian baharu
Jelasnya, kita masih mempunyai banyak kerja yang perlu dilakukan. Dengan hanya menukar model untuk menggunakan garis dasar A2C yang stabil, dan bukannya robot PPO2 semasa, kami boleh meningkatkan prestasi kami dengan ketara pada set data ini. Akhir sekali, mengikut cadangan Sean O’Gorman, kami boleh mengemas kini fungsi ganjaran kami sedikit supaya kami menambah ganjaran kepada nilai bersih dan bukannya hanya mencapai nilai bersih yang tinggi dan meninggalkannya di sana.
reward = self.net_worth - prev_net_worth
Kedua-dua perubahan ini sahaja meningkatkan prestasi pada set data ujian dengan ketara, dan seperti yang anda lihat di bawah, kami akhirnya dapat mencapai keuntungan pada data baharu yang tiada dalam set latihan.

Tetapi kita boleh melakukan lebih baik. Untuk kami menambah baik hasil ini, kami perlu mengoptimumkan hiperparameter kami dan melatih bot kami lebih lama. Tiba masanya untuk membolehkan GPU anda berfungsi dan menyala pada semua silinder!
Siaran ini telah menjadi agak panjang pada ketika ini, dan kami masih mempunyai banyak butiran untuk dipertimbangkan, jadi kami akan berehat di sini. Dalam siaran seterusnya, kami akan menggunakan Pengoptimuman Bayesian untuk membahagikan hiperparameter terbaik untuk ruang masalah kami dan bersedia untuk latihan/ujian pada GPU menggunakan CUDA.
Dalam artikel ini, kami berhasrat untuk mencipta bot dagangan Bitcoin yang menguntungkan dari awal menggunakan pembelajaran pengukuhan. Kami boleh melaksanakan tugas berikut:
Cipta persekitaran dagangan Bitcoin dari awal menggunakan gim OpenAI.
Gunakan Matplotlib untuk membina visualisasi persekitaran.
Latih dan uji bot kami menggunakan pengesahan silang mudah.
Tweak sedikit robot kami untuk mencapai keuntungan
Walaupun robot dagangan kami tidak menguntungkan seperti yang kami inginkan, kami menuju ke arah yang betul. Lain kali, kami akan memastikan bot kami boleh mengalahkan pasaran secara konsisten dan kami akan melihat prestasi bot dagangan kami pada data langsung. Nantikan artikel saya yang seterusnya, dan Long Live Bitcoin!