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

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

در مطالب گذشته مجله فرادرس، به ساخت ربات معامله‌گر با استفاده از میانگین متحرک ساده (Simple Moving Average | SMA) و تقسیم مجموعه داده پرداختیم. در این مطلب، قصد داریم یک ربات معامله‌گر با استفاده از اسیلاتور استوکستیک (Stochastic Oscillator) می‌پردازیم که یک اندیکاتور (Indicator) نوسانگر است.

اسیلاتور استوکستیک

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

$$\begin{aligned}
&L L_{t}=\min \left(\left\{L o w_{i} \mid t-L+1 \leq i \leq t\right\}\right) \\
&H H_{t}=\max \left(\left\{H i g h_{i} \mid t-L+1 \leq i \leq t\right\}\right) \\
&K_{t}=100 \times \frac{\text { close }_{t}-L L_{t}}{H H_{t}-L L_{t}}
\end{aligned}$$

خط K همواره عددی بین 0 و 100 است. اعداد بین 0 تا 30 نشان‌دهنده بیش‌فروش (Oversold) است و انتظار صعود قیمت را در آینده داریم. اعداد بین 70 تا 100 نیز نشان‌دهنده بیش‌خرید (Overbought) است و انتظار نزول قیمت را در آینده داریم.

سپس، یک خط D به صورت میانگین متحرک ساده 3 روزه از روی K محاسبه می‌شود:

$$D_t=SMA_t (K,3) $$

به خط K استوکستیک سریع (Fast Stochastic) و به خط D استوکستیک آرام (Slow Stochastic) گفته می‌شود.

تقاطع خط K با خط D به سمت بالا، سیگنال برای خرید می‌باشد و برعکس آن، تقاطع خط K با خط D به سمت پایین، سیگنال برای فروش است.

به این ترتیب، اختلاف خط K و خط D می‌تواند معیار مناسبی به عنوان سیگنال باشد:

$$ Signal _ t = K_t-D_t $$

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

1import numpy as np
2import pandas as pd
3import pandas_datareader as pdt
4import matplotlib.pyplot as plt

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

  1. محاسبات برداری (Vectorized Calculation) و استفاده از آرایه‌ها (Array)
  2. کار با دیتافریم‌ها (Dataframe)
  3. دریافت آنلاین (Online) مجموعه داده مربوط به تاریخچه قیمتی (Historical Price) نمادها
  4. رسم نمودار قیمت، سیگنال و نقاط خرید و فروش ربات

حال تنظیمات مربوط به Randomness و Style را اعمال می‌کنیم:

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

پیاده‌سازی کلاس

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

1class stcBot:

این کلاس شامل 8 متد (Method) خواهد بود.

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

با توجه به اینکه طول پنجره محاسبه خط K به صورت دقیق معلوم نیست، باید بهینه‌سازی شود. به همین دلیل، حد بالا و پایین طول پنجره خط K، به همراه طول میانگین متحرک مربوط به خط D و در نهایت نسبت اندازه مجموعه داده آموزش (Train Dataset) به کل مجموعه داده در ورودی متن سازنده دریافت خواهند شد:

1    def __init__(self, Ld:int=5, MinL:int=2, MaxL:int=200, sTrain:float=0.6):

حال موارد دریافت‌شده را در شی (Object) که با نام self می‌شناسیم، ذخیره می‌کنیم:

1    def __init__(self, Ld:int=4, MinL:int=2, MaxL:int=200, sTrain:float=0.6):
2        self.Ld = Ld
3        self.MinL = MinL
4        self.MaxL = MaxL
5        self.sTrain = sTrain

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

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

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

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

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

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)

دو ستون Volume و Adj Close مورد نیاز نبوده و آن‌ها را حذف می‌کنیم:

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)

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

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

این متد عملیاتی روی مجموعه داده خام (Raw Dataset) انجام می‌دهد و آن را به شکل قابل استفاده درمی‌آورد. با توجه به اینکه نیاز داریم تا تمامی Lهای بین$$L_\min$$ و$$L_\max$$ را بررسی کنیم، باید با استفاده از یک حلقه، برای تمامی Lها اندیکاتور را محاسبه کنیم:

1    def ProcessData(self):
2        for L in range(self.MinL, self.MaxL+1):

حال دو ستون LL و HH را با استفاده از متدهای rolling, min, max محاسبه می‌کنیم:

1    def ProcessData(self):
2        for L in range(self.MinL, self.MaxL+1):
3            self.DF['LL'] = self.DF['Low'].rolling(L).min()
4            self.DF['HH'] = self.DF['High'].rolling(L).max()

حال می‌توانیم خط K، خط D و سیگنال نهایی را محاسبه کنیم:

1    def ProcessData(self):
2        for L in range(self.MinL, self.MaxL+1):
3            self.DF['LL'] = self.DF['Low'].rolling(L).min()
4            self.DF['HH'] = self.DF['High'].rolling(L).max()
5            self.DF['K'] = (self.DF['Close'] - self.DF['LL']) / (self.DF['HH'] - self.DF['LL'])
6            self.DF['D'] = self.DF[f'K'].rolling(self.Ld).mean()
7            self.DF[f'Signal({L})'] = self.DF['K'] - self.DF['D']

سپس ستون‌های اضافی را حذف می‌کنیم:

1    def ProcessData(self):
2        for L in range(self.MinL, self.MaxL+1):
3            self.DF['LL'] = self.DF['Low'].rolling(L).min()
4            self.DF['HH'] = self.DF['High'].rolling(L).max()
5            self.DF['K'] = (self.DF['Close'] - self.DF['LL']) / (self.DF['HH'] - self.DF['LL'])
6            self.DF['D'] = self.DF[f'K'].rolling(self.Ld).mean()
7            self.DF[f'Signal({L})'] = self.DF['K'] - self.DF['D']
8            self.DF.drop(['LL', 'HH', 'K', 'D'], axis=1, inplace=True)

به این ترتیب، برای هر L خط سیگنال محاسبه و به دیتافریم افزوده می‌شود.

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

1    def ProcessData(self):
2        for L in range(self.MinL, self.MaxL+1):
3            self.DF['LL'] = self.DF['Low'].rolling(L).min()
4            self.DF['HH'] = self.DF['High'].rolling(L).max()
5            self.DF['K'] = (self.DF['Close'] - self.DF['LL']) / (self.DF['HH'] - self.DF['LL'])
6            self.DF['D'] = self.DF[f'K'].rolling(self.Ld).mean()
7            self.DF[f'Signal({L})'] = self.DF['K'] - self.DF['D']
8            self.DF.drop(['LL', 'HH', 'K', 'D'], axis=1, inplace=True)
9        self.DF['FP'] = self.DF['Open'].shift(-1)

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

1    def ProcessData(self):
2        for L in range(self.MinL, self.MaxL+1):
3            self.DF['LL'] = self.DF['Low'].rolling(L).min()
4            self.DF['HH'] = self.DF['High'].rolling(L).max()
5            self.DF['K'] = (self.DF['Close'] - self.DF['LL']) / (self.DF['HH'] - self.DF['LL'])
6            self.DF['D'] = self.DF[f'K'].rolling(self.Ld).mean()
7            self.DF[f'Signal({L})'] = self.DF['K'] - self.DF['D']
8            self.DF.drop(['LL', 'HH', 'K', 'D'], axis=1, inplace=True)
9        self.DF['FP'] = self.DF['Open'].shift(-1)
10        self.DF.dropna(inplace=True)

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

1    def ProcessData(self):
2        for L in range(self.MinL, self.MaxL+1):
3            self.DF['LL'] = self.DF['Low'].rolling(L).min()
4            self.DF['HH'] = self.DF['High'].rolling(L).max()
5            self.DF['K'] = (self.DF['Close'] - self.DF['LL']) / (self.DF['HH'] - self.DF['LL'])
6            self.DF['D'] = self.DF[f'K'].rolling(self.Ld).mean()
7            self.DF[f'Signal({L})'] = self.DF['K'] - self.DF['D']
8            self.DF.drop(['LL', 'HH', 'K', 'D'], axis=1, inplace=True)
9        self.DF['FP'] = self.DF['Open'].shift(-1)
10        self.DF.dropna(inplace=True)
11        self.nD = len(self.DF)
12        self.nDtr = round(self.sTrain * self.nD)
13        self.nDte = self.nD - self.nDtr
14        self.trDF = self.DF.iloc[:self.nDtr, :]
15        self.teDF = self.DF.iloc[self.nDtr:, :]

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

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

این متد در ورودی دیتافریم مربوط به مجموعه داده و طول پنجره اندیکاتور را دریافت می‌کند:

1    def Trade(self, DF:pd.core.frame.DataFrame, L:int):

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

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

حال، سرمایه اولیه و سهام اولیه را تعیین می‌کنیم:

1    def Trade(self, DF:pd.core.frame.DataFrame, L:int):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Signals = np.zeros(nD)
7        Buys = {'Time':[], 'Price':[]}
8        Sells = {'Time':[], 'Price':[]}
9        money = 1
10        share = 0

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

1    def Trade(self, DF:pd.core.frame.DataFrame, L:int):
2        nD = len(DF)
3        Moneys = np.zeros(nD)
4        Shares = np.zeros(nD)
5        Values = np.zeros(nD)
6        Signals = np.zeros(nD)
7        Buys = {'Time':[], 'Price':[]}
8        Sells = {'Time':[], 'Price':[]}
9        money = 1
10        share = 0
11        for i in range(nD):
12            fp = DF['FP'][i]
13            signal = DF[f'Signal({L})'][i]

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

Signal<0Signal=0Signal>0
SellHoldHoldShare>0
HoldHoldBuyShare=0

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

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

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

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

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

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

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

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

1    def Train(self):
2        self.Ls = np.arange(start=self.MinL, stop=self.MaxL + 1, step=1)
3        self.Rs = []

حال با استفاده از یک حلقه، به ازای هر L تابع Trade فراخوانی و میانگین درصد سود روزانه به لیست Rs اضافه می‌کنیم:

1    def Train(self):
2        self.Ls = np.arange(start=self.MinL, stop=self.MaxL + 1, step=1)
3        self.Rs = []
4        for L in self.Ls:
5            R = self.Trade(self.trDF, L)[-1]
6            self.Rs.append(R)

پس از اتمام حلقه، لیست Rs را به آرایه Numpy تبدیل می‌کنیم، سپس بیشترین میانگین درصد سود حاصل و بهترین L را ذخیره می‌کنیم و خروجی را نمایش می‌دهیم:

1    def Train(self):
2        self.Ls = np.arange(start=self.MinL, stop=self.MaxL + 1, step=1)
3        self.Rs = []
4        for L in self.Ls:
5            R = self.Trade(self.trDF, L)[-1]
6            self.Rs.append(R)
7        self.Rs = np.array(self.Rs)
8        self.BestReturn = self.Rs.max()
9        self.L = self.Ls[self.Rs.argmax()]
10        print('_' * 50)
11        print('Optimization Result:')
12        print(f'\tBest L: {self.L}')
13        print(f'\tBest Return: {self.BestReturn} %')
14        print('_' * 50)

به این ترتیب، این متد بهترین L را برای ربات انتخاب و در شی ذخیره می‌کند.

تا به اینجا، 5 متد اصلی و مهم کلاس پیاده‌سازی شد. 3 متد بعدی مربوط به مصورسازی (Visualization) ربات هستند.

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

این نمودار رابطه بین میانگین درصد سود روزانه با طول پنجره خط K را نشان می‌دهد. برای رسم این نمودار از دو آرایه Ls و Rs استفاده می‌کنیم:

1    def PlotRs(self):
2        plt.plot(self.Ls, self.Rs, ls='-', lw=0.8, c='teal', marker='o', ms=3, label='Points')
3        plt.scatter(self.L, self.BestReturn, s=50, marker='o', color='crimson', label='Best')
4        plt.title('Bot Return for Different Values of L')
5        plt.xlabel('L')
6        plt.ylabel('Average Daily Return (%)')
7        plt.legend()
8        plt.show()

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

پیاده‌سازی متد رسم نمودار Price-Time و Value-Time

این متد در ورودی مجموعه داده مورد نظر را دریافت خواهد کرد و سپس متد Trade روی آن اجرا خواهد شد:

1    def PlotValue(self, Data:str):
2        if Data == 'Train':
3            DF = self.trDF
4        elif Data == 'Test':
5            DF = self.teDF
6        _, _, Values, _, _, _, bReturn = self.Trade(DF, self.L)

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

1    def PlotValue(self, Data:str):
2        if Data == 'Train':
3            DF = self.trDF
4        elif Data == 'Test':
5            DF = self.teDF
6        _, _, Values, _, _, _, bReturn = self.Trade(DF, self.L)
7        plt.subplot(1, 2, 1)
8        hReturn = 100 * ((DF['Close'][-1] / DF['Close'][0])**(1 / (len(DF) - 1)) - 1)
9        hMeanValue = DF['Close'][0] * (1 + hReturn / 100)**np.arange(start=0, stop=Values.size, step=1)
10        plt.plot(DF.index, DF['Close'], ls='-', lw=0.8, c='crimson', label='Close')
11        plt.plot(DF.index, hMeanValue, ls='--', lw=1, c='k', label=f'Mean Values (ADR: {round(hReturn, 4)} %)')
12        plt.title(f'Price Over Time ({Data})')
13        plt.xlabel('Time (Day)')
14        plt.ylabel('Price ($)')
15        plt.yscale('log')
16        plt.legend()
17        plt.subplot(1, 2, 2)
18        bMeanValue = (1 + bReturn / 100)**np.arange(start=0, stop=Values.size, step=1)
19        plt.plot(DF.index, Values, ls='-', lw=0.8, c='crimson', label='Real Values')
20        plt.plot(DF.index, bMeanValue, ls='--', lw=1, c='k', label=f'Mean Values (ADR: {round(bReturn, 4)} %)')
21        plt.title(f'Value Over Time ({Data})')
22        plt.xlabel('Time (Day)')
23        plt.ylabel('Value')
24        plt.yscale('log')
25        plt.legend()
26        plt.show()

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

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

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

1    def PlotSignal(self, Data:str):
2        if Data == 'Train':
3            DF = self.trDF
4        elif Data == 'Test':
5            DF = self.teDF
6        _, _, _, Signals, Buys, Sells, _ = self.Trade(DF, self.L)
7        plt.subplot(3, 1, (1, 2))
8        plt.plot(DF.index, DF['Close'].to_numpy(), ls='-', lw=0.7, c='k', label='Close')
9        plt.scatter(DF.index[Buys['Time']], Buys['Price'], s=24, c='lime', marker='o', label='Buy')
10        plt.scatter(DF.index[Sells['Time']], Sells['Price'], s=24, c='crimson', marker='o', label='Sell')
11        plt.title(f'Price Over Time ({Data})')
12        plt.ylabel('Price ($)')
13        plt.yscale('log')
14        plt.tight_layout()
15        plt.legend()
16        plt.subplot(3, 1, 3)
17        Z = np.zeros_like(Signals)
18        plt.plot(DF.index, Signals, ls='-', lw=0.7, c='k')
19        plt.fill_between(DF.index, Signals, Z, where=(Signals > Z), color='lime', alpha=0.9, label='Buy Signal')
20        plt.fill_between(DF.index, Signals, Z, where=(Signals < Z), color='crimson', alpha=0.9, label='Sell Signal')
21        plt.axhline(ls='-', lw=0.8, c='b')
22        plt.title(f'Signal Over Time ({Data})')
23        plt.xlabel('Time (Day)')
24        plt.ylabel('Value')
25        plt.tight_layout()
26        plt.legend()
27        plt.show()

به این ترتیب، هر 8 متد مورد نیاز برای کلاس stcBot پیاده‌سازی شد. حال می‌توانیم از کلاس ایجادشده استفاده کنیم.

استفاده از کلاس

حال یک شی از کلاس ایجاد می‌کنیم:

1Trader = stcBot()

سپس، مجموعه داده را دریافت می‌کنیم و پردازش‌های مورد نیاز را انجام می‌دهیم:

1Trader.GetData('BTC-USD', '2009-01-01', '2022-04-24')
2
3Trader.ProcessData()

سپس، مدل را آموزش می‌دهیم:

1Trader.Train()

که پس از اتمام آن، نتیجه به شکل زیر برگردانده می‌شود:

1Optimization Result:
2        Best L: 80
3        Best Return: 0.2914800215035429 %

به این ترتیب، مشاهده می‌کنیم که مقدار L=80 به عنوان بهترین طول پنجره استوکستیک سریع انتخاب شده است. در نتیجه استفاده از این طول پنجره، میانگین درصد سود روزانه برابر با 0.2914 % حاصل شده که مناسب است. باید توجه داشته که این سود تنها نشان‌دهنده عملکرد روی مجموعه داده آموزش است.

حال می‌توانیم نمودار Return-L را رسم کنیم:

1Trader.PlotRs()

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

اسیلاتور استوکستیک

به این ترتیب، مشاهده می‌کنیم که این استراتژی به ازای تمامی Lها سودده است. همچنین، کمترین و بیشترین میانگین درصد سود روزانه به ترتیب مربوط به L=2 و L=80 است. توجه داشته باشید که L=41 نیز اختلاف ناچیزی با L=80 دارد. نکته مهم دیگر که باید به آن توجه کرد، اهمیت Ld است. طول میانگین متحرک ساده مربوط به خط D در سیگنال حاصل اثرگذار است، به همین دلیل با تغییر Ld نمودار فوق نیز تغییر خواهد کرد.

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

1Trader.PlotValue('Train')
2Trader.PlotValue('Test')

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

اندیکاتور استوکاستیک در پایتون

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

ربات معامله‌گر رمزارز با استفاده از اسیلاتور استوکستیک

به این ترتیب، مشاهده می‌کنیم که ربات روی مجموعه داده آموزش به میانگین درصد سود روزانه 0.2915 % رسیده که نسبت به رسد خود نماد اندکی بهتر است. روی مجموعه داده آزمایش نیز ربات به میانگین درصد سود روزانه 0.1201 % رسیده است که شاید در نگاه اول مناسب نباشد، اما با در نظر گرفتن عملکرد نماد در مجموعه داده آزمایش، می‌توان به این نتیجه رسید که ربات با توجه به شرایط موجود، عملکرد مناسب خود را حفظ کرده است. اگر نسبت میانگین درصد سود روزانه ربات با به میانگین درصد سود نماد حساب کنیم، خواهیم داشت:

$$\begin{aligned}
&\text { Train }=\frac{0.2915}{0.2526}=1.15 \\
&\text { Test }=\frac{0.1201}{0.1161}=1.03
\end{aligned}$$

به این ترتیب، عملکرد مثبت ربات قابل مشاهده است.

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

1Trader.PlotSignal('Train')
2Trader.PlotSignal('Test')

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

ربات معامله‌گر رمزارز با استفاده از اسیلاتور استوکستیک

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

اسیلاتور استوکاستیک

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

نکته مهم دیگری که باید به آن پرداخت، نرخ بُرد (Win Rate) است. این معیار نشان‌دهنده نسبت تعداد معاملات برنده به کل معاملات است. برای محاسبه این معیار می‌توانیم یک تابع ایجاد کنیم که در ورودی دیکشنری مربوط به نقاط خرید و نقاط فروش را دریافت می‌کند:

1def WR(Buys:dict, Sells:dict):

حال، یک متغیر برای ذخیره تعداد معاملات و تعداد معاملات موفق ایجاد می‌کنیم:

1def WR(Buys:dict, Sells:dict):
2    nT = 0
3    nW = 0

اکنون یک حلقه ایجاد می‌کنیم و تعداد معاملات را به‌روز (Update) می‌کنیم:

1def WR(Buys:dict, Sells:dict):
2    nT = 0
3    nW = 0
4    for b, s in zip(Buys['Price'], Sells['Price']):
5        nT += 1
6        if s > b:
7            nW += 1

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

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

بدین صورت، این تابع کامل می‌شود. این تابع را در انتهای متد Trade استفاده می‌کنیم:

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

به این ترتیب، این متد در هر بار اجرا، نرخ بُرد را نیز برخواهد گرداند. توجه داشته باشید که به دلیل تغییر در خروجی‌های متد Trade باید در مواردی که این متد فراخوانی شده، اصلاحاتی انجام شود تا شاهد بروز خطا در کد نباشم.

اکنون برای دریافت نرخ بُرد می‌توانیم بنویسیم:

1_, _, _, _, _, _, trWR, _ = Trader.Trade(Trader.trDF, Trader.L)
2_, _, _, _, _, _, teWR, _ = Trader.Trade(Trader.teDF, Trader.L)
3
4print(f'Train Win Rate: {round(trWR, 2)} %')
5print(f'Test Win Rate:  {round(teWR, 2)} %')

پس از اجرا خواهیم داشت:

Train Win Rate: 47.66 %
Test Win Rate:  37.25 %

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

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

جمع‌بندی

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

  1. اندیکاتور استوکستیک RSI (Stochastic RSI) چیست و چه مزایایی دارد؟
  2. چگونه از انجام معاملات فراوان توسط ربات جلوگیری کنیم؟
  3. چرا پیاده‌سازی متدهای رسم نمودار به شکل متد، می‌تواند بهتر از پیاده‌سازی آن‌ها به شکل تابع باشد؟
  4. کد ربات را به‌گونه‌ای تغییر دهید که علاوه بر بهینه‌سازی، مقدار را نیز بهینه کند.
  5. تابع بهینه‌ساز Brute را از کتابخانه Scipy مطالعه کرده و شباهت آن به فرایند پیاده‌سازی شده در برنامه را بیابید.
بر اساس رای ۲۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
مجله فرادرس
نظر شما چیست؟

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