Сидя дома в течение года, человек начинает исследовать множество новых и разных увлечений, чтобы попытаться сохранить свое здравомыслие. Лично я решил помочь своим друзьям найти финансовый крах с помощью бота Discord для торговли акциями!

В этом путешествии я решил создать своего нового друга по раздору, альпаку Диспаку!

Я расскажу о своих шагах по созданию этой потенциально ужасной идеи, чтобы другие также могли делиться своим богатством со своими друзьями без какой-либо ответственности (пока… Оставайтесь с нами для части 2).

⚠️ ПРЕДУПРЕЖДЕНИЯ И ИНФОРМАЦИЯ⚠️

В этом руководстве будет использоваться бумажная учетная запись на https://app.alpaca.markets/, поэтому реальные деньги не задействованы.

ЕСЛИ ВЫ РЕШИТЕ ​​ИСПОЛЬЗОВАТЬ РЕАЛЬНЫЕ ДЕНЬГИ, помните о том, что разрешение другим людям торговать ценными бумагами от вашего имени может иметь юридические и налоговые последствия. Я не знаю, каковы эти разветвления; знания о том, что они существуют, достаточно, чтобы удержать меня от предоставления другим людям такого рода контроля. Я рекомендую вам тоже не давать другим людям контроль над вашими деньгами. Любые действия, которые вы предпринимаете вне этой рекомендации, являются вашим собственным проклятым решением.

Вот несколько полезных ресурсов в целом:

Шаг 0 — Настройка среды

В этом руководстве я не буду вдаваться в подробности моей настройки разработки, но я расскажу о том, как я размещаю этого бота, и о моем процессе внесения изменений в «производство».

Caprover в DigitalOcean
Caprover — это платформа для развертывания, которую я использовал в течение последних нескольких месяцев для упрощения самостоятельного размещения некоторых программ с открытым исходным кодом, которые я использую (bitwarden и т. д.). ), а также небольшие личные проекты.

Я нашел Caprover, так как использую DigitalOcean (реферальная ссылка 💛) в качестве хостинг-провайдера. На их торговой площадке есть простой в развертывании образ, который поможет вам начать работу довольно быстро. Просто убедитесь, что у вас есть готовое доменное имя.

В этом руководстве мы будем развертывать Dockerfiles в Caprover, поэтому любые другие варианты хостинга или локальные запуски Docker будут работать так же хорошо.

Исходная файловая структура
Мы начнем этот проект со следующей файловой структуры.

.
├── Dockerfile
├── README.md
├── requirements.txt
└── src
    └── bot.py

Это соответствует нашему Docker, выглядящему так.

FROM gorialis/discord.py
WORKDIR /bot
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY /src .
ENV DISCORD_TOKEN=[WE WILL GET THIS SOON]
ENV ALPACA_KEY_ID=[WE WILL GET THIS SOON]
ENV ALPACA_KEY_SECRET=[WE WILL GET THIS SOON]
CMD ["python", "./bot.py"]

Базовый образ для этого Dockerfile был создан с учетом библиотеки discord.py.

С этой файловой структурой и установленным докером все, что нам нужно сделать, чтобы начать работу, — это просто запустить следующую команду.

$ docker build -t dispacapy .
$ docker run dispacapy

Каждый раз, когда мы добавляем изменение, мы просто перезапускаем docker build и docker run и сможем увидеть наши изменения вживую!

Шаг 1 — Настройка бота Discord

В конце этого шага у нас будет пользователь-бот Discord, которого мы можем пригласить на наш сервер, а также токен приложения для добавления в наш файл Dockerfile.

Первое, что нам нужно сделать, это настроить и подготовить нашего бота для разногласий.

Обратите внимание, что я пишу это руководство после того, как были представлены команды косой черты, но до того, как я действительно изучил, как их использовать.

Поскольку мы создаем только чат-бота без каких-либо специальных разрешений, нам не нужно беспокоиться о грязных вещах OAuth.

Начнем с перехода на https://discord.com/developers. Я предполагаю, что у вас, читатель, есть дискорд-аккаунт. Если нет, то вперед и сделать один!

После входа в систему вы должны найти страницу, похожую на этот снимок экрана:

Здесь мы нажмем New Application и назовем наше замечательное новое приложение. Это приведет нас к странице общей информации.

Чтобы сделать наше приложение настоящим ботом, нам нужно нажать на боковую панель Bot, а затем Build-A-Bot.ВЫ ДОЛЖНЫ СДЕЛАТЬ ЭТО, ЧТОБЫ СОЗДАТЬ ПОЛЬЗОВАТЕЛЯ-БОТА.

Это необратимое действие, и Discord обязательно скажет вам об этом. Как только наш бот будет создан, мы можем получить DISCORD_TOKEN для нашего Dockerfile.

СДЕЛАТЬ. НЕТ. ТОЛКАТЬ. ВАШ. ДИСКОРД. ЖЕЛЕЗ. К. ГИТХАБ

Они найдут это. Они расскажут вам об этом. Они его отзовут.

Наконец, последний шаг — пригласить вашего бота на ваш сервер. Это можно сделать, перейдя в пункт бокового меню OAuth2. Мы можем запустить нашего бота, не требуя от нас настройки аутентификации пользователя и перенаправления. Это значительно упрощает нашу жизнь.

Мы выберем область bot OAuth, которая создаст ссылку-приглашение, которую мы можем использовать для добавления нашего бота на сервер. Прежде чем мы добавим нашего бота, мы хотим убедиться, что у нас также выбраны наши разрешения.

Для этого проекта нам понадобятся только Send Messages, Attach Files и Add Reactions. Когда все будет готово, перейдите по созданной вами ссылке, и вы сможете добавить своего бота на любой сервер, который дает вам на это разрешение.

https://discord.com/api/oauth2/authorize?client_id=myclientid12345&permissions=34880&scope=bot -- THIS IS NOT A REAL URL

Шаг 2. Настройка учетной записи Alpaca

В конце этого шага у нас будет бумажный счет Альпака, который позволит нам торговать акциями, а также получать исторические данные. Их документация по API довольно фантастическая.

Мне нравится Альпака. Легко встать и бежать. Это дает вам бумажный счет для практики ваших алгоритмов, а затем позволяет легко переключиться на реальные деньги.

⚠️БУДЬТЕ ОЧЕНЬ ОСТОРОЖНЫ, ТОРГУЯ СВОИМИ ДЕНЬГАМИ. ⚠️

Когда вы создадите свою учетную запись и подтвердите свой адрес электронной почты, вы окажетесь на потенциально пугающем экране. Мы можем полностью игнорировать это, переключившись на наш бумажный счет. Мы делаем это, нажав на Live Trading и выбрав бумажный торговый счет в раскрывающемся списке.

Как только мы окажемся на нашем бумажном торговом счете, мы можем сгенерировать наш ключ API и добавить его в наш Dockerfile выше. Это делается нажатием кнопки View в поле Your API Keys.

Шаг 3 — Передача данных от Alpaca вам

У нас есть аккаунт бота в Discord. У нас есть торговый счет Альпака. Давайте начнем создавать бота, получающего от него данные. В конце этого шага у нас будет ранняя работа бота, который будет реагировать на команды и давать нам информацию об отдельной акции. Всегда важно быть информированным при торговле.

В этом руководстве не будет затрагиваться вопрос о том, как проводить какой-либо финансовый анализ. Это сложная тема, о которой я не имею права говорить. На Medium, YouTube и других ресурсах есть много других источников, которые научат вас рынкам.

Если мы откроем bot.py, мы сможем начать возвращать некоторую информацию, когда мы ее запрашиваем.

## Discord.py is a well supported wrapper for the Discord API
import discord
from discord.ext import commands
## alpaca_trade_api wraps the Alpaca API
import alpaca_trade_api as tradeapi
## We'll be using matplotlib to generate simple line graphs
import matplotlib.pyplot as plt
## Useful imports to have
import io, os
## Environmental Consts
## These are set in the Dockerfile
DISCORD_TOKEN = os.environ.get("DISCORD_TOKEN")
ALPACA_KEY_ID = os.environ.get("ALPACA_KEY_ID")
ALPACA_KEY_SECRET = os.environ.get("ALPACA_KEY_SECRET")
ALPACA_BASE_URL = 'https://paper-api.alpaca.markets'
## These are parameters to make our future charts prettier 
plt.rcParams.update({'xtick.labelsize' : 'small',
                     'ytick.labelsize' : 'small',
                     'figure.figsize' : [16,9]})
## Connect to your Alpaca account
alpaca_api = tradeapi.REST(ALPACA_KEY_ID,
                           ALPACA_KEY_SECRET,
                           base_url=ALPACA_BASE_URL, 
                           api_version='v2')
## Initialize our Discord bot
## Using a command prefix helps find issued commands 
bot = commands.Bot(command_prefix='>')
## Create our first command
@bot.command()
async def hello_world(context):
    await context.send("Hello Dispaca!")
## Any other commands we want to add should be added here
## Start our bot
bot.run(DISCORD_TOKEN)

Если мы развернем наш bot.py сверху, мы сможем получить нашу первую связь с ботом!

Введите >hello_world на сервер, которым вы делитесь со своим ботом, и вы получите ответ!

Каждая последующая команда, которую мы добавляем, будет следовать той же структуре, что и async def hello_world(). Мы используем декоратор @bot.command() для обозначения функций как команд. Обязательно ознакомьтесь с документацией, чтобы узнать, что еще можно сделать с декораторами.

Теперь, когда мы можем поговорить с Диспакой, давайте начнем вести полезный разговор.

@bot.command()
async def account(context):
    print("Checking account")
. ## Retrieve your Alpaca paper account info
    account_info = alpaca_api.get_account()
    await context.send(f"{account_info}")

Добавление async def account(context) к вашему боту позволит вам использовать команду >account.

При вызове ваш бот обратится к Alpaca API и получит информацию о вашей учетной записи. Это будет некрасиво, но очистка будет для другого раздела. Прежде чем мы двинемся дальше, важно отметить, что именно здесь мы сможем получить нашу покупательную способность, деньги и другие важные ценности.

@bot.command()
async def last_price(context, ticker):
    print(f"Checking the last price of {ticker}")
    
    last_price = alpaca_api.get_last_trade(ticker)
    await context.send(f"Last price for {ticker} was {last_price}")

Здесь мы добавили >last_price, который будет делать именно то, что он говорит, — получить последнюю цену некоторых акций. Например, мы можем отправить >last_price GOOG и получить последнюю цену!

Давайте воспользуемся этим моментом, чтобы поговорить об исключениях. Discord.py будет игнорировать исключения в командах. Код, написанный выше, может легко дать сбой при получении команды, и пользователь может никогда об этом не узнать.

Вот что происходит, когда вы отправляете >last_price goog.

Просмотр выходных журналов из Docker покажет, что именно пошло не так.

Ignoring exception in command last_price:
...
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://data.alpaca.markets/v1/last/stocks/goog
The above exception was the direct cause of the following exception:
...
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: HTTPError: 404 Client Error: Not Found for url: https://data.alpaca.markets/v1/last/stocks/goog

Рыночные данные Alpaca чувствительны к регистру, поэтому поиск goog не даст тех же результатов, что и GOOG.

@bot.command()
async def last_price(context, ticker):
    if isinstance(ticker, str):
        ticker = ticker.upper()
    print(f"Checking the last price of {ticker}")
    try:
        last_price = alpaca_api.get_last_trade(ticker)
        await context.send(f"{ticker} -- ${last_price.price}")
except Exception as e:
    await context.send(f"Error getting the last price: {e}")

Для быстрого смягчения давайте установим все входные данные в верхний регистр. Также мы должны добавить исключение, чтобы мы могли отправить обратно уведомление о сбое.

Теперь мы обрабатываем некоторые ошибки.

Наконец, давайте создадим диаграмму и отправим ее пользователю. Что хорошего в последней цене без какого-либо контекста. Хотя есть несколько разных способов поделиться диаграммой, я решил создать .png с помощью Matplotlib и отправить ее в Discord. Таким образом, нам не нужно беспокоиться о каких-либо других услугах или хостинге.

@bot.command()
async def check(context, ticker):
    print(f"Checking the history of a stock")
    
    ## Make sure the symbol is upper case
    if isinstance(ticker, str):
        ticker = ticker.upper()
    try:
        ## Retrieve the last 100 days of trading data
        bars = alpaca_api.get_barset(ticker, 'day', limit=100)
        bars = bars.df[ticker]
        
        ## This bytes buffer will hole the image we send back
        fig = io.BytesIO()
        ## Grab the last closing price
        last_price = bars.tail(1)['close'].values[0]
        ## Make a chart from the data we retrieved
        plt.title(f"{ticker} -- Last Price ${last_price:.02f}")
        plt.xlabel("Last 100 days")
        plt.plot(bars["close"])
        ## Save the image to the buffer we created earlier
        plt.savefig(fig, format="png")
        fig.seek(0)
## Sending back the image to the user.
        await context.send(file=discord.File(fig, f"{ticker}.png"))
        plt.close()
    except Exception as e:
      await context.send(f"Error getting the stock's data: {e}")

Поскольку это изображение загружается в Discord, мы можем поделиться общедоступной ссылкой с кем угодно.

Шаг 4 — Создание встраивания для общего удобства пользователя

Теперь мы можем получить доступ к большому количеству данных и построить простые графики этих данных. Давайте начнем делать некоторые из этих данных более удобными для просмотра в разногласиях. На этом этапе мы создадим Embed, заполнив его только той информацией, которая нам нужна, и вернем ее обратно пользователю.

Я использовал Discord Embed Sandbox для разработки своих вставок. Существуют и другие инструменты

Давайте посмотрим, как выглядел аккаунт в формате JSON. (Документация по счетам).

Account({   
    'account_blocked': False,
    'account_number': 'PA0T4EI00TV0',
    'buying_power': '400000',
    'cash': '100000',
    'created_at': '2021-03-25T00:17:25.566381Z',
    'currency': 'USD',
    'daytrade_count': 0,
    'daytrading_buying_power': '400000',
    'equity': '100000',
    'id': '246a152e-a15d-433a-b5dc-0abcaca6d656',
    'initial_margin': '0',
    'last_equity': '100000',
    'last_maintenance_margin': '0',
    'long_market_value': '0',
    'maintenance_margin': '0',
    'multiplier': '4',
    'pattern_day_trader': False,
    'portfolio_value': '100000',
    'regt_buying_power': '200000',
    'short_market_value': '0',
    'shorting_enabled': True,
    'sma': '0',
    'status': 'ACTIVE',
    'trade_suspended_by_user': False,
    'trading_blocked': False,
    'transfers_blocked': False})

По ощущениям мы можем уверенно схватить:

  • cash
  • buying_power
  • equity
  • portfolio_value

Если мы обнаружим, что нам нужны другие значения, мы добавим их позже.

def generate_account_embed(account):
    embed=discord.Embed(title="Account Status", 
                       description="Alpaca Markets Account Status", 
                       color=0x47c02c)
    
    embed.add_field(name="Cash", value=f"${account.cash}")
    embed.add_field(name="Buying Power",
                    value=f"${account.buying_power}")
    embed.add_field(name="Portfolio Value",
                    value=f"${account.portfolio_value}")
    embed.add_field(name="Equity", value=f"${account.equity}")
    
    return embed

Здесь мы используем сгенерированный код Python в качестве шаблона для нашей вставки. Мы заполняем его всеми переменными, которые мы передаем.

## Replace the previous instance of this function in bot.py
@bot.command()
async def account(context):
    print(f"Checking account")
    account_info = alpaca_api.get_account()
    account_embed = generate_account_embed(account_info)
    await context.send(embed=account_embed)

Не забудьте заменить функцию account в файле bot.py перед повторным запуском.

Посмотрите, насколько чище и проще стало видеть текущий статус вашей учетной записи.

Шаг 5 — Реакции на торговые подтверждения

Это был долгий путь, но мы, наконец, готовы попросить Dispaca купить для нас ценные бумаги. В конце этого шага мы сможем попросить Dispaca совершить сделку для нас, а также получить подтверждение от нас, прежде чем она отправит эту сделку. Мы бы не хотели случайно купить 100 акций GameStop… верно? 🚀

В случае с нашим ботом давайте посмотрим, как может выглядеть простейшая команда на покупку.

@bot.command()
async def buy(context, ticker, quantity):
    print(f"Ordering {quantity} shares of {ticker}")
    buy_order = alpaca_api.submit_order(ticker, quantity,'buy', 
                                        'market', 'day')
    await context.send(f"{buy_order}")

Выдача >buy GME 100 просто сообщит Альпаке, что мы хотим 100 акций GameStop и купим их.

Ни подтверждения, ни проверки стоимости счета, ничего. Все, что мы получили бы обратно, это подтверждение заказа. Уродливая капля JSON.

Order({   
        'asset_class': 'us_equity',     
        'asset_id': '9eae922d-8d67-4c40-b6ea-faca9b092b89',
        'canceled_at': None,     
        'client_order_id': '6ff64956-0123-4567-887a-632086981208',
        'created_at': '2021-03-24T21:07:28.551801Z',     
        'expired_at': None,
        'extended_hours': False,
        'failed_at': None,
        'filled_at': None,
        'filled_avg_price': None,
        'filled_qty': '0',
        'hwm': None,
        'id': 'd1d08f71-a4a5-45f7-8508-64a86918b74e',     
        'legs': None,     
        'limit_price': None,     
        'notional': None,     
        'order_class': '',     
        'order_type': 'market',     
        'qty': '100',     
        'replaced_at': None,     
        'replaced_by': None,     
        'replaces': None,     
        'side': 'buy',     
        'status': 'accepted',     
        'stop_price': None,     
        'submitted_at': '2021-03-24T21:07:28.54462Z',     
        'symbol': 'GME',     
        'time_in_force': 'day',     
        'trail_percent': None,     
        'trail_price': None,     
        'type': 'market',     
        'updated_at': '2021-03-24T21:07:28.551801Z'})

Покупка ценных бумаг не должна быть легкой, это большая ответственность! Как насчет того, чтобы вместо этого создать вставку, которую мы можем использовать, чтобы показать, как будет выглядеть наш заказ. Затем мы ждем подтверждения заказа через реакцию на эту вставку. Если мы получаем правильную реакцию, мы отдаем приказ, в противном случае мы просто тратим время и делаем вид, что ничего никогда не происходило.

def generate_buy_embed(ticker, quantity, market_price):
    embed = discord.Embed()
    total_cost = int(quantity) * market_price
    embed=discord.Embed(title=f"Buying {ticker}", 
                    description="Review your buy order below.\
                    React with 👍 to confirm in the next 30 seconds")
    embed.add_field(name="Quantity", 
                    value=f"{quantity}", inline=False)
    embed.add_field(name="Per Share Cost", 
                    value=f"${market_price}", inline=False)
    embed.add_field(name="Estimated Cost", 
                    value=f"${total_cost}", inline=False)
    embed.add_field(name="In Force", 
                    value="Good Until Cancelled", inline=False )
    return embed

Выше мы начинаем создавать то, как мы хотим, чтобы наша вставка выглядела. Мы хотим, чтобы информация быстро усваивалась и на нее было легко реагировать.

@bot.command()
async def buy(context, ticker, quantity):
    if isinstance(ticker, str):
        ticker = ticker.upper()
    ## Lets get some supporting information about this stock
    try:
        last_trade = alpaca_api.get_last_trade(ticker)
        last_price = last_trade.price
    except Exception as e:
        await context.send(f"Error getting the last price: {e}")
        return
    ## This is the embed we set up earlier
    buy_embed = generate_buy_embed(ticker, quantity, last_price)
    await context.send(embed=buy_embed)
    ## We only care about the user who started the trade
    def check(reaction, user):
        return user == context.message.author
    try:
        ## Wait for a reaction event. 30 second timeout.
        reaction, user = await bot.wait_for("reaction_add", 
                         timeout=30.0, check=check)
    except TimeoutError:
        await context.send("Cancelling the trade. No activity")
    else:
        if str(reaction.emoji) == '👍':
            await context.send("Executing on the trade")
            ## Placing the actual order 
            placed_order = alpaca_api.submit_order(symbol=ticker, 
                           qty=quantity,
                           side='buy', 
                           type='market', 
                           time_in_force='gtc')
            await context.send(f"Order ID: {placed_order.id}")
        else:
            await context.send("Cancelling Order")

Это немного более сложная команда, но это очень круто!

Когда Dispaca увидит >buy GME 100, он выйдет и возьмет цену последней сделки. Мы будем использовать эту цену для оценки стоимости нашего заказа.

await bot.wait_for("reaction_add", timeout=30.0, check=check)

Здесь мы говорим нашему боту слушать реакцию в течение 30 секунд. Если пользователь, начавший заказ, ответит 👍, заказ продолжится! Отправляем ордер на покупку по текущей рыночной цене. С другой стороны, если пользователь отреагирует любым другим смайликом или пройдет 30 секунд, заказ будет отменен.

Это дает нам, по крайней мере, представление о том, сколько мы потратим, и дополнительное преимущество, позволяющее избежать катастрофической ошибки =].

Я настоятельно рекомендую ознакомиться с API ордеров Alpaca, чтобы узнать, как вы можете создавать различные типы рыночных ордеров. Для этого урока мы выбрали самый простой вариант.

Вывод

Спасибо, что дочитали этот урок до конца. На этом этапе вы должны были иметь возможность развернуть бота разногласий, который может покупать акции от вашего имени с подтверждением.

Есть еще МНОГО, что можно сделать, например, продать ценные бумаги, которые вы только что купили, а также более сложные типы ордеров и анализ. Оставайтесь с нами во второй части, где я расскажу больше о создании Dispaca, Discord Alpaca.