ساخت ربات معامله گر رمزارز با لگاریتم تغییرات قیمت — پیاده سازی در پایتون

۴۶۲ بازدید
آخرین به‌روزرسانی: ۰۷ شهریور ۱۴۰۲
زمان مطالعه: ۱۵ دقیقه
ساخت ربات معامله گر رمزارز با لگاریتم تغییرات قیمت — پیاده سازی در پایتون

در مطالب گذشته، به کاربرد میانگین متحرک ساده و اندیکاتور استوکستیک در ایجاد و آموزش ربات‌های معامله‌گر پرداختیم. در این مطلب قصد داریم یک مدل خودهمبسته (Autoregressive) ایجاد کنیم که با استفاده از لگاریتم نسبت قیمت در L روز گذشته، سیگنال مربوط به هر روز را محاسبه کند و با توجه به آن معامله کند.

لگاریتم تغییرات قیمت

لگاریتم نسبت قیمت را می‌توانیم به شکل زیر تعریف می‌کنیم:

$$L C_{t}=\log \left(\frac{\text { Close }_{t}}{\text { open }_{t}}\right)$$

با توجه به اینکه قصد داریم یک مدل خودهمبسته ایجاد کنیم، با اضافه کردن اپراتور تأخیر (Lag Operator)، لگاریتم نسبت قیمت مربوط به تأخیر $$l$$ در زمان $$t$$ به شکل زیر قابل محاسبه خواهد بود:

$$ L C_{t, l}=\log \left(\frac{C l o s e_{t-l}}{O p e n_{t-l}}\right)$$

حال می‌توانیم یک مدل خودهمبسته خطی تعریف کنیم:

$$ S_{t}=\sum_{l=0}^{L-1} W_{l} \times L C_{t, l}=\sum_{l=0}^{L-1} W_{l} \times \log \left(\frac{\text { Close }_{t-l}}{\text{Open }_{t-l}}\right) $$

به این ترتیب سیگنال در زمان $$t$$ ترکیبی خطی از لگاریتم نسبت قیمت در $$L$$ دوره گذشته خواهد بود.

تعیین حد آستانه

برای جلوگیری از انجام معاملات اشتباه، یک حد آستانه (Threshold) تعریف می‌کنیم که همواره عددی مثبت است. با استفاده از حد آستانه، سیگنال نهایی را به شکل زیر محاسبه می‌کنیم:

$$ F S_{t}= \begin{cases}0 & \left|S_{t}\right|<T H \\ S_{t} & \text { otherwise }\end{cases} $$

به این ترتیب، سیگنال‌های با شدت کم، به $$0$$ تنظیم می‌شوند و مانع از انجام معاملات اشتباه می‌شود. مقدار حد آستانه نیز باید بهینه‌سازی شود، به همین دلیل، آن را نیز در آرایه (Array) پارامترها در نظر می‌گیریم.

اگر در یک نمودار سیگنال نهایی را در مقابل سیگنال اولیه رسم کنیم، شکل زیر را خواهیم داشت.

ربات معامله گر در پایتون

به این ترتیب، می‌توان مشاهده کرد که در بازه مقدار سیگنال نهایی برابر با صفر است و در خارج از بازه، به سیگنال ورودی واکنش نشان می‌دهد.

نظم‌دهی به ربات

با توجه به اینکه ممکن است تغییرات قیمت در برخی روزها، بدون تأثیر در سیگنال باشند، اضافه شدن آن‌ها به مدل‌سازی سیگنال، باعث بیش‌برازش (Overfitting) ربات خواهد شد. به همین دلیل، از یک جمله نظم‌دهی (Regularization Term) نیز برای جلوگیری از این اتفاق استفاده می‌کنیم. جمله نظم‌دهی را از درجه دوم در نظر گرفته و به شکل زیر تعریف می‌کنیم:

$$G(W)=\lambda \sum_{l=0}^{L-1} W_{l}^{2}=\lambda\|W\|_{2}^{2} $$

حال می‌توانیم تابع هزینه را به شکل زیر ایجاد کنیم:

$$L(W, T H)=-R(W, T H)+G(W) $$

تابع $$R$$ نشان‌دهنده بازده ربات است که با شبیه‌سازی معاملات در طول زمان به دست می‌آید. به این ترتیب هم سود ربات بیشینه می‌شود، هم از انجام معاملات اشتباه جلوگیری می‌شود و هم به ربات نظم‌دهی می‌شود تا بیش‌برازش رخ ندهد.

پیاده‌سازی ربات

حال برای پیاده‌سازی ربات، وارد محیط برنامه‌نویسی می‌شویم و کتابخانه‌های مورد نیاز را فراخوانی می‌کنیم:

این کتابخانه‌ها به ترتیب برای موارد زیر استفاده خواهند شد:

1import numpy as np
2import pandas as pd
3import pyswarm as ps
4import pandas_datareader as pdt
5import matplotlib.pyplot as plt
  1. محاسبات برداری و ایجاد آرایه
  2. کار با دیتافریم‌ها
  3. بهینه‌سازی ربات
  4. دریافت داده به صورت برخط (Online)
  5. رسم نمودار

حال کلاس مربوط به ربات را ایجاد می‌کنیم:

1class arLCbot:

متد سازنده را تعریف می‌کنیم و در ورودی مقدار $$L$$ (که نشان‌دهنده پنجره زمانی نگاه به گذشته است) و نسبت اندازه مجموعه داده آموزش (Train Dataset) به مجموعه داده کل را دریافت می‌کنیم:

1    def __init__(self, L:int, sTrain:float=0.6):

حال پارامترهای ورودی را در شیء ذخیره و دو لیست مربوط به Log بازده، خطا و مقدار نظم‌دهی در طول فرآیند آموزش را ایجاد می‌کنیم:

1    def __init__(self, L:int, sTrain:float=0.6):
2        self.L = L
3        self.sTrain = sTrain
4        self.TrainLogReturn = []
5        self.TrainLogLoss = []
6        self.TrainLogRegularization = []

پیاده‌سازی متد دریافت داده

حال می‌توانیم متد دیگری برای دریافت داده مربوط به نماد مورد نظر ایجاد کنیم:

1    def GetData(self, Ticker:str, Start:str, End:str):

ورودی‌های تابع را در شیء ذخیره می‌کنیم:

1    def GetData(self, Ticker:str, Start:str, End:str):
2        self.Ticker = Ticker
3        self.Start = Start
4        self.End = End

سپس دیتافریم مربوط به قیمت نماد را دریافت می‌کنیم و در شیء ذخیره می‌کنیم:

1    def GetData(self, Ticker:str, Start:str, End:str):
2        self.Ticker = Ticker
3        self.Start = Start
4        self.End = End
5        self.DF = pdt.DataReader(Ticker,
6                                 data_source='yahoo',
7                                 start=Start,
8                                 end=End)

حال ستون‌های اضافی موجود را نیز حذف می‌کنیم:

1    def GetData(self, Ticker:str, Start:str, End:str):
2        self.Ticker = Ticker
3        self.Start = Start
4        self.End = End
5        self.DF = pdt.DataReader(Ticker,
6                                 data_source='yahoo',
7                                 start=Start,
8                                 end=End)
9        self.DF.drop(labels=['Volume', 'Adj Close'],
10                     axis=1,
11                     inplace=True)

به این ترتیب، مجموعه داده خام دریافت و ذخیره خواهد شد.

پیاده‌سازی متد پردازش داده

حال برای پردازش داده، یک متد دیگری تعریف می‌کنیم که هیچ ورودی‌ای به جز شیء ندارد:

1    def ProcessData(self):

در اولین قدم، لگاریتم نسبت قیمت را محاسبه می‌کنیم و آن را به عنوان یک ستون به دیتافریم اضافه می‌کنیم:

1    def ProcessData(self):
2        self.DF['LC'] = np.log(self.DF['Close'] / self.DF['Open'])

حال می‌توانیم با استفاده از یک حلقه، برای تمامی تأخیرها از $$0$$ تا $$L-1$$ ستون مورد نظر را ایجاد کنیم. برای این فرآیند از متد shift استفاده می‌کنیم:

1    def ProcessData(self):
2        self.DF['LC'] = np.log(self.DF['Close'] / self.DF['Open'])
3        for i in range(self.L):
4            self.DF[f'LC(t-{i})'] = self.DF['LC'].shift(i)

حال ستون مربوط به اولین فرصت خرید را نیز اضافه می‌کنیم که قیمت Open روز بعد را نشان می‌دهد:

1    def ProcessData(self):
2        self.DF['LC'] = np.log(self.DF['Close'] / self.DF['Open'])
3        for i in range(self.L):
4            self.DF[f'LC(t-{i})'] = self.DF['LC'].shift(i)
5        self.DF['FP'] = self.DF['Open'].shift(-1)

با توجه به ایجاد مقدار NaN یا Not a Number در برخی سطرها، آن‌ها را حذف می‌کنیم. سپس اندازه مجموعه داده حاصل را محاسبه، و اندازه مجموعه داده آموزش را محاسبی می‌کنیم. در نهایت، دو مجموعه داده آموزش و آزمایش را جدا و در شیء ذخیره می‌کنیم:

1    def ProcessData(self):
2        self.DF['LC'] = np.log(self.DF['Close'] / self.DF['Open'])
3        for i in range(self.L):
4            self.DF[f'LC(t-{i})'] = self.DF['LC'].shift(i)
5        self.DF['FP'] = self.DF['Open'].shift(-1)
6        self.DF.dropna(inplace=True)
7        self.nD = len(self.DF)
8        self.nDtr = round(self.sTrain * self.nD)
9        self.nDte = self.nD - self.nDtr
10        self.trDF = self.DF.iloc[:self.nDtr, :]
11        self.teDF = self.DF.iloc[self.nDtr:, :]

به این ترتیب، در این متد، داده خام پردازش، ستون‌های مورد نیاز اضافه، سپس عملیات پاکسازی و تقسیم داده صورت می‌گیرد.

پیاده‌سازی متد شبیه‌سازی معامله

حال متد مربوط شبیه‌سازی انجام معاملات را ایجاد می‌کنیم و در ورودی مقدار حد آستانه، ماتریس وزن‌ها (ضرایب تأخیرها) و دیتافریم را دریافت می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)

در مرحله بعد، سه آرایه برای ذخیره پول، سهم و ارزش پرتفوی در طول زمان ایجاد می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)

حال دو دیکشنری نیز برای ذخیره سابقه معاملات خرید و فروش به طور جداگانه ایجاد می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}

به این ترتیب، تمامی متغیرها برای ذخیره تاریخچه ربات کامل خواهد بود. حال باید سیگنال را در طول زمان محاسبه کنیم. برای این منظور، باید ماتریس وزن هر روز را در ماتریس تأخیر در طول روزهای مختلف (ماتریس X) ضرب کنیم. بنابراین ماتریس مربوط به تأخیرهای مختلف در طول مجموعه داده را از دیتافریم جدا می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()

توجه داشته باشید که ستون آخر دیتافریم مربوط به اولین فرصت خرید است و $$L$$ ستون قبل آن مربوط به تأخیرهای مورد نظر است که برای محاسبه سیگنال مورد نیاز هستند. حال ضرب ماتریسی بین وزن‌ها ($$W$$) و ماتریس $$X$$ را انجام می‌دهیم و سیگنال اولیه حاصل می‌شود:

1    def Trade(self, P:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()
9        Signals = np.dot(X, P)

حال باید به کمک حد آستانه، سیگنال نهایی را محاسبه کنیم، که خواهیم داشت:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()
9        Signals = np.dot(X, W)
10        Signals[np.abs(Signals) < TH] = 0

به این ترتیب، سیگنال حاصل ترکیبی خطی از لگاریتم نسبت قیمت در L روز گذشته خواهد بود که برای هر روز قابل محاسبه است و مقادیر با شدت کم، به عدد صفر گرد شده‌اند. حال سرمایه اولیه و سهام اولیه را تعیین می‌کنیم و حلقه مربوط به معامله را ایجاد می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()
9        Signals = np.dot(X, W)
10        Signals[np.abs(Signals) < TH] = 0
11        money = 1
12        share = 0
13        for i in range(nD):

حال اولین فرصت خرید و سیگنال روز مربوطه را استخراج می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()
9        Signals = np.dot(X, W)
10        Signals[np.abs(Signals) < TH] = 0
11        money = 1
12        share = 0
13        for i in range(nD):
14            fp = DF['FP'][i]
15            signal = Signals[i]

حال شروط مربوط به انجام معامله را نوشته، تبدیلات بین پول و سهام را انجام و در نهایت معامله انجام‌شده را در تاریخچه ذخیره می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()
9        Signals = np.dot(X, W)
10        Signals[np.abs(Signals) < TH] = 0
11        money = 1
12        share = 0
13        for i in range(nD):
14            fp = DF['FP'][i]
15            signal = Signals[i]
16            if signal > 0 and share == 0:
17                share = money / fp
18                money = 0
19                Buys['Time'].append(i)
20                Buys['Price'].append(fp)
21            elif signal < 0 and share > 0:
22                money = share * fp
23                share = 0
24                Sells['Time'].append(i)
25                Sells['Price'].append(fp)

در انتهای روز معاملاتی نیز پول، سهم و ارزش پرتفوی را محاسبه و ذخیره می‌کنیم:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()
9        Signals = np.dot(X, W)
10        Signals[np.abs(Signals) < TH] = 0
11        money = 1
12        share = 0
13        for i in range(nD):
14            fp = DF['FP'][i]
15            signal = Signals[i]
16            if signal > 0 and share == 0:
17                share = money / fp
18                money = 0
19                Buys['Time'].append(i)
20                Buys['Price'].append(fp)
21            elif signal < 0 and share > 0:
22                money = share * fp
23                share = 0
24                Sells['Time'].append(i)
25                Sells['Price'].append(fp)
26            Moneys[i] = money
27            Shares[i] = share
28            Values[i] = money + share * fp

به این ترتیب، محاسبات مربوط به هر روز انجام خواهد شد. در انتها نیاز داریم تا میانگین بازده روزانه و نرخ بُرد (Win Rate) را نیز محاسبه کنیم و خروجی‌های مورد نیاز را برگردانیم. به این منظور خواهیم داشت:

1    def Trade(self, TH:float, W:np.ndarray, DF:pd.core.frame.DataFrame):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Buys = {'Time':[], 'Price':[]}
7        Sells = {'Time':[], 'Price':[]}
8        X = DF.iloc[:, -self.L - 1:-1].to_numpy()
9        Signals = np.dot(X, W)
10        Signals[np.abs(Signals) < TH] = 0
11        money = 1
12        share = 0
13        for i in range(nD):
14            fp = DF['FP'][i]
15            signal = Signals[i]
16            if signal > 0 and share == 0:
17                share = money / fp
18                money = 0
19                Buys['Time'].append(i)
20                Buys['Price'].append(fp)
21            elif signal < 0 and share > 0:
22                money = share * fp
23                share = 0
24                Sells['Time'].append(i)
25                Sells['Price'].append(fp)
26            Moneys[i] = money
27            Shares[i] = share
28            Values[i] = money + share * fp
29        Return = 100 * ((Values[-1] / Values[0])**(1 / (nD - 1)) - 1)
30        wr = WR(Buys, Sells)
31        return Moneys, Shares, Values, Signals, Buys, Sells, wr, Return

تابع مربوط به محاسبه نرخ برد را نیز به شکل زیر تعریف می‌کنیم:

1def WR(Buys:dict, Sells:dict):
2    nT = 0 # Count of Total Trader
3    nW = 0 # Count of Wins
4    for b, s in zip(Buys['Price'], Sells['Price']):
5        nT += 1
6        if s > b:
7            nW += 1
8    if nT != 0:
9        return 100 * nW / nT
10    return 0

به این ترتیب، متد مربوط به شبیه‌سازی معامله نیز تکمیل می‌شود.

پیاده‌سازی متد تابع هزینه

حال متد مربوط به تابع هزینه (Loss Function) را ایجاد می‌کنیم و در ورودی پارامترها، دیتافریم و مقدار لاندا (λ) که برای نظم‌دهی ربات هست را دریافت می‌کنیم:

1    def Loss(self, P:np.ndarray, DF:pd.core.frame.DataFrame, Lambda:float=8e-2):

حال، در اولین اقدام، مقدار حد آستانه و آرایه وزن‌ها را از پارامترها استخراج می‌کنیم. به طور قراردادی، اولین عضو پارامترها را حد آستانه در نظر می‌گیریم:

1    def Loss(self, P:np.ndarray, DF:pd.core.frame.DataFrame, Lambda:float=8e-2):
2        TH = P[0]
3        W = P[1:]

سپس متد Trade را فراخوانی می‌کنیم و از بین تمامی خروجی‌ها، آخرین مورد که مربوط به میانگین بازده روزانه است را با نام $$r$$ تعریف می‌کنیم:

1    def Loss(self, P:np.ndarray, DF:pd.core.frame.DataFrame, Lambda:float=8e-2):
2        TH = P[0]
3        W = P[1:]
4        R = self.Trade(TH, W, DF)[-1]

حال مقدار جمله نظم‌دهی را محاسبه و سپس مقدار هزینه را محاسبه می‌کنیم:

1    def Loss(self, P:np.ndarray, DF:pd.core.frame.DataFrame, Lambda:float=8e-2):
2        TH = P[0]
3        W = P[1:]
4        R = self.Trade(TH, W, DF)[-1]
5        G = Lambda * np.power(W, 2).sum()
6        L = -R + G

اکنون مقدار بازده، هزینه و مقدار نظم‌دهی را در لیست‌های مربوط ذخیره می‌کنیم. در نهایت نیز، مقدار هزینه را برمی‌گردانیم:

1    def Loss(self, P:np.ndarray, DF:pd.core.frame.DataFrame, Lambda:float=8e-2):
2        TH = P[0]
3        W = P[1:]
4        R = self.Trade(TH, W, DF)[-1]
5        G = Lambda * np.power(W, 2).sum()
6        L = -R + G
7        self.TrainLogReturn.append(R)
8        self.TrainLogLoss.append(L)
9        self.TrainLogRegularization.append(G)
10        return L

به این ترتیب، تابع هزینه برای بهینه‌سازی پارامترها آماده است.

پیاده‌سازی متد بهینه‌ساز

در این مرحله، نیاز داریم تا یک متد نیز برای انجام فرایند آموزش ربات ایجاد کنیم. این متد از الگوریتم بهینه‌سازی ازدحام ذرات (Particle Swarm Optimization یا PSO) استفاده خواهد، به همین دلیل، در ورودی تعداد ذرات و بیشترین تعداد مراحل را دریافت می‌کند:

1    def Train(self, SS:int, MI:int):

حال، حد پایین (Lower Bound) و حد بالا (Upper Bound) وزن‌ها را از $$-1$$ تا $$+1$$ تعیین می‌کنیم:

1    def Train(self, SS:int, MI:int):
2        lb = -1 * np.ones(self.L + 1)
3        ub = +1 * np.ones(self.L + 1)

با توجه به اینکه حد آستانه همواره مثبت است و نباید مقادیر خیلی بزرگی به خود بگیرد، آن را نیز بین $$0.005$$ و $$0.15$$ محدود می‌کنیم:

1    def Train(self, SS:int, MI:int):
2        lb = -1 * np.ones(self.L + 1)
3        ub = +1 * np.ones(self.L + 1)
4        lb[0] = 0.005
5        ub[0] = 0.15

حال تابع pso از کتابخانه pyswarm را بر روی تابع هزینه اعمال می‌کنیم و سایر ورودی‌ها را نیز تعریف می‌کنیم:

1    def Train(self, SS:int, MI:int):
2        lb = -1 * np.ones(self.L + 1)
3        ub = +1 * np.ones(self.L + 1)
4        lb[0] = 0.005
5        ub[0] = 0.15
6        self.P, self.BestLoss = ps.pso(self.Loss,
7                                       lb,
8                                       ub,
9                                       args=(self.trDF, ),
10                                       swarmsize=SS,
11                                       maxiter=MI)

این تابع، در خروجی به ترتیب بهترین جواب و بهترین مقدار تابع هزینه را برمی‌گرداند که به ترتیب در self.P و self.BestLoss ذخیره می‌کنیم.

مقدار حد آستانه و آرایه وزن‌ها را نیز استخراج و در شی ذخیره می‌کنیم:

1    def Train(self, SS:int, MI:int):
2        lb = -1 * np.ones(self.L + 1)
3        ub = +1 * np.ones(self.L + 1)
4        lb[0] = 0.005
5        ub[0] = 0.15
6        self.P, self.BestLoss = ps.pso(self.Loss,
7                                       lb,
8                                       ub,
9                                       args=(self.trDF, ),
10                                       swarmsize=SS,
11                                       maxiter=MI)
12        self.TH = self.P[0]
13        self.W = self.P[1:]

حال می‌توانیم میانگین بازده روزانه را نیز محاسبه کنیم:

1    def Train(self, SS:int, MI:int):
2        lb = -1 * np.ones(self.L + 1)
3        ub = +1 * np.ones(self.L + 1)
4        lb[0] = 0.005
5        ub[0] = 0.15
6        self.P, self.BestLoss = ps.pso(self.Loss,
7                                       lb,
8                                       ub,
9                                       args=(self.trDF, ),
10                                       swarmsize=SS,
11                                       maxiter=MI)
12        self.TH = self.P[0]
13        self.W = self.P[1:]
14        self.BestReturn = self.Trade(self.TH,
15                                     self.W,
16                                     self.trDF)[-1]

به این ترتیب، کلیه موارد در شرایط بهینه محاسبه می‌شود. حال نیاز داریم تا نتایج را نمایش دهیم:

1    def Train(self, SS:int, MI:int):
2        lb = -1 * np.ones(self.L + 1)
3        ub = +1 * np.ones(self.L + 1)
4        lb[0] = 0.005
5        ub[0] = 0.15
6        self.P, self.BestLoss = ps.pso(self.Loss,
7                                       lb,
8                                       ub,
9                                       args=(self.trDF, ),
10                                       swarmsize=SS,
11                                       maxiter=MI)
12        self.TH = self.P[0]
13        self.W = self.P[1:]
14        self.BestReturn = self.Trade(self.TH,
15                                     self.W,
16                                     self.trDF)[-1]
17        print('_' * 60)
18        print('Optimization Result:')
19        print(f'\tTH:        {round(self.TH, 4)}')
20        for i in range(self.L):
21            print(f'\tW(Lag={i}): {round(self.P[i + 1], 4)}')
22        print(f'\tReturn:    {round(self.BestReturn, 4)} %')
23        print('_' * 60)

به این ترتیب، مقدار حد آستانه، وزن هر روز و بازده روزانه حاصل نمایش داده خواهد شد.

پیاده‌سازی متد رسم نمودار وزن‌ها

حال برای مصورسازی (Visualization) وزن‌ها متد زیر را تعریف می‌کنیم تا مقدار وزن هر کدام از تأخیرها با شکل بصری قابل نمایش باشد:

1    def PlotWeights(self):
2        T = np.arange(start=0, stop=self.L, step=1)
3        plt.bar(T,
4                self.W,
5                width=0.4,
6                color='crimson')
7        plt.axhline(lw=1, c='k')
8        plt.title('Weight of Lags')
9        plt.xlabel('Lag')
10        plt.ylabel('Weight')
11        plt.show()

توجه داشته باشید که اولین وزن به ستون مربوط به لگاریتم نسبت قیمت در روز فعلی ضرب می‌شود. به همین دلیل، ایندکس (Index) نشان‌دهنده تأخیر نیز می‌شود.

پیاده‌سازی متد رسم عملکرد ربات در طول آموزش

با توجه به اینکه در طول آموزش مدل، بازده، خطا و مقدار نظم‌دهی را ذخیره می‌کنیم، در این بخش می‌توانیم سه نمودار رسم کنیم:

1    def PlotTrainLog(self):
2        plt.plot(self.TrainLogReturn,
3                 lw=0.8,
4                 c='teal',
5                 label='Return')
6        plt.plot(self.TrainLogLoss,
7                 lw=0.8,
8                 c='crimson',
9                 label='Loss')
10        plt.plot(self.TrainLogRegularization,
11                 lw=0.8,
12                 c='lime',
13                 label='Regularization')
14        plt.title('Bot Return, Loss & Regularization Over Training Iterations')
15        plt.xlabel('Function Evaluation')
16        plt.ylabel('Value')
17        plt.legend()
18        plt.show()

پیاده‌سازی متد رسم نمودار قیمت و ارزش پرتفوی در طول زمان

این متد یک ورودی برای تعیین داده مورد نظر دریافت می‌کند و براساس آن نمودار را رسم می‌کند:

1    def PlotValue(self, Data:str):
2        if Data == 'Train':
3            DF = self.trDF
4        elif Data == 'Test':
5            DF = self.teDF
6        O = self.Trade(self.TH, self.W, DF)
7        Values, bReturn = O[2], O[7]
8        plt.subplot(1, 2, 1)
9        hReturn = 100 * ((DF['Close'][-1] / DF['Close'][0])**(1 / (len(DF) - 1)) - 1)
10        T = np.arange(start=0, stop=Values.size, step=1)
11        hMeanValue = DF['Close'][0] * (1 + hReturn / 100)**T
12        plt.plot(DF.index,
13                 DF['Close'],
14                 lw=0.8,
15                 c='crimson',
16                 label='Close')
17        plt.plot(DF.index,
18                 hMeanValue,
19                 ls='--',
20                 lw=1,
21                 c='k',
22                 label=f'Mean Values (ADR: {round(hReturn, 4)} %)')
23        plt.title(f'Price Over Time ({Data})')
24        plt.xlabel('Time (Day)')
25        plt.ylabel('Price ($)')
26        plt.yscale('log')
27        plt.legend()
28        plt.subplot(1, 2, 2)
29        bMeanValue = (1 + bReturn / 100)**T
30        plt.plot(DF.index,
31                 Values,
32                 lw=0.8,
33                 c='crimson',
34                 label='Real Values')
35        plt.plot(DF.index,
36                 bMeanValue,
37                 ls='--',
38                 lw=1,
39                 c='k',
40                 label=f'Mean Values (ADR: {round(bReturn, 4)} %)')
41        plt.title(f'Value Over Time ({Data})')
42        plt.xlabel('Time (Day)')
43        plt.ylabel('Value')
44        plt.yscale('log')
45        plt.legend()
46        plt.show()

توجه داشته باشید که میانگین بازده روزانه نماد به شکل زیر آخرین قیمت را براساس اولین قیمت و فاصله زمانی توصیف می‌کند:

$$ \text { Close }_{T}=\text { Close }_{1} \times\left(1+\frac{A D R}{100}\right)^{T-1} $$

با حل رابطه، می‌توان گفت:

$$ A D R=100 \times\left(\sqrt[T-1]{\frac{\operatorname{Close}_{T}}{\operatorname{Close}_{1}}}-1\right) $$

به همین دلیل، برای محاسبه میانگین بازده روزانه نماد در حالت Hold به شکل زیر می‌نویسیم:

1hReturn = 100 * ((DF['Close'][-1] / DF['Close'][0])**(1 / (len(DF) - 1)) - 1)

پیاده‌سازی متد رسم نمودار سیگنال و خرید/فروش روی قیمت

این متد نیز مانند متد قبلی روی مجموعه آموزش و آزمایش قابل فراخوانی هست، به همین دلیل یک ورودی خواهد داشت:

1    def PlotSignal(self, Data:str):
2        if Data == 'Train':
3            DF = self.trDF
4        elif Data == 'Test':
5            DF = self.teDF
6        O = self.Trade(self.TH, self.W, DF)
7        Signals, Buys, Sells = O[3], O[4], O[5]
8        plt.subplot(3, 1, (1, 2))
9        plt.plot(DF.index,
10                 DF['Close'].to_numpy(),
11                 lw=0.7,
12                 c='k',
13                 label='Close')
14        plt.scatter(DF.index[Buys['Time']],
15                    Buys['Price'],
16                    s=24,
17                    c='lime',
18                    marker='o',
19                    label='Buy')
20        plt.scatter(DF.index[Sells['Time']],
21                    Sells['Price'],
22                    s=24,
23                    c='crimson',
24                    marker='o',
25                    label='Sell')
26        plt.title(f'Price Over Time ({Data})')
27        plt.ylabel('Price ($)')
28        plt.yscale('log')
29        plt.tight_layout()
30        plt.legend()
31        plt.subplot(3, 1, 3)
32        Z = np.zeros_like(Signals)
33        plt.plot(DF.index,
34                 Signals,
35                 lw=0.5,
36                 c='k')
37        plt.fill_between(DF.index,
38                         Signals,
39                         Z,
40                         where=(Signals > Z),
41                         color='lime',
42                         alpha=0.9,
43                         label='Buy Signal')
44        plt.fill_between(DF.index,
45                         Signals,
46                         Z,
47                         where=(Signals < Z),
48                         color='crimson',
49                         alpha=0.9,
50                         label='Sell Signal')
51        plt.axhline(lw=0.8, c='b')
52        plt.title(f'Signal Over Time ({Data})')
53        plt.xlabel('Time (Day)')
54        plt.ylabel('Value')
55        plt.tight_layout()
56        plt.legend()
57        plt.show()

به این ترتیب، تمامی متدهای مورد نیاز برای کلاس پیاده‌سازی می‌شود.

حال برای استفاده از کلاس نوشته‌شده، ابتدا تنظیمات زیر را اعمال می‌کنیم:

1np.random.seed(0)
2plt.style.use('ggplot')

حال می‌توانیم ربات را ایجاد کرده و مقدار $$L$$ را برابر $$10$$ قرار دهیم. به این ترتیب، ربات از لگاریتم نسبت قیمت در $$10$$ روز اخیر برای تصمیم‌گیری استفاده خواهد کرد:

1Trader = arLCbot(10)

اکنون، داده مورد نظر را دریافت می‌کنیم:

1Trader.GetData('BTC-USD', '2018-01-01', '2022-05-01')

به این ترتیب، حدود $$1600$$ روز داده خام خواهیم داشت.

حال می‌توانیم مجموعه داده را پردازش و آن را تقسیم کنیم:

1Trader.ProcessData()

در نهایت نیز تابع مربوط به آموزش ربات را فراخوانی می‌کنیم:

1Trader.Train(SS=40, MI=15)

به این ترتیب، فرایند آموزش ربات به اتمام می‌رسد و نتایج زیر ظاهر می‌شود:

Optimization Result:
        TH:        0.0486
        W(Lag=0): -0.0569
        W(Lag=1):  0.0669
        W(Lag=2):  0.194
        W(Lag=3): -0.0351
        W(Lag=4):  0.4303
        W(Lag=5):  0.4932
        W(Lag=6): -0.3226
        W(Lag=7):  0.2929
        W(Lag=8): -0.0237
        W(Lag=9):  0.2188
        Return:    0.1887 %

مشاهده می‌کنیم که بهترین مقدار برای حد آستانه $$0.0486$$ تعیین می‌شود. به این ترتیب، سیگنال‌های بین $$-0.0486$$ و $$+0.0486$$ برای گرفتن هیچ موقعیتی استفاده نخواهد شد.

شدیدترین ارتباط سیگنال امروز نیز با $$5$$ روز قبل نشان داده می‌شود.

برای مجموعه داده آموزش نیز میانگین بازده روزانه $$0.1887$$ درصد به دست می‌آید.

حال می‌توانیم نمودار وزن‌ها را نیز به شکل زیر رسم کنیم:

1Trader.PlotWeights()

که در نتیجه این دستور، نمودار زیر حاصل می‌شود.

ربات رمزارز در پایتون

حال می‌توانیم نمودار مربوط به عملکرد ربات در طول آموزش را نیز رسم کنیم:

1Trader.PlotTrainLog()

که در این صورت، نمودار زیر حاصل خواهد شد.

ربات معامله‌گر رمزارز با استفاده از لگاریتم تغییرات قیمت در پایتون

به این ترتیب، مشاهده می‌کنیم که همزمان با کاهش خطا و مقدار نظم‌دهی، بازده ربات نیز افزایش می‌یابد. توجه داشته باشید که اختلاف دو نمودار Return و Regularization برابر با نمودار Loss است.

حال می‌توانیم نمودار مربوط به بازده نماد و بازده ربات را در کنار هم رسم کنیم:

1Trader.PlotValue('Train')

که نمودار زیر حاصل می‌شود.

که نمودار زیر حاصل می‌شود:

به این ترتیب، مشاهده می‌کنیم که با وجود روند و بازده منفی در خود نماد، ربات توانسته به سود روزانه $$0.1887$$ درصد برسد که بسیار مناسب است.

حال می‌توانیم نمودار سیگنال و نقاط خرید و فروش را نیز رسم کنیم:

1Trader.PlotSignal('Train')

که در این مورد نیز شکل زیر را خواهیم داشت.

که در این مورد نیز خواهیم داشت:

به این ترتیب، مشاهده می‌کنیم که در اغلب روزها مقدار سیگنال صفر است که به دلیل استفاده از حد آستانه است. به این ترتیب۷ تعداد کم شده است و در مقابل اعتبار آن‌ها افزایش یافته است.

می‌توانیم این دو نمودار را برای مجموعه داده آزمایش نیز تکرار کنیم، که در این صورت شکل‌های زیر را خواهیم داشت.

شکل اول به‌صورت زیر است.

ربات رمز ارز در پایتون

شکل دوم نیز در ادامه آورده شده است.

ربات معامله رمز ارز

به این ترتیب، در نمودار اول مشاهده می‌کنیم که نماد از یک روند صعودی برخوردار بوده که به میانگین سود روزانه $$0.1841$$ درصد رسیده است که عدد نسبتاً بزرگی است. در مقابل، ربات نیز در بازه متناظر به میانگین سود $$0.1757$$ درصد رسیده است که مقدار نزدیکی است. در نمودار دوم می‌توان با بررسی دقیق‌تر به این نتیجه رسید که غالب سود ربات، در روندهای صعودی قوی دریافت شده است. سایر معاملات انجام‌شده در مواقعی بوده‌اند که بازار دارای روند قدرتمندی نبوده و ربات دچار ضعف در عملکرد شده است. به این دلیل، استفاده از یک مدل خودهمبسته براساس تاریخچه لگاریتم نسبت قیمت، در کنار مزایا، این ضعف را نیز دارد.

محاسبه نرخ بُرد

برای محاسبه نرخ بُرد، ابتدا متد Trade را روی هر دو مجموعه داده آموزش و آزمایش اجرا می‌کنیم:

1trWR = Trader.Trade(Trader.TH,
2                    Trader.W,
3                    Trader.trDF)[-2]
4teWR = Trader.Trade(Trader.TH,
5                    Trader.W,
6                    Trader.teDF)[-2]

توجه داشته باشید که متد Trade در خروجی $$8$$ متغیر برمی‌گرداند که هفتمین متغیر مربوط به نرخ بُرد است.

حال هر دو عدد حاصل را نمایش می‌دهیم:

1print(f'Train Win Rate: {round(trWR, 2)} %')
2print(f'Test Win Rate:  {round(teWR, 2)} %')

که خواهیم داشت:

Train Win Rate: 59.09 %
Test Win Rate:  58.82 %

به این ترتیب، مشاهده می‌کنیم که غالب معاملات سودده بوده‌اند و عملکرد روی مجموعه داده آزمایش، به عملکرد بر روی مجموعه داده آزمایش نزدیک است، بنابراین، می‌توان تعمیم‌پذیری (Generalizability) مدل را تأیید کرد.

جمع‌بندی

به این ترتیب، ربات مورد نظر را به کمک برنامه‌نویسی شی‌ءگرا (Object Oriented Programmingیا OOP) پیاده‌سازی کردیم و نتایج را تحلیل کردیم. برای مطالعه بیشتر، می‌توان موارد زیر را بررسی کرد:

  1. اگر به جای لگاریتم نسبت قیمت، از تغییرات قیمت نسبی استفاده کنیم، نتایج به چه شکلی تغییر خواهد کرد؟
  2. اگر مقدار λ برابر با صفر تنظیم شود، نتایج به چه شکلی خواهد بود؟
  3. چگونه می‌توان این ربات را بهبود داد تا بتواند روندها را بهتر شناسایی کند؟
  4. اگر در رابطه درنظر‌گرفته‌شده برای سیگنال، علاوه بر ضرایب مربوط به لگاریتم نسبت قیمت در روزهای گذشته، یک مقدار ثابت نیز به عبارت اضافه کنیم تا نقش عرض از مبدأ (Intercept) یا بایاس (Bias) را داشته باشد، نتایج به چه شکلی تغییر خواهد کرد؟ آیا تعمیم‌پذیری مدل تغییر خواهد کرد؟
  5. با تقسیم مجموعه داده اولیه به 3 بخش، یک مجموعه داده ارزیابی نیز ایجاد کنید و در طول آموزش بازده ربات را برای این مجموعه داده نیز محاسبه کنید. این نمودار چه شباهتی به منحنی یادگیری (Learning Curve) دارد؟ از این نمودار چه اطلاعاتی می‌توان استخراج کرد؟
بر اساس رای ۱۷ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
مجله فرادرس
۳ دیدگاه برای «ساخت ربات معامله گر رمزارز با لگاریتم تغییرات قیمت — پیاده سازی در پایتون»

با سلام و همینطور عرض تشکر ازآقای دکتر کلامی عزیز. سوالی داشتم از جنابعالی . بهترین روش جهت ترکیب سیگنالها که نتیجه عملکرد را بهتر کند. تشکر

سلام، در اغلب پدیده‌های بازارهای مالی، پدیده‌ها از نوع غیرخطی می‌باشند. به همین دلیل ترکیب سیگنال‌ها نیز بهترین نتیجه را در شرایطی خواهد داشت که یک ترکیب غیرخطی از آن‌ها داشته باشیم. در چنین شرایطی اغلب از الگوریتم‌های یادگیری ماشین و حتی یادگیری عمیق استفاده می‌کنیم. این نکته نیز حائز اهمیت می‌باشد که ترکیب خطی با اینکه توانایی برازش کمتری دارد، در مقابل نتایج حاصل از آن‌ها قابل اعتمادتر خواهد بود.

خیلی خوب ممنون ازشما

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *