انجام معامله، یک فرایند پیچیده است که نیاز به توانایی و آموزش بالایی دارد. همچنین طراحی یک سیستم معاملاتی درست، پایبندی به آن و دخالت ندادن احساسات در معاملات نیز از اهمیت بسیار بالایی برخودار هستند. بخش زیادی از مشکل معامله‌گران نتیجه ضعف در رعایت این موارد است. ربات‌های معامله‌گر (Autotrader Bot) می‌توانند با یادگیری رفتار بازار، تحلیل داده، با سرعت و دقت بسیار بالاتری عمل کنند. به همین دلیل، استفاده از علم داده (Data Science)، هوش مصنوعی (Artificial Intelligence) و علم تحلیل تکنیکال (Technical Analysis) در کنار هم، می‌تواند باعث انجام معاملات بهینه و سودده شود. در این مطلب، با ساخت ربات معامله گر رمزارز آشنا می‌شویم.

به دلیل استفاده از ربات‌ها از قوانین، روابط و روش کارهای کاملاً مشخص و روشن، به معاملات انجام شده توسط آن‌ها، معاملات الگوریتمی (Algorithmic Trading) گفته می‌شود. در این مطلب قصد داریم با استفاده از اندیکاتور میانگین متحرک ساده (Simple Moving Average | SMA)، یک سیگنال ایجاد کرده و براساس آن معامله انجام دهیم و سود حاصل را بیشینه کنیم. برای آشنایی با میانگین متحرک ساده می‌توانید به مطلب «میانگین متحرک چیست؟ + پیاده سازی Moving Average در پایتون» مراجعه کنید.

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

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

import numpy as np
import pyswarm as ps
import pandas_datareader as pdt
import matplotlib.pyplot as plt

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

  1. کار با آرایه‌ها و محاسبات برداری
  2. بیشینه‌سازی سود ربات با استفاده از الگوریتم بهینه‌سازی ازدحام ذرات (Particle Swarm Optimization | PSO)
  3. دریافت داده مربوط به قیمت نمادها
  4. رسم نمودار

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

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

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

class smaLR:

حال یک متد (Method) سازنده ایجاد می‌کنیم:

    def __init__(self):

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

    def __init__(self):
        self.TrainLog = []

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

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

حال ورودی‌های گرفته شده را داخل شیء (self) ذخیره می‌کنیم تا در صورت نیاز استفاده کنیم.

    def GetData(self, Ticker:str, Start:str, End:str):
        self.Ticker = Ticker
        self.Start = Start
        self.End = End

حال می‌توانیم مجموعه داده را در قالب یک دیتافریم (Data Frame) دریافت کرده و داخل شیء ذخیره کنیم:

    def GetData(self, Ticker:str, Start:str, End:str):
        self.Ticker = Ticker
        self.Start = Start
        self.End = End
        self.DF = pdt.DataReader(Ticker,
                                 data_source='yahoo',
                                 start=Start,
                                 end=End)

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

  1. تاریخ
  2. بیشترین قیمت معامله‌شده در روز (High)
  3. کمترین قیمت معامله‌شده در روز (Low)
  4. قیمت آغازین (Open)
  5. قیمت پایانی (Close)
  6. حجم معاملات (Volume)
  7. قیمت پایانی تعدیل‌شده (Adj Close)

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

    def GetData(self, Ticker:str, Start:str, End:str):
        self.Ticker = Ticker
        self.Start = Start
        self.End = End
        self.DF = pdt.DataReader(Ticker,
                                 data_source='yahoo',
                                 start=Start,
                                 end=End)
        self.DF.drop(labels=['High', 'Low', 'Volume', 'Adj Close'],
                     axis=1,
                     inplace=True)

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

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

    def ProcessData(self, L:int):

حال مانند آنچه قبلاً انجام شد، طول میانگین متحرک ساده را در شیء ذخیره می‌کنیم:

    def ProcessData(self, L:int):
        self.L = L

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

    def ProcessData(self, L:int):
        self.L = L
        self.DF[f'SMA({L})'] = self.DF['Close'].rolling(L).mean()

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

    def ProcessData(self, L:int):
        self.L = L
        self.DF[f'SMA({L})'] = self.DF['Close'].rolling(L).mean()
        self.DF[f'Log-Ratio({L})'] = np.log(self.DF['Close'] / self.DF[f'SMA({L})'])

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

نسبت قیمت به میانگین متحرک ساده می‌تواند معیار بهتری باشد، زیرا یک «اسیلاتور» (Oscillator) است، اما حول مقدار ۱ نوسان خواهد کرد. برای رفع این مشکل می‌توانیم مقادیر خروجی را منهای ۱ کنیم. از طرفی این معیار قرینه نیست. برای مثال، دو حالت را در نظر بگیرید که در یکی قیمت ۲ برابر میانگین متحرک ساده و در دیگری میانگین متحرک ۲ برابر قیمت است. در این دو حالت مقدار نسبت خواهد بود:

$$ \begin{aligned}
R_{1} &=\frac{ { Close }}{S M A}-1=\frac{2 \times S M A}{S M A}-1=1 \\
R_{2} &=\frac{C l o s e}{S M A}-1=\frac{C l o s e}{2 \times C {lose}}-1=-0.5
\end{aligned} $$

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

$$ \begin{aligned}
L R_{1} &=\log \left(\frac{C {lose}}{S M A}\right)=\log \left(\frac{2 \times S M A}{S M A}\right)=\log 2=0.301 \\
L R_{2} &=\log \left(\frac{C l o s e}{S M A}\right)=\log \left(\frac{Close}{2 \times C {lose}}\right)=\log 0.5=-0.301
\end{aligned} $$

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

فیلم آموزشی مرتبط

نیاز است ستون دیگری نیز در مجموعه داده ایجاد شود که قیمت اولین فرصت معامله ممکن را نشان دهد. برای مثال اگر در انتهای روز معاملاتی $$n$$ سیگنال خرید دریافت کردیم، در ابتدای روز $$n+1$$ می‌توانیم خرید کنیم، پس قیمت Open روز بعد، به عنوان اولین فرصت معامله امروز تعیین می‌شود که خواهیم داشت:

    def ProcessData(self, L:int):
        self.L = L
        self.DF[f'SMA({L})'] = self.DF['Close'].rolling(L).mean()
        self.DF[f'Log-Ratio({L})'] = np.log(self.DF['Close'] / self.DF[f'SMA({L})'])
        self.DF['FP'] = self.DF['Open'].shift(-1)

به این ترتیب تمامی ستون‌های مورد نیاز محاسبه و اضافه شدند. با توجه به این‌که برای برخی روزها مقدار میانگین متحرک و برای روز آخر مقدار قیمت آغازین فردا را نداریم، برای این ستون‌ها مقدار NaN یا Not a Number قرار داده می‌شود که مناسب نیست، به همین دلیل سطرهای شامل NaN را حذف (Drop) می‌کنیم:

    def ProcessData(self, L:int):
        self.L = L
        self.DF[f'SMA({L})'] = self.DF['Close'].rolling(L).mean()
        self.DF[f'Log-Ratio({L})'] = np.log(self.DF['Close'] / self.DF[f'SMA({L})'])
        self.DF['FP'] = self.DF['Open'].shift(-1)
        self.DF.dropna(inplace=True)

تا به اینجا مجموعه داده آماده است. اقدام دیگری که باید انجام شود، محاسبه اندازه مجموعه داده و ذخیره آن است:

    def ProcessData(self, L:int):
        self.L = L
        self.DF[f'SMA({L})'] = self.DF['Close'].rolling(L).mean()
        self.DF[f'Log-Ratio({L})'] = np.log(self.DF['Close'] / self.DF[f'SMA({L})'])
        self.DF['FP'] = self.DF['Open'].shift(-1)
        self.DF.dropna(inplace=True)
        self.nD = len(self.DF)

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

$$\operatorname{Signal} l_{t}=p_{0}+p_{1} \times L R_{t}=p_{0}+p_{1} \times \log \left(\frac{C {los} e_{t}}{S M A_{t}}\right) $$

به این ترتیب، تنها دو پارامتر وجود خواهد داشت. برای متد خواهیم داشت:

    def Trade(self, Parameters:np.ndarray):

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)

این ۴ آرایه به ترتیب موارد زیر را در خود ذخیره می‌کنند:

  1. پول آزاد موجود در هر لحظه از زمان
  2. سهام موجود در هر لحظه از زمان
  3. ارزش پرتفوی (Portfolio Value) ایجاد شده در هر لحظه از زمان
  4. سیگنال ایجادشده در هر لحظه از زمان

دو «دیکشنری» (Dictionary) دیگر نیز برای ذخیره روز‌هایی که خرید یا فروش انجام‌شده ایجاد می‌کنیم:

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}

حال یک «سرمایه اولیه» (Budget) و تعداد سهام اولیه را تعیین می‌کنیم:

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0
        for i in range(self.nD):

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0
        for i in range(self.nD):
            fp = self.DF['FP'][i]
            lr = self.DF[f'Log-Ratio({self.L})'][i]
            signal = Parameters[0] + Parameters[1] * lr

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

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

توجه داشته باشید که این شیوه از معامله، به هیچ وجه توصیه نمی‌شود و صرفاً برای سادگی الگوریتم به این شکل در نظر گرفته شده است.

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0
        for i in range(self.nD):
            fp = self.DF['FP'][i]
            lr = self.DF[f'Log-Ratio({self.L})'][i]
            signal = Parameters[0] + Parameters[1] * lr
            if signal > 0 and share == 0:
                share = money / fp
                money = 0

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0
        for i in range(self.nD):
            fp = self.DF['FP'][i]
            lr = self.DF[f'Log-Ratio({self.L})'][i]
            signal = Parameters[0] + Parameters[1] * lr
            if signal > 0 and share == 0:
                share = money / fp
                money = 0
                Buys['Time'].append(i)
                Buys['Price'].append(fp)

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0
        for i in range(self.nD):
            fp = self.DF['FP'][i]
            lr = self.DF[f'Log-Ratio({self.L})'][i]
            signal = Parameters[0] + Parameters[1] * lr
            if signal > 0 and share == 0:
                share = money / fp
                money = 0
                Buys['Time'].append(i)
                Buys['Price'].append(fp)
            elif signal < 0 and share > 0:
                money = share * fp
                share = 0
                Sells['Time'].append(i)
                Sells['Price'].append(fp)
فیلم آموزشی مرتبط

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0
        for i in range(self.nD):
            fp = self.DF['FP'][i]
            lr = self.DF[f'Log-Ratio({self.L})'][i]
            signal = Parameters[0] + Parameters[1] * lr
            if signal > 0 and share == 0:
                share = money / fp
                money = 0
                Buys['Time'].append(i)
                Buys['Price'].append(fp)
            elif signal < 0 and share > 0:
                money = share * fp
                share = 0
                Sells['Time'].append(i)
                Sells['Price'].append(fp)
            Moneys[i] = money
            Shares[i] = share
            Values[i] = money + share * fp
            Signals[i] = signal

به این ترتیب، به ازای هر روز، شروط بررسی می‌شود، در صورت نیاز معامله می‌شود و در نهایت اطلاعت تاریخچه ذخیره می‌شود. معیار مهمی که در انتهای این تابع محاسبه شود، میانگین سود روزانه است. فرض کنید که در ابتدای معاملات ارزش پرتفوی ربات $$v_1$$ اشت و پس از $$n$$ روز، ارزش آن به $$v_2$$ رسیده است. در این شرایط، میانگین درصد سود هر روز به شکل زیر خواهد بود:

$$ v_{2}=v_{1} \times\left(1+\frac{r}{100}\right)^{n-1} $$

با توجه به این‌که در $$n$$ روز معاملاتی، تنها $$n-1$$ بار امکان دریافت سود دارد، باید توان عبارت ضریب $$n-1$$ باشد.

با حل رابطه فوق، مقدار $$r$$ خواهد بود:

فرمول

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

    def Trade(self, Parameters:np.ndarray):
        Moneys = np.zeros(self.nD)
        Shares = np.zeros(self.nD)
        Values = np.zeros(self.nD)
        Signals = np.zeros(self.nD)
        Buys = {'Time':[], 'Price':[]}
        Sells = {'Time':[], 'Price':[]}
        money = 1
        share = 0
        for i in range(self.nD):
            fp = self.DF['FP'][i]
            lr = self.DF[f'Log-Ratio({self.L})'][i]
            signal = Parameters[0] + Parameters[1] * lr
            if signal > 0 and share == 0:
                share = money / fp
                money = 0
                Buys['Time'].append(i)
                Buys['Price'].append(fp)
            elif signal < 0 and share > 0:
                money = share * fp
                share = 0
                Sells['Time'].append(i)
                Sells['Price'].append(fp)
            Moneys[i] = money
            Shares[i] = share
            Values[i] = money + share * fp
            Signals[i] = signal
        Return = 100 * ((Values[-1] / Values[0])**(1 / (self.nD - 1)) - 1)
        return Moneys, Shares, Values, Signals, Buys, Sells, Return

به این ترتیب متد Trade کامل می‌شود. حال، نیاز به متد دیگری داریم تا بتواند مقدار تابع هزینه (Loss Function) را برگرداند. توجه داشته باشید که تابع هزینه باید کمینه‌سازی (Minimization) شود، در حالی که خروجی متد Trade مقدار سود است که باید بیشینه‌سازی (Maximization) شود. برای رفع این مشکل، تعریف می‌کنیم:

$$\operatorname{Loss}(P)=-\operatorname{Return}(P) $$

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

    def Loss(self, P:np.ndarray):
        Return = self.Trade(P)[-1]
        return -Return

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

    def Loss(self, P:np.ndarray):
        Return = self.Trade(P)[-1]
        self.TrainLog.append(Return)
        return -Return

حال این متد نیز کامل است.

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

    def Train(self, MaxIteration:int, SwarmSize:int):

حال حد بالا و پایین پارامترها را تعیین می‌کنیم که اعداد $$+1$$ و $$-1$$ هستند:

    def Train(self, MaxIteration:int, SwarmSize:int):
        lb = -1 * np.ones(2)
        ub = +1 * np.ones(2)

حال الگوریتم PSO را روی تابع هزینه اجرا و خروجی‌ها را تعریف می‌کنیم:

    def Train(self, MaxIteration:int, SwarmSize:int):
        lb = -1 * np.ones(2)
        ub = +1 * np.ones(2)
        self.P, BestLoss = ps.pso(self.Loss,
                                  lb,
                                  ub,
                                  swarmsize=SwarmSize,
                                  maxiter=MaxIteration)

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

    def Train(self, MaxIteration:int, SwarmSize:int):
        lb = -1 * np.ones(2)
        ub = +1 * np.ones(2)
        self.P, BestLoss = ps.pso(self.Loss,
                                  lb,
                                  ub,
                                  swarmsize=SwarmSize,
                                  maxiter=MaxIteration)
        BestReturn = -BestLoss
        print('_'*50)
        print('Optimization Result:')
        print(f'\tBest Parameters: {self.P}')
        print(f'\tBest Return: {BestReturn} %')
        print('_'*50)

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

ایجاد شیء و فراخوانی متدها

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

Trader = smaLR()

Trader.GetData('BTC-USD', '2019-01-01', '2022-01-01')

Trader.ProcessData(60)

Trader.Train(50, 60)

به این ترتیب:

  1. ابتدا ربات ایجاد می‌شود.
  2. داده‌های 3 سال بین $$2019-01-01$$ و $$2022-01-01$$ برای رمزارز بیتکوین (Bitcoin) دریافت می‌شود.
  3. یک میانگین متحرک ساده ۶۰ روزه بر روی داده محاسبه و سپس لگاریتم نسبت قیمت به میانگین متحرک ساده محاسبه می‌شود.
  4. در نهایت نیز ۶۰ ذره ایجاد و به تعداد ۵۰ مرحله بر روی مجموعه داده آموزش داده می‌شود.

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

__________________________________________________

Optimization Result:
        Best Parameters: [-0.00912918  0.18591647]
        Best Return: 0.29836380166610166 %
__________________________________________________

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

$$r_{30}=100 \times\left(\left(1+\frac{r_{1}}{100}\right)^{30}-1\right) \%=9.347 \% $$

به این ترتیب، انتظار ماهیانه ما از ربات، ۹ درصد سود به صورت ماهیانه است. توجه داشته باشید که این محاسبات به صورت ساده و با پیش‌فرض‌هایی انجام شده است که در دنیای واقعی ممکن است برآورده نشود.

این نتیجه با پارامترهای زیر حاصل شده است:

$$\begin{aligned}
&p_{0}=-0.0091 \\
&p_{1}=+0.1859
\end{aligned}$$

حال اگر این اعداد را در رابطه قرار دهیم، تابع سیگنال حاصل می‌شود:

$$\operatorname{Signal}_{t}=p_{0}+p_{1} \times L R_{t}=-0.0091+0.1859 \times \log \left(\frac{\operatorname{Close} _{t}}{S M A_{t}}\right) $$

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

plt.plot(Trader.TrainLog, ls='-', lw=0.8, c='crimson')
plt.title('Model Return Over Function Evaluations')
plt.xlabel('Function Evaluation')
plt.ylabel('Average Daily Return (%)')
plt.show()

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

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

به این ترتیب، مشاهده می‌کنیم که ربات از سودهای بسیار پایین و حتی منفی، به مقدار تقریباً ۰٫۲۹۸۳ نزدیک می‌شود، هرچند که برخی ذرات نتوانسته‌اند به این نقطه همگرا (Converge) شوند. توجه داشته باشید که تابع Trade به تعداد ۳۰۰۰ بار اجرا شده است، به عبارت دیگر، ربات ۳۰۰۰ ترکیب مختلف از پارامترها را تا انتهای این دوره امتحان کرده است و دارای تجربه معامله به اندازه ۹۰۰۰ سال است!

حال می‌توانیم برای نمایش نتایج، یک بار دیگر متد Trade را با پارامترهای بهینه اجرا کنیم:

_, _, Values, Signals, Buys, Sells, Return = Trader.Trade(Trader.P)

به این ترتیب، تاریخچه مربوط به بهترین عملکرد حاصل می‌شود.

ابتدا می‌توانیم نمودار «نیمه-لگاریتمی» (Semi-Logarithm) مربوط به قیمت، میانگین متحرک ساده در یک نمودار و ارزش پرتفوی را به همراه روند رشد سرمایه را در نمودار دیگر در مقابل هم رسم کنیم:

plt.subplot(1, 2, 1)
plt.semilogy(Trader.DF['Close'].to_numpy(), ls='-', lw=0.8, c='k', label='Close')
plt.semilogy(Trader.DF['SMA(60)'].to_numpy(), ls='-', lw=1, c='b', label='SMA(60)')
plt.title('Price Over Time')
plt.xlabel('Time (Day)')
plt.ylabel('Price ($)')
plt.legend()

plt.subplot(1, 2, 2)
MeanValue = (1 + Return / 100)**np.arange(start=0, stop=Values.size, step=1)
plt.semilogy(Values, ls='-', lw=0.8, c='crimson', label='Real Values')
plt.semilogy(MeanValue, ls='--', lw=1, c='k', label=f'Mean Values (Return: {round(Return, 4)} %)')
plt.title('Value Over Time')
plt.xlabel('Time (Day)')
plt.ylabel('Value')
plt.legend()

plt.show()

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

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

مشاهده می‌کنیم که نمودار ارزش پرتفوی در برخی بازه‌ها مسطح شده است که به معنی خروجی از سهم است. با تطبیق دو نمودار با یکدیگر، متوجه می‌شویم که تقریباً در بازه‌هایی که قیمت زیر میانگین متحرک ساده بوده، ربات از سهم خارج شده است. به نوعی، ربات به صورت خودآموز یاد گرفته است که حرکت قیمت به زیر میانگین متحرک نمایی، سیگنالی برای فروش و برعکس است. نکته دیگری که وجود دارد، این است که ربات با گرفتن ۱ واحد سرمایه در ابتدا، آن را در طول ۱۰۳۸ روز به ۲۱٫۹۶ واحد تبدیل کرده است. نماد بیتکوین در این مدت، از قیمت ۳۸۵۹ دلار به ۴۷۶۸۶ دلار رشد کرده است. می‌توان نشان داده که عملکرد ربات ۱٫۸۴ برابر بهتر بوده است:

$$\frac{\frac{21.96-1}{1}}{\frac{47686-3859}{3859}}=1.84$$

به این ترتیب، مشهود است که انجام معاملات در این بازه توسط ربات، به اندازه ٪۸۴ سود سرمایه‌گذاری را افزایش داده است. حال می‌توانیم در نمودار دیگری، نمودار قیمت، میانگین متحرک نمایی و در زیر آن نمودار سیگنال را رسم کنیم:

plt.subplot(3, 1, (1, 2))
plt.semilogy(Trader.DF['Close'].to_numpy(), ls='-', lw=0.8, c='k', label='Close')
plt.semilogy(Trader.DF['SMA(60)'].to_numpy(), ls='-', lw=1, c='b', label='SMA(60)')
plt.title('Price Over Time')
plt.ylabel('Price ($)')
plt.legend()

plt.subplot(3, 1, 3)
Z=np.zeros_like(Signals)
plt.plot(Signals, ls='-', lw=0.7, c='k')
plt.fill_between(np.arange(Signals.size), Signals, Z, where=(Signals > Z), color='lime', alpha=0.9, label='Buy Signal')
plt.fill_between(np.arange(Signals.size), Signals, Z, where=(Signals < Z), color='crimson', alpha=0.9, label='Sell Signal')
plt.axhline(ls='-', lw=1, c='b')
plt.title('Signal Over Time')
plt.xlabel('Time (Day)')
plt.ylabel('Value')
plt.legend()

plt.show()

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

میانگین متحرک

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

نقاط خرید و فروش ربات

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

Return (%) L (Day)
0.2478 5
0.2406 8
0.2556 13
0.2661 21
0.3005 34
0.2762 40
0.3003 45
0.2973 55
0.2983 60
0.2905 65
0.2976 70
0.2681 89

به این ترتیب مشاهده می‌کنیم که یک میانگین متحرک ساده با طول بازه حدود ۳۴ روز می‌توان انتخاب بهتری باشد.

جمع‌بندی

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

  1. چگونه می‌توان از یک میانگین متحرک نمایی (Exponential Moving Average | EMA) به جای میانگین متحرک ساده استفاده کرد؟ حدس می‌زنید بازده ربات چه مقدار و در چه جهت تغییر کند؟
  2. برنامه را روی اندازه نمادها اجرا کنید و بازده‌ها را با یکدیگر مقایسه کنید. تفاوت از کجا حاصل می‌شود؟ آیا شباهتی بین پارامترهای نهایی وجود دارد؟
  3. چگونه می‌توانیم معیاری برای نشان دادن مزیت ربات نسبت به Hold کردن طراحی کنیم؟
  4. برای برخی نمادها امکان انجام معاملات Short وجود دارد. چگونه می‌توان این امکان را در برنامه ایجاد کرد؟ با اضافه شدن این امکان، بازده ربات به چه شکل تغییر می‌کند؟
  5. اگر بخواهیم بین دو نماد که ربات برای هر دو سیگنال خرید گرفته است، یکی را انتخاب کنیم، کدامیک باید انتخاب شود؟ چرا؟
  6. یک معامله‌گر باید علاوه بر بیشینه‌سازی سود، ریسک معاملات را نیز تا جای ممکن کاهش دهد. چگونه می‌توان ریسک هر نماد را محاسبه کرد؟
  7. نتایج ربات ایجاد شده، تنها بر روی داده‌های آموزش (Train Dataset) محاسبه شد. چگونه می‌توان یک مجموعه داده آزمایش (Test Dataset) ایجاد کرد؟ این مجموعه باید از کدام بخش مجموعه داده انتخاب شود؟
  8. اگر بخواهیم از دو میانگین متحرک ساده با طول‌های متفاوت برای محاسبه سیگنال استفاده کنیم، معادله سیگنال به چه صورت خواهد بود؟ حدس می‌زنید بازده ربات در این شرایط به چه صورت تغییر خواهد کرد؟

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

بر اساس رای ۹ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

«سید علی کلامی هریس»، دانشجوی سال چهارم داروسازی دانشگاه علوم پزشکی تهران است. او در سال 1397 از دبیرستان «پروفسور حسابی» تبریز فارغ‌التحصیل شد و هم اکنون در کنار تحصیل در حوزه دارو‌سازی، به فعالیت در زمینه برنامه‌نویسی، یادگیری ماشین و تحلیل بازارهای مالی با استفاده از الگوریتم‌های هوشمند می‌پردازد.

یک نظر ثبت شده در “ساخت ربات معامله گر رمزارز با میانگین متحرک ساده در پایتون — بخش اول

نظر شما چیست؟

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

مشاهده بیشتر