Торговля криптовалютой

  • Часть 1: Стоп-лосс и тейк-профит с Uniswap
  • Часть 2: Автоматизированное тестирование
  • Автоматизация торговли криптовалютой

Часть 1: Стоп-лосс и тейк-профит с Uniswap

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

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

Если вы хотите зарабатывать на торговле криптовалютой, одной из самых важных вещей является управление рисками. И простой способ сделать это — соотношение риска и прибыли.

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

Простая стратегия управления рисками для спотовой торговли, основанная на соотношении риск/прибыль

Допустим, вы покупаете токен X за 1 ETH (о разных стратегиях входа/снайпера я напишу позже на моем Substack). Теперь могут произойти две вещи:

  1. Цена растет — молодцы
  2. Цена падает — дерьмо случается

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

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

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

Так, например, вы покупаете любое количество токена X за 1 ETH.

Вы продаете токен X, когда стоимость позиции падает ниже 0,9 ETH с небольшим убытком в 0,1 ETH. Ваш риск составляет 0,1 ETH:

И вы продаете токены X, когда их стоимость поднимается выше 1,5 ETH с приростом в 0,5 ETH. Ваше вознаграждение составляет 0,5 ETH.

Таким образом, соотношение риска и прибыли будет 1 к 5. За каждый 1 ETH, которым вы рискуете в этой сделке, вы можете заработать 5 ETH.

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

В традиционных финансах рекомендуется устанавливать стоп-лосс на уровне 0,5% — 1% от торгового баланса. Допустим, у вас на счету 10 ETH, хороший стоп-лосс может составлять 0,1 ETH. В этом уроке мы собираемся вести спотовую торговлю на Uniswap без кредитного плеча, поэтому вы можете увеличивать процент до тех пор, пока не почувствуете себя комфортно. Но не играйте в азартные игры и не поднимайтесь выше 10%. Хороший ночной сон важнее денег…

Наша цель сейчас — автоматизировать эту торговую стратегию на Python на Uniswap:

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

На децентрализованной бирже, такой как Uniswap, нет фиксированной цены

Прежде чем мы начнем с кода, я просто хочу выделить очень важную концепцию для торговли на DEX:

Мы не можем использовать текущую цену токена для наших ордеров стоп-лосс или тейк-профит. На самом деле «цены», как на централизованной бирже или в традиционных финансах, не существует.

Это справедливо для любого автоматического маркет-мейкера (AMM), следующего формуле y*x=k. В этой формуле k — постоянная цена, а y и x — резервные суммы токенов.

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

Таким образом, если мы хотим продать 1 миллион токенов X, цена каждого токена, который мы продаем, немного снизится. Таким образом, мы не можем просто взять текущую цену и использовать ее для нашего стоп-профита и тейк-профита, но вместо этого мы должны либо перевернуть формулу AMM, чтобы рассчитать реальную цену, либо мы просто вызовем функцию get_amounts_out, которую мы уже использовали в предыдущем руководстве, чтобы позволить Uniswap рассчитать, сколько токенов мы получим от свопа.

Вызов get_get_amounts — это более простой подход, поэтому мы так и сделаем.

Базовая структура бота

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

Таким образом, наш бот сохраняет начальное состояние, такое как текущая стоимость наших токенов во время запуска, а затем запускает цикл бота.while True:
position_value = get_position_value()
if position_value > take_profit:
sell_token()
if position_value < stop_loss:
sell_token()
time.sleep(60)

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

Этот подход, при котором вы проверяете что-то через регулярные промежутки времени, также называется «опросом» и является популярным и очень простым решением, которое мы также будем использовать в этом примере бота.

Подсказка: Более продвинутый подход, который я использую для своих ботов, обрабатывающих тысячи запросов в секунду, основан на событиях. Я знаю, что стоимость моей позиции меняется только тогда, когда происходит своп. Поэтому я использую здесь не опросы, а так называемую модель издатель/подписчик. Это означает, что я создаю соединение веб-сокета с узлом блокчейна (вы можете сделать это, например, с помощью Quicknode) и сканирую каждый входящий новый блок на наличие всех соответствующих событий свопа и проверяю значение позиции только после того, как произошло новое событие свопа. Еще более продвинутым было бы опережение определенных транзакций и расчет резервов на местном уровне, не только наблюдая за подтвержденными блоками, но и да… Не сегодня. Это повлечет за собой множество новых концепций, и для большинства стратегий спотовой торговли простой подход к опросу вполне подойдет.

Код для торгового бота на Python

Сейчас я проведу вас через код, и вы также можете найти полный код в репозитории Python и DeFi на Github.

Несколько замечаний перед тем, как мы начнем:

  • Код работает и может быть использован в продакшене с небольшой доработкой, но он все равно не идеален — так что перепроверяйте все
  • Я сделаю этот пример настолько простым, насколько это возможно (менее 200 строк кода), так что да, есть возможности для улучшения
  • Я ограничусь примерами ботов одним рабочим файлом Python — для производственного кода я обычно разбиваю их и пишу больше повторно используемых компонентов кода

Импорт:

import os
from web3 import Account, Web3
import json
import dotenv
import time
import argparse

dotenv.load_dotenv()

Единственными новыми вещами здесь являются argparse, который я объясню позже и который является частью стандартной библиотеки Python, и dotenv ( https://pypi.org/project/python-dotenv )/. Python dotenv позволяет работать с переменными окружения. Это делает работу с определенными вещами, такими как закрытые ключи или ключи API, немного более безопасной.

Константы:

MIN_ERC20_ABI = json.loads(
«»»
[{«constant»: true, «inputs»: [], «name»: «name», «outputs»: [ { «name»: «», «type»: «string» } ], «payable»: false, «stateMutability»: «view», «type»: «function» }, { «constant»: false, «inputs»: [ { «name»: «_spender», «type»: «address» }, { «name»: «_value», «type»: «uint256» } ], «name»: «approve», «outputs»: [ { «name»: «», «type»: «bool» } ], «payable»: false, «stateMutability»: «nonpayable», «type»: «function» }, { «constant»: true, «inputs»: [], «name»: «totalSupply», «outputs»: [ { «name»: «», «type»: «uint256» } ], «payable»: false, «stateMutability»: «view», «type»: «function» }, { «constant»: false, «inputs»: [ { «name»: «_from», «type»: «address» }, { «name»: «_to», «type»: «address» }, { «name»: «_value», «type»: «uint256» } ], «name»: «transferFrom», «outputs»: [ { «name»: «», «type»: «bool» } ], «payable»: false, «stateMutability»: «nonpayable», «type»: «function» }, { «constant»: true, «inputs»: [], «name»: «decimals», «outputs»: [ { «name»: «», «type»: «uint8» } ], «payable»: false, «stateMutability»: «view», «type»: «function» }, { «constant»: true, «inputs»: [ { «name»: «_owner», «type»: «address» } ], «name»: «balanceOf», «outputs»: [ { «name»: «balance», «type»: «uint256» } ], «payable»: false, «stateMutability»: «view», «type»: «function» }, { «constant»: true, «inputs»: [], «name»: «symbol», «outputs»: [ { «name»: «», «type»: «string» } ], «payable»: false, «stateMutability»: «view», «type»: «function» }, { «constant»: false, «inputs»: [ { «name»: «_to», «type»: «address» }, { «name»: «_value», «type»: «uint256» } ], «name»: «transfer», «outputs»: [ { «name»: «», «type»: «bool» } ], «payable»: false, «stateMutability»: «nonpayable», «type»: «function» }, { «constant»: true, «inputs»: [ { «name»: «_owner», «type»: «address» }, { «name»: «_spender», «type»: «address» } ], «name»: «allowance», «outputs»: [ { «name»: «», «type»: «uint256» } ], «payable»: false, «stateMutability»: «view», «type»: «function» }, { «payable»: true, «stateMutability»: «payable», «type»: «fallback» }, { «anonymous»: false, «inputs»: [ { «indexed»: true, «name»: «owner», «type»: «address» }, { «indexed»: true, «name»: «spender», «type»: «address» }, { «indexed»: false, «name»: «value», «type»: «uint256» } ], «name»: «Approval», «type»: «event» }, { «anonymous»: false, «inputs»: [ { «indexed»: true, «name»: «from», «type»: «address» }, { «indexed»: true, «name»: «to», «type»: «address» }, { «indexed»: false, «name»: «value», «type»: «uint256» } ], «name»: «Transfer», «type»: «event»}]
«»»
)

UNISWAPV2_ROUTER_ABI = json.loads(
«»»
[{«inputs»:[{«internalType»:»address»,»name»:»_factory»,»type»:»address»},{«internalType»:»address»,»name»:»_WETH»,»type»:»address»}],»stateMutability»:»nonpayable»,»type»:»constructor»},{«inputs»:[],»name»:»WETH»,»outputs»:[{«internalType»:»address»,»name»:»»,»type»:»address»}],»stateMutability»:»view»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»tokenA»,»type»:»address»},{«internalType»:»address»,»name»:»tokenB»,»type»:»address»},{«internalType»:»uint256″,»name»:»amountADesired»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountBDesired»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountAMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountBMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»addLiquidity»,»outputs»:[{«internalType»:»uint256″,»name»:»amountA»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountB»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»token»,»type»:»address»},{«internalType»:»uint256″,»name»:»amountTokenDesired»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountTokenMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETHMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»addLiquidityETH»,»outputs»:[{«internalType»:»uint256″,»name»:»amountToken»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETH»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″}],»stateMutability»:»payable»,»type»:»function»},{«inputs»:[],»name»:»factory»,»outputs»:[{«internalType»:»address»,»name»:»»,»type»:»address»}],»stateMutability»:»view»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountOut»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»reserveIn»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»reserveOut»,»type»:»uint256″}],»name»:»getAmountIn»,»outputs»:[{«internalType»:»uint256″,»name»:»amountIn»,»type»:»uint256″}],»stateMutability»:»pure»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountIn»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»reserveIn»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»reserveOut»,»type»:»uint256″}],»name»:»getAmountOut»,»outputs»:[{«internalType»:»uint256″,»name»:»amountOut»,»type»:»uint256″}],»stateMutability»:»pure»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountOut»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»}],»name»:»getAmountsIn»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»view»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountIn»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»}],»name»:»getAmountsOut»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»view»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountA»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»reserveA»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»reserveB»,»type»:»uint256″}],»name»:»quote»,»outputs»:[{«internalType»:»uint256″,»name»:»amountB»,»type»:»uint256″}],»stateMutability»:»pure»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»tokenA»,»type»:»address»},{«internalType»:»address»,»name»:»tokenB»,»type»:»address»},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountAMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountBMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»removeLiquidity»,»outputs»:[{«internalType»:»uint256″,»name»:»amountA»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountB»,»type»:»uint256″}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»token»,»type»:»address»},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountTokenMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETHMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»removeLiquidityETH»,»outputs»:[{«internalType»:»uint256″,»name»:»amountToken»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETH»,»type»:»uint256″}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»token»,»type»:»address»},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountTokenMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETHMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»removeLiquidityETHSupportingFeeOnTransferTokens»,»outputs»:[{«internalType»:»uint256″,»name»:»amountETH»,»type»:»uint256″}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»token»,»type»:»address»},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountTokenMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETHMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″},{«internalType»:»bool»,»name»:»approveMax»,»type»:»bool»},{«internalType»:»uint8″,»name»:»v»,»type»:»uint8″},{«internalType»:»bytes32″,»name»:»r»,»type»:»bytes32″},{«internalType»:»bytes32″,»name»:»s»,»type»:»bytes32″}],»name»:»removeLiquidityETHWithPermit»,»outputs»:[{«internalType»:»uint256″,»name»:»amountToken»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETH»,»type»:»uint256″}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»token»,»type»:»address»},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountTokenMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountETHMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″},{«internalType»:»bool»,»name»:»approveMax»,»type»:»bool»},{«internalType»:»uint8″,»name»:»v»,»type»:»uint8″},{«internalType»:»bytes32″,»name»:»r»,»type»:»bytes32″},{«internalType»:»bytes32″,»name»:»s»,»type»:»bytes32″}],»name»:»removeLiquidityETHWithPermitSupportingFeeOnTransferTokens»,»outputs»:[{«internalType»:»uint256″,»name»:»amountETH»,»type»:»uint256″}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»address»,»name»:»tokenA»,»type»:»address»},{«internalType»:»address»,»name»:»tokenB»,»type»:»address»},{«internalType»:»uint256″,»name»:»liquidity»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountAMin»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountBMin»,»type»:»uint256″},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″},{«internalType»:»bool»,»name»:»approveMax»,»type»:»bool»},{«internalType»:»uint8″,»name»:»v»,»type»:»uint8″},{«internalType»:»bytes32″,»name»:»r»,»type»:»bytes32″},{«internalType»:»bytes32″,»name»:»s»,»type»:»bytes32″}],»name»:»removeLiquidityWithPermit»,»outputs»:[{«internalType»:»uint256″,»name»:»amountA»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountB»,»type»:»uint256″}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountOut»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapETHForExactTokens»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»payable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountOutMin»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapExactETHForTokens»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»payable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountOutMin»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapExactETHForTokensSupportingFeeOnTransferTokens»,»outputs»:[],»stateMutability»:»payable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountIn»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountOutMin»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapExactTokensForETH»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountIn»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountOutMin»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapExactTokensForETHSupportingFeeOnTransferTokens»,»outputs»:[],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountIn»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountOutMin»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapExactTokensForTokens»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountIn»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountOutMin»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapExactTokensForTokensSupportingFeeOnTransferTokens»,»outputs»:[],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountOut»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountInMax»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapTokensForExactETH»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»nonpayable»,»type»:»function»},{«inputs»:[{«internalType»:»uint256″,»name»:»amountOut»,»type»:»uint256″},{«internalType»:»uint256″,»name»:»amountInMax»,»type»:»uint256″},{«internalType»:»address[]»,»name»:»path»,»type»:»address[]»},{«internalType»:»address»,»name»:»to»,»type»:»address»},{«internalType»:»uint256″,»name»:»deadline»,»type»:»uint256″}],»name»:»swapTokensForExactTokens»,»outputs»:[{«internalType»:»uint256[]»,»name»:»amounts»,»type»:»uint256[]»}],»stateMutability»:»nonpayable»,»type»:»function»},{«stateMutability»:»payable»,»type»:»receive»}]
«»»
)

UNISWAP_V2_SWAP_ROUTER_ADDRESS = «0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D»
WETH_TOKEN_ADDRESS = «0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2»

Так же, как и в предыдущих уроках, только на этот раз мы определяем ABI в том же файле. Вы можете получить JSON ABI в Etherscan (или любом другом обозревателе блоков), перейдя в контракт и проверив там раздел кода ( https://arbiscan.io/address/0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D#code ).

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

Конструктор ботов

Боту нужно состояние, чтобы хранить какие-то переменные, например, начальное значение позиции. Всякий раз, когда у нас есть функции, объединенные с состоянием, мы используем класс Python.

Таким образом, вся логика нашего бота будет находиться в классе AutoSellBot.class AutoSellBot:
def __init__(
self,
name: str,
token_address: str,
stop_loss_percent: int = 10,
take_profit_percent: int = 50,
check_interval: int = 60,
slippage_percent: int = 3,
private_key: str = «»,
rpc: str = «»,
) -> None:
self.name = name
self.token_address = token_address
self.stop_loss_percent = stop_loss_percent
self.take_profit_percent = take_profit_percent
self.check_interval = check_interval
self.slippage_percent = slippage_percent
self.private_key = private_key if private_key else os.getenv(«PRIVATE_KEY»)
if not self.private_key:
raise ValueError(«error: no private key provided»)

self.sell_path = [self.token_address, WETH_TOKEN_ADDRESS]

self.rpc = rpc if rpc else os.getenv(«RPC_ENPOINT»)
if not self.rpc:
raise ValueError(«error: no rpc endpoint configured»)

self.account = Account.from_key(self.private_key)

self.web3 = Web3(Web3.HTTPProvider(os.getenv(«RPC_ENPOINT»)))
self.router_contract = self.web3.eth.contract(
address=UNISWAP_V2_SWAP_ROUTER_ADDRESS, abi=UNISWAPV2_ROUTER_ABI
)
self.token_contract = self.web3.eth.contract(
address=self.token_address, abi=MIN_ERC20_ABI
)

self.token_balance = self.get_balance()
if self.token_balance == 0:
raise ValueError(«error: token_balance is 0»)

self.initial_value = self.get_position_value()
self.stop_loss_value = self.initial_value * (1 — self.stop_loss_percent / 100)
self.take_profit_value = self.initial_value * (
1 + self.take_profit_percent / 100
)

approved = self.approve_token()
assert approved, f»{self.name}: error: could not aprove token»
print(f»{self.name}: bot started»)

Конструктор инициализирует несколько переменных, таких как stop_loss_percentage или take_profit_percentage для нашего соотношения риска и прибыли. Для этого мы рассчитываем начальную стоимость нашей позиции в эфире* и добавляем/вычитаем процентное значение нашего тейк-профита/стоп-лосса.

Далее в нашем цикле бота мы просто сравниваем текущее значение позиции с этими начальными значениями и реагируем.

*Подсказка: В этом примере я использую Ether/WETH с 18 десятичными знаками, но вы можете использовать любую пару токенов. Вам просто нужно поменять местами адреса и десятичные дроби.

Мы также загружаем учетную запись ботов для торговли. Здесь вы видите, как я использую os.getenv(«PRIVATE_KEY»). Это возможно благодаря тому, что я использовал пакет dotenv.

Чтобы это работало, вам нужно создать файл .env в том же каталоге, что и скрипт бота, со следующим содержимым:PRIVATE_KEY = «0x5d9d3c897ad4f2b8b51906185607f79672d7fec086a6fb6afc2de423c017330c»
RPC_ENPOINT = «http://127.0.0.1:8545»

Подсказка: если вы используете Windows, попробуйте WSL (подсистема Windows для Linux, которая довольно хорошо работает со всем этим)

Преимущество этого подхода заключается в том, что если вы, как и я, работаете с Github, вы можете зафиксировать свой код, не рискуя утечкой конфиденциального содержимого, такого как закрытые ключи или ключи API, хранящиеся в вашем файле .env. И да… Я знаю, что только что сделал это… но это локальная учетная запись Ganache, как показано в первом обучающем 😉

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

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

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

Еще одна вещь, которую следует отметить, мы уже одобряем маршрутизатор Uniswap для продажи нашего токена в конструкторе. Здесь мы используем неограниченную сумму, но можем использовать и баланс. Опять же, это делается для того, чтобы как можно быстрее потерпеть неудачу в случае, если что-то не так с ключом или предоставленным RPC, а также для того, чтобы не делать этого, когда рынок рушится или комиссии могут быть намного выше. Итак, типичные ситуации, в которых нашему боту нужно действовать.

Подсказка: Если вам нужен надежный поставщик RPC, подходящий для торговли криптовалютой, обратите внимание на Quicknode.

Некоторые вспомогательные функции для нашего торгового бота

def get_balance(self):
return self.token_contract.functions.balanceOf(self.account.address).call()

def get_position_value(self):
amounts_out = self.router_contract.functions.getAmountsOut(
self.token_balance, self.sell_path
).call()
# amounts_out[0] is the token amount in — amounts out [1] is the ETH amount out
return amounts_out[1]

def approve_token(self):
approve_tx = self.token_contract.functions.approve(
UNISWAP_V2_SWAP_ROUTER_ADDRESS, 2**256 — 1
).build_transaction(
{
«gas»: 500_000,
«maxPriorityFeePerGas»: self.web3.eth.max_priority_fee,
«maxFeePerGas»: 100 * 10**10,
«nonce»: self.web3.eth.get_transaction_count(self.account.address),
}
)

signed_approve_tx = self.web3.eth.account.sign_transaction(
approve_tx, self.account.key
)

tx_hash = self.web3.eth.send_raw_transaction(signed_approve_tx.rawTransaction)
tx_receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)

if tx_receipt and tx_receipt[«status»] == 1:
print(
f»{self.name}: approve successful: approved {UNISWAP_V2_SWAP_ROUTER_ADDRESS} to spend unlimited token»
)
return True
else:
raise Exception(f»{self.name} error: could not approve token»)

def sell_token(self, min_amount_out=0):
sell_tx_params = {
«nonce»: self.web3.eth.get_transaction_count(self.account.address),
«from»: self.account.address,
«gas»: 500_000,
«maxPriorityFeePerGas»: self.web3.eth.max_priority_fee,
«maxFeePerGas»: 100 * 10**10,
}
sell_tx = self.router_contract.functions.swapExactTokensForETH(
self.token_balance, # amount to sell
min_amount_out, # min amount out
self.sell_path,
self.account.address,
int(time.time()) + 180, # deadline now + 180 sec
).build_transaction(sell_tx_params)

signed_sell_tx = self.web3.eth.account.sign_transaction(
sell_tx, self.account.key
)

tx_hash = self.web3.eth.send_raw_transaction(signed_sell_tx.rawTransaction)
tx_receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash)
if tx_receipt and tx_receipt[«status»] == 1:
# now make sure we sold them
self.token_balance = self.get_balance()
if self.token_balance == 0:
print(f»{self.name}: all token sold: tx hash: {Web3.to_hex(tx_hash)}»)
return True
else:
print(f»{self.name}: error selling token»)
return False

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

Стратегия бота

def execute(self):
position_value = self.get_position_value()
print(f»{self.name}: position value {position_value/10**18:.6f}»)
# we check if we hit our stop loss or take profit level
if (position_value <= self.stop_loss_value) or (
position_value >= self.take_profit_value
):
# we calculate the min amount out here based on the current value and the configured slippage
min_amount_out = int(position_value * (1 — self.slippage_percent / 100))
print(f»{self.name}: position value hit limit — selling all token»)
self.sell_token(min_amount_out)

Наша стратегия довольно проста. Чтобы использовать бота, вы просто покупаете любой токен и предоставляете боту адрес токена вместе с лимитами стоп-лосс и тейк-профит.

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

Здесь вы можете улучшить различные вещи, например, иметь несколько тейк-профитов / стоп-лоссов и продавать только часть наших токенов.

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

Важный:

Наличие стоп-лосса в 10% не означает, что мы не можем потерять больше. Это означает, что стоп-лосс сработает, когда значение позиции во время проверки достигнет наших целей и наш бот попытается продать токен.

Допустим, у нас есть интервал проверки каждые 60 секунд, начальное значение позиции 1 ETH и стоп-лосс на уровне 10%, то есть 0,9 ETH.

Теперь первая проверка возвращает 1 ETH, а вторая возвращает значение позиции 0.8 ETH, потому что в течение нашего 60-секундного интервала проверки были плохие новости. Наш стоп-лосс сработал бы только сейчас, во время второй проверки, и мы потеряли 20%!

Чтобы компенсировать это, вы должны установить короткий интервал проверки для вашего опроса в зависимости от доступной ликвидности. Токен с очень маленькой ликвидностью (<100k) следует проверять гораздо чаще, чем токен с глубоким пулом ликвидности (500k+). Технически это не проблема, и даже самый бесплатный RPC API провайдер позволяет проверять позиции каждую секунду.

Подсказка: Я бы порекомендовал платный тариф с одним из самых быстрых RPC-провайдеров, если вы серьезно относитесь к торговле криптовалютой — я использую Quicknode для всего этого.

Еще одна вещь, которую следует учитывать, но обычно не такая уж большая проблема с автоматическими ботами, — это так называемое проскальзывание, которое я объяснил ранее более подробно. Поэтому мы позволяем задавать это значение в параметрах бота.

Значение min_amount_out — это реальная ценность, которую мы получаем. Если стоимость позиции токена упадет очень сильно, мы можем получить немного другую сумму.

Как установить эти параметры – решать вам. Я предпочитаю, чтобы мои сделки исполнялись при достижении лимитов.

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

Цикл бота

И, наконец, последняя часть нашего бота: бесконечный цикл, о котором я уже упоминал в разделе о структуре бота: def bot_loop(self):
# the bot is supposed to run until all tokens are sold
while self.token_balance:
# we catch any runtime / network errors here — just to make sure it runs
try:
self.execute()
time.sleep(self.check_interval)
except Exception as e:
print(f»exception: {e}»)
print(f»{self.name}: stopping bot — all token sold»)

Это довольно просто. Пока у нас есть токены в нашей учетной записи бота (пока self.token_balance), выполняйте нашу функцию стратегии бота и спите в течение настроенного интервала.

Мы отделяем исполнение стратегии от цикла бота, чтобы упростить тестирование стратегии. Более подробно об автоматическом тестировании я расскажу в следующем уроке.

Мы также окружаем стратегию бота try/except, чтобы выявить любые проблемы, которые могут возникнуть во время выполнения бота. Скорее всего, это могут быть сетевые ошибки, такие как тайм-аут соединения на этом этапе. Правильнее было бы сделать это более подробно в функции выполнения, но для этого урока просто важно, чтобы мы поддерживали работу бота в случае каких-либо исключений.

И все. Теперь нам просто нужно создать экземпляр бота в нашей функции main и запустить его.

Основная функция

if __name__ == «__main__»:
parser = argparse.ArgumentParser(
description=»Simple stop loss and take profit trading bot (crjameson.xyz).»
)

# mandatory arguments
# example token on mainnet uniswap v2: ELON_TOKEN_ADDRESS = «0x761D38e5ddf6ccf6Cf7c55759d5210750B5D60F3»

parser.add_argument(
«token_address», type=str, default=»», help=»token address you want to monitor»
)
# optional arguments — we set the default values here
parser.add_argument(«—name», type=str, default=»simple bot», help=»bot name»)
parser.add_argument(«—sl», type=int, default=5, help=»stop-loss percentage»)
parser.add_argument(«—tp», type=int, default=20, help=»take profit percentage»)
parser.add_argument(
«—interval», type=int, default=60, help=»check interval in seconds»
)
parser.add_argument(«—slippage», type=int, default=3, help=»slippage percentage»)
parser.add_argument(«—key», type=str, default=»», help=»your private key»)
parser.add_argument(
«—rpc», type=str, default=»http://127.0.0.1:8545″, help=»your rpc endpoint»
)

args = parser.parse_args()

bot = AutoSellBot(
args.name,
args.token_address,
stop_loss_percent=args.sl,
take_profit_percent=args.tp,
check_interval=args.interval,
slippage_percent=args.slippage,
private_key=args.key,
)
bot.bot_loop()

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

Здесь мы используем модуль argparse из стандартной библиотеки.

Сохранив все это в файл с именем autosell_bot.py теперь мы можем запустить нашего бота следующим образом:python autosell_bot.py 0x<TOKEN_ADDRESS> —name «autobot» —sl 10 —tp 50

Конец

И все. Если вы следовали инструкциям, у вас есть собственный полезный маленький торговый бот на python, способный управлять вашей позицией на Uniswap v2.

Домашнее задание может заключаться в том, чтобы добавить больше уровней стоп-лосс/тейк-профит, разрешить торговые пары, отличные от WETH, или Uniswap v3. Есть много способов оптимизировать его, но в следующем уроке этой серии мы сделаем нечто гораздо более важное:

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

Код бота, как всегда, доступен на моем GitHub здесь:

https://github.com/crjameson/python-defi-tutorials/tree/main/bots/autosellbot

Часть 2: Автоматизированное тестирование

Я познакомлю вас с одной из самых важных концепций при разработке торговых ботов на реальные деньги: автоматизированное тестирование.

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

Подсказка: Не пропускайте тестирование! особенно для торговых ботов на реальные деньги. Я бы рекомендовал писать тесты для каждого более сложного скрипта, который должен выполняться в любой производственной среде.

Краткое введение в автоматизированное тестирование

Допустим, мы хотим убедиться, что код бота, который мы только что написали в предыдущем уроке, работает. Мы можем либо протестировать все вручную, импортировав ключи в Metamask, либо выполнить своп для покупки токенов, снова проверить Metamask, сменить учетную запись, снова проверить и т. д. … И делать все это, каждый раз, когда мы что-то меняем в коде… Вы понимаете, это громоздко и ненадежно.

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

Я буду использовать pytest во время этого урока ( https://docs.pytest.org/en/7.4.x/ ), потому что мне нравится синтаксис, он широко распространен, хорошо документирован и прост в использовании, как только вы поймете основные концепции. Но Python также поставляется с собственным фреймворком для тестирования (https://docs.python.org/3/library/unittest.html), который также является отличным выбором.

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

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

И еще одно преимущество заключается в том, что вы научитесь структурировать свои функции Python в виде небольших тестируемых «блоков», которые обрабатывают ровно одну вещь.

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

Когда вы пишете свои тесты, нас интересуют два типа тестов.

  1. Модульные тесты: это означает, что вы берете один изолированный фрагмент кода, например, функцию, и проверяете, что он ведет себя так, как ожидалось.
  2. Интеграционные тесты: во время интеграционных тестов вы тестируете несколько функций, классов и модулей и смотрите, все ли работает вместе.

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

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

Pytest код для автоматического тестирования бота

Чтобы протестировать нашего бота, мы создаем файл с именем test_autosellbot.py и помещаем его в тот же каталог, что и файл скрипта нашего бота.

Pytest автоматически найдет все файлы, начинающиеся с test_, и выполнит все функции в именованном test_.

Импорт

Начнем снова с импорта:from autosell_bot import AutoSellBot
from autosell_bot import (
UNISWAP_V2_SWAP_ROUTER_ADDRESS,
WETH_TOKEN_ADDRESS,
MIN_ERC20_ABI,
UNISWAPV2_ROUTER_ABI,
)
import pytest
from web3 import Account, Web3
import time

Убедитесь, что вы установили pytest и пакет pytest-mock через pip.

Некоторые константы

Следующим шагом будет определение нескольких констант, необходимых для наших тестов:TEST_ACCOUNT_1 = «0x5d9d3c897ad4f2b8b51906185607f79672d7fec086a6fb6afc2de423c017330c»
TEST_ACCOUNT_2 = «0x9562571d198ba47c95aea31c2714573fbadb8d6b6da42b3b3a352cefd0b37537»
ELON_TOKEN_ADDRESS = «0x761D38e5ddf6ccf6Cf7c55759d5210750B5D60F3»

Это приватные ключи для наших локальных тестовых аккаунтов Ganache и адрес токена DogElonMars (символ: ELON), который мы будем использовать для тестирования. Это не рекомендация к покупке, но мне просто нравится название, и оно имеет приличную ликвидность для наших тестов, доступных на Uniswap v2.

Оснащение

Когда вы хотите разделить код между несколькими тестами, вы можете использовать так называемые «фикстуры» в pytest.@pytest.fixture(scope=»session»)
def web3():
rpc_endpoint = «http://127.0.0.1:8545» # our local ganache instance
web3 = Web3(Web3.HTTPProvider(rpc_endpoint))
return web3

Это пример приспособления, обеспечивающего web3-соединение со всеми нашими тестами во время текущей сессии. Чтобы использовать это, просто передайте web3 в качестве параметра в функцию test_.

Scope=»sessions» означает, что его экземпляр будет создан только один раз за тестовый сеанс. Если вам нужен фикстура для каждой функции, вы можете использовать scope=»function».

Подсказка: Если позже вы будете тестировать несколько файлов и более крупные проекты, я рекомендую поместить все фикстуры в conftest.py файл, помещенный в отдельный подкаталог tests/ со всеми различными тестовыми файлами. Все эти детали задокументированы в официальной документации pytest.

Вспомогательные функции

Теперь нам нужна вспомогательная функция для покупки токена ELON, чтобы наш бот мог продать их позже. Это всего лишь копия функции buy из предыдущих уроков.

Модули pytest похожи на любой другой модуль python, и все функции, не начинающиеся с test_, будут просто проигнорированы pytest. Таким образом, вы можете добавить столько вспомогательных функций, сколько вам нужно.def buy_token(web3, account, amount, token_address=ELON_TOKEN_ADDRESS):
router_contract = web3.eth.contract(
address=UNISWAP_V2_SWAP_ROUTER_ADDRESS, abi=UNISWAPV2_ROUTER_ABI
)
token_contract = web3.eth.contract(address=token_address, abi=MIN_ERC20_ABI)

buy_path = [WETH_TOKEN_ADDRESS, token_address]

buy_tx_params = {
«nonce»: web3.eth.get_transaction_count(account.address),
«from»: account.address,
«chainId»: 1337,
«gas»: 500_000,
«maxPriorityFeePerGas»: web3.eth.max_priority_fee,
«maxFeePerGas»: 100 * 10**10,
«value»: amount,
}
buy_tx = router_contract.functions.swapExactETHForTokens(
0, # min amount out
buy_path,
account.address,
int(time.time()) + 180, # deadline now + 180 sec
).build_transaction(buy_tx_params)

signed_buy_tx = web3.eth.account.sign_transaction(buy_tx, account.key)

tx_hash = web3.eth.send_raw_transaction(signed_buy_tx.rawTransaction)
web3.eth.wait_for_transaction_receipt(tx_hash)

# now make sure we got some tokens
token_balance = token_contract.functions.balanceOf(account.address).call()
print(f»token balance: {token_balance / 10**18}»)
print(f»eth balance: {web3.eth.get_balance(account.address) / 10**18}»)
return token_balance

Наконец-то мы можем определить наш первый тест.

Тестирование функции approve

Я начну с юнит-теста, проверки того, что функция одобрения ботов действительно одобряет маршрутизатор Uniswap на трату токена.

Итак, мой тестовый пример:

Когда я создаю экземпляр бота, я ожидаю, что мой код одобрит маршрутизатор Uniswap v2, чтобы потратить неограниченное количество моего токена ELON.# pip install pytest-mock to use mocker fixture
def test_approve(mocker):
get_balance_call = mocker.patch(
«autosell_bot.AutoSellBot.get_balance», return_value=1000000
)
get_position_value_call = mocker.patch(
«autosell_bot.AutoSellBot.get_position_value», return_value=1000000
)

account1_bot = AutoSellBot(
«autosell_bot», ELON_TOKEN_ADDRESS, private_key=TEST_ACCOUNT_1
)
# now we make sure the bot has approved the router to sell the token
approval = account1_bot.token_contract.functions.allowance(
account1_bot.account.address, UNISWAP_V2_SWAP_ROUTER_ADDRESS
).call()
assert approval == 2**256 — 1

Эти несколько строк кода убеждают в том, что наша функция утверждения в конструкторе бота работает. В этом тесте уже есть одна продвинутая концепция тестирования.

Как вы видите, «mocker» является аргументом тестовой функции. Объект mocker — это приспособление, позволяющее «имитировать» другие функции. Это означает, что вместо вызова реальной функции, как get_balance, наш код бота просто вернет заданное возвращаемое значение 100000.

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

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

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

Чтобы убедиться, что он был вызван, мы получаем текущее значение одобрения для кошелька бота, вызывая контракт токена, и мы утверждаем, что это значение равно 2**256–1, что означает неограниченно.

В принципе, все тесты структурированы следующим образом:

У вас есть функция с именем test_ с некоторыми приспособлениями или аргументами, содержащими один или несколько операторов assert, проверяющих желаемое поведение. Каждый оператор assert — это просто выражение, результатом вычисления которого является истина или ложь.

Чтобы запустить этот тест, просто выполните следующую команду в том же каталоге, где находится ваш тестовый файл:pytest -s —disable-warnings —durations=0 test_autosellbot.py

Обычно я отключаю предупреждения, использую -s для захвата операторов print и вывода stdout!, а — durations=0 измеряет время выполнения тестов.

Если вы хотите протестировать только одну функцию тестового файла, вы можете сделать это следующим образом:pytest -s —disable-warnings —durations=0 test_autosellbot.py::test_approve

Обычно я вызываю каждую тестовую функцию отдельно до тех пор, пока не закончу их реализацию и не проведу окончательный тест для всего файла / проекта.

Подсказка: Вы можете либо настроить свой тестовый фреймворк на автоматический запуск/остановку Ganache после каждого теста или тестовой сессии (для этого можно использовать pytest_sessionstart / pytest_sessionfinish), либо самое простое решение на данный момент — просто запустить Ganache и перезапустить его вручную после тестовой сессии. Просто имейте в виду, что если вы хотите протестировать функции изменения состояния, такие как approve/swap, использование одного и того же экземпляра Ganache может привести к неправильным результатам.

Подсказка: Инструменты управления проектами Python, такие как poetryмогут помочь вам запускать тесты и автоматизировать различные задачи, такие как управление виртуальной средой, зависимостями и другими вещами. Обязательно ознакомьтесь с ним перед написанием производственного кода.

Тестирование исполнения стоп-лосса

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

Итак, мой тестовый пример:

Когда я выполняю стратегию бота и значение позиции одинаковое, ничего не должно происходить.

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

Когда я выполняю стратегию бота и значение позиции ниже моего порога стоп-лосса, я ожидаю, что код бота вызовет функцию sell_token.

Подсказка: Может показаться немного излишним записывать это таким образом, но на самом деле я обычно так и делаю (по крайней мере, в своем уме). Я перебираю все возможные ситуации, записываю их, а затем определяю для них тестовый пример. Если позже я поймаю какую-либо новую ошибку во время выполнения, я просто добавлю для нее новый тестовый пример, а затем исправлю свой код до тех пор, пока этот тест также не пройдет.def test_execute_sl(mocker):
# our initial balance is set to 1 ether (1*10**18 wei)
get_balance_call = mocker.patch(
«autosell_bot.AutoSellBot.get_balance», return_value=1 * 10**18
)
sell_token_call = mocker.patch(
«autosell_bot.AutoSellBot.sell_token», return_value=True
)
# we mock the function call to get_position_value to make it return different position values
get_position_value_call = mocker.patch(
«autosell_bot.AutoSellBot.get_position_value»
)
get_position_value_call.side_effect = [
int(1 * 10**18), # constructor call — it returns the same value
int(0.99 * 10**18), # first call — it returns a little less — still more than the limit
int(0.8 * 10**18), # second call — we made 20% loss -> sell
]

account1_bot = AutoSellBot(
«autosell_bot», ELON_TOKEN_ADDRESS, private_key=TEST_ACCOUNT_1
)

# now we run the bot for the first time
account1_bot.execute()
# assert that nothing happened
sell_token_call.assert_not_called()
sell_token_call.reset_mock()

# now we run the bot a second time, this time the value decreased and it should call sell
account1_bot.execute()
sell_token_call.assert_called_once()

Здесь мы снова высмеиваем несколько функций. Для get_balance и sell_token мы просто определяем статическое возвращаемое значение.

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

Новым является использование get_position_value_call.side_effect. side_effect позволяет передавать список возвращаемых значений для каждого вызова функции.

Мы передаем 3 значения int в качестве аргумента, и при первом вызове get_position_value во время инициализации бота он вернет 1*10**18.

Теперь я запускаю стратегию в первый раз, вызвав execute(). Эта функция снова вызывает get_position_value_mock, которая на этот раз возвращает 0.99 * 10**18. Таким образом, стоимость позиции снизилась, но все еще находится выше нашего порога.

Поэтому я ожидаю, что моя функция продажи не вызывается. Чтобы проверить это, вы можете использовать функцию assert_not_called() макета. Просто не забудьте прочитать документацию обо всех этих удивительных функциях здесь ( https://pytest-mock.readthedocs.io/en/latest/ ).

Затем я сбрасываю свой фиктивный объект и снова запускаю бота. На этот раз моя фиктивная функция get get_position_value возвращает 0.8 * 10 **18, что ниже значения стоп-лосса. Таким образом, я утверждаю, что моя функция sell была вызвана один раз.

Вот и все. Теперь у нас есть тестовая функция, чтобы убедиться, что наш стоп-лосс работает и выполняет функцию продажи.

Тестирование исполнения тейк-профита

Это в основном то же самое, что и выше, поэтому я просто дам вам код без каких-либо дополнительных объяснений.def test_execute_tp(mocker):
# our initial balance is set to 1 ether (1*10**18 wei)
get_balance_call = mocker.patch(
«autosell_bot.AutoSellBot.get_balance», return_value=1 * 10**18
)
sell_token_call = mocker.patch(
«autosell_bot.AutoSellBot.sell_token», return_value=True
)
# we mock the function call to get_position_value to make it return different position values
get_position_value_call = mocker.patch(
«autosell_bot.AutoSellBot.get_position_value»
)
get_position_value_call.side_effect = [
int(1 * 10**18), # constructor call — it returns the same value
int(1.1 * 10**18), # first call — it returns a little more — still less than the limit
int(1.6 * 10**18), # second call — we made 60% gain -> sell
]

account1_bot = AutoSellBot(
«autosell_bot», ELON_TOKEN_ADDRESS, private_key=TEST_ACCOUNT_1
)

# now we run the bot for the first time
account1_bot.execute()
# assert that nothing happened
sell_token_call.assert_not_called()
sell_token_call.reset_mock()

# now we run the bot a second time, this time the value decreased and it should call sell
account1_bot.execute()
sell_token_call.assert_called_once()

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

Интеграционный тест

Идея этого теста заключается в том, чтобы заставить все функции работать вместе в «реалистичной» среде. Мы по-прежнему используем разветвленный экземпляр Ganache основной сети Ethereum, как описано в руководстве по установке.

Тест-план состоит в том, чтобы использовать один аккаунт для покупки токена ELON за 900 ETH и пампа цены токена. Затем мы покупаем токен ELON с помощью нашего бота-аккаунта за 1 ETH.

Теперь мы хотим сбросить с первого кошелька всю позицию ELON на сумму 900 ETH и ожидаем, что наш бот также автоматически продаст свой токен ELON.

Чтобы протестировать тейк-профит таким же образом, вы можете просто изменить порядок транзакций.

А вот как это выглядит в коде:def test_autosellbot(web3):
account1 = Account.from_key(TEST_ACCOUNT_1)
account2 = Account.from_key(TEST_ACCOUNT_2)

# buy with token wallet 2 for 900 eth
account2_token_balance = buy_token(web3, account2, 900 * 10**18)
assert account2_token_balance > 0

# buy with token wallet 1 for 1 eth
account1_token_balance = buy_token(web3, account1, 1 * 10**18)
assert account1_token_balance > 0

# create our bots — they only start with a balance > 0
account1_bot = AutoSellBot(
«autosell_bot», ELON_TOKEN_ADDRESS, private_key=TEST_ACCOUNT_1
)
account2_bot = AutoSellBot(
«dump_bot», ELON_TOKEN_ADDRESS, private_key=TEST_ACCOUNT_2
)

# run the account 1 bot — to test that nothing happens
account1_bot.execute()
account1_token_balance_run1 = account1_bot.get_balance()
assert account1_token_balance_run1 == account1_token_balance

# dump with token wallet 2 and sell all token
account2_bot.sell_token()
account2_token_balance_sold = account2_bot.get_balance()
assert account2_token_balance_sold == 0

# make sure account 1 bot sold as well
account1_bot.execute()
account1_token_balance_run2 = account1_bot.get_balance()
assert account1_token_balance_run2 == 0

Подсказка: Я использую приспособление web3, не используя его, это было просто в демонстрационных целях … И вы знаете, что лучше быть готовым 😉

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

Полный исходный код этого теста, как всегда, доступен в репозитории Github:

https://github.com/crjameson/python-defi-tutorials/blob/main/bots/autosellbot/test_autosellbot.py

Автоматизация торговли криптовалютой

Фото Пьера Бортири — Peiobty on Unsplash

# Создание бота для торговли криптовалютой на Python: полное руководство по автоматизации торговли криптовалютой с помощью TradingView

Хотите автоматизировать торговлю криптовалютой с помощью TradingView? Создание бота для торговли криптовалютой с помощью Python и его интеграция с TradingView может предоставить вам мощное решение. В этом подробном руководстве мы проведем вас через процесс создания торгового бота, который будет совершать сделки на основе оповещений TradingView. Независимо от того, являетесь ли вы новичком или опытным трейдером, это руководство поможет вам автоматизировать торговлю криптовалютой. Давайте начнем!

Содержание

1. Введение в ботов для торговли криптовалютой2. Преимущества использования бота для торговли криптовалютой с интеграцией с TradingView3. Настройка среды разработки4. Подключение к API TradingView5. Получение оповещений от TradingView6. Разработка торговых стратегий7. Совершение сделок с помощью API криптобиржи8. Мониторинг и анализ производительности ботов9. Развертывание и запуск торгового бота10. Заключение

1. Введение в ботов для торговли криптовалютой

Боты для торговли криптовалютой — это программы, которые автоматизируют торговую деятельность на рынке криптовалют. Они могут совершать сделки на основе заранее определенных правил и стратегий, устраняя необходимость ручного вмешательства. Используя возможности торговых ботов, вы можете воспользоваться рыночными возможностями 24/7, повысить скорость исполнения сделок и исключить человеческие эмоции из торгового процесса.

2. Преимущества использования бота для торговли криптовалютой с интеграцией с TradingView

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

3. Настройка среды разработки

Чтобы создать бота для торговли криптовалютой с помощью Python, вам необходимо настроить среду разработки. Убедитесь, что в вашей системе установлен Python, и выберите редактор кода по своему вкусу. Кроме того, установите необходимые библиотеки Python, такие как ccxt для взаимодействия с криптовалютными биржами и запросы для выполнения HTTP-запросов к API TradingView.

4. Подключение к API TradingView

Чтобы получать оповещения от TradingView, вам необходимо подключиться к API TradingView. Начните с создания учетной записи на TradingView и сгенерируйте учетные данные API. API TradingView позволяет подписаться на оповещения в режиме реального времени по определенной торговой паре или символу. Используйте документацию по API, чтобы узнать о доступных конечных точках и методах доступа к данным оповещений.

5. Получение оповещений от TradingView

Как только вы подключитесь к API TradingView, вы сможете начать получать оповещения в своем скрипте Python. Настройте подключение WebSocket для прослушивания оповещений в режиме реального времени. При срабатывании оповещения API TradingView отправит соответствующие данные в ваш скрипт. Извлеките необходимую информацию, такую как торговая пара и сигнал на покупку/продажу, из полезной нагрузки оповещения.
import ccxt
import time

# TradingView webhook configuration
webhook_url = «YOUR_WEBHOOK_URL»

# Trading parameters
symbol = ‘BTC/USDT’
quantity = 0.01 # Amount of cryptocurrency to trade
stop_loss_percent = 2 # Stop-loss percentage

# Bollinger Bands parameters
timeframe = ‘5m’
period = 20
std_dev = 2

# Initialize the exchange
exchange = ccxt.binance({
‘apiKey’: ‘YOUR_API_KEY’,
‘secret’: ‘YOUR_SECRET_KEY’,
‘enableRateLimit’: True,
})

# Connect to TradingView webhook
def send_webhook(payload):
import requests
headers = {‘Content-Type’: ‘application/json’}
response = requests.post(webhook_url, json=payload, headers=headers)
if response.status_code == 200:
print(«Webhook sent successfully!»)
else:
print(«Failed to send webhook.»)

# Retrieve latest candlestick data
def get_candle_data():
candles = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=period)
return candles[-1]

# Calculate Bollinger Bands
def calculate_bollinger_bands():
candles = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=period)
closes = [candle[4] for candle in candles] # Close prices
sma = sum(closes) / period # Simple Moving Average
std_deviation = (sum((x — sma) ** 2 for x in closes) / period) ** 0.5
upper_band = sma + std_dev * std_deviation
lower_band = sma — std_dev * std_deviation
return upper_band, lower_band

# Place a market order
def place_order(side):
order = exchange.create_market_buy_order(symbol, quantity) if side == ‘buy’ else exchange.create_market_sell_order(symbol, quantity)
return order

# Calculate stop-loss price
def calculate_stop_loss():
candles = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=2)
current_price = candles[-2][4] # Close price of the previous candle
stop_loss_price = current_price — (current_price * stop_loss_percent / 100)
return stop_loss_price

# Main trading loop
while True:
try:
# Get latest candlestick data
candle = get_candle_data()

# Extract candlestick data
timestamp, open_, high, low, close, volume = candle

# Calculate Bollinger Bands
upper_band, lower_band = calculate_bollinger_bands()

if close > upper_band:
# Price above upper Bollinger Band, sell signal
order = place_order(‘sell’)
stop_loss_price = calculate_stop_loss()
message = {
«text»: «Sell signal detected. Placed a market sell order for {}.».format(symbol),
«attachments»: [
{
«text»: «Stop-loss price: {}».format(stop_loss_price)
}
]
}
send_webhook(message)
elif close < lower_band:
# Price below lower Bollinger Band, buy signal
order = place_order(‘buy’)
stop_loss_price = calculate_stop_loss()
message = {
«text»: «Buy signal detected. Placed a market buy order for {}.».format(symbol),
«attachments»: [
{
«text»: «Stop-loss price: {}».format(stop_loss_price)
}
]
}
send_webhook(message)

# Wait for the next candle
time.sleep(300) # Wait for 5 minutes

except Exception as e:
print(«An error occurred:», str(e))
time.sleep(60) # Wait for 1 minute before retrying

В этом примере мы добавили стратегию Полосы Боллинджера для генерации сигналов на покупку и продажу. Функция calculate_bollinger_bands() рассчитывает верхнюю и нижнюю полосы Боллинджера на основе цен закрытия свечей последнего period.

Основной торговый цикл проверяет, пересекает ли цена закрытия верхнюю полосу Боллинджера, что указывает на сигнал на продажу, или ниже нижней полосы Боллинджера, что указывает на сигнал на покупку. Затем он размещает соответствующий рыночный ордер и рассчитывает цену стоп-лосса на основе цены закрытия предыдущей свечи.

Обратите внимание, что это простая реализация стратегии Полосы Боллинджера и может потребовать дальнейшей настройки и доработки в соответствии с вашими конкретными торговыми предпочтениями и управлением рисками. Кроме того, не забудьте заменить ‘'YOUR_WEBHOOK_URL'‘, ‘'YOUR_API_KEY' и ‘'YOUR_SECRET_KEY' своими собственными значениями.

6. Разработка торговых стратегий

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

7. Совершение сделок с помощью API криптобиржи

Чтобы совершать сделки на основе ваших торговых стратегий, вам необходимо взаимодействовать с API выбранной вами криптовалютной биржи. Используйте библиотеку, такую как ccxt, для подключения к API биржи и размещения ордеров программным способом. Убедитесь, что вы создали учетные данные API на бирже и предоставили необходимые данные аутентификации в своем скрипте Python.

8. Мониторинг и анализ производительности ботов

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

9. Развертывание и запуск торгового бота

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

Заключение

Создание бота для торговли криптовалютой на Python и его интеграция с TradingView может значительно расширить ваши возможности по автоматизации и оптимизации стратегий торговли криптовалютой. Выполнив шаги, описанные в этом руководстве, вы сможете подключиться к API TradingView, получать оповещения, разрабатывать торговые стратегии, совершать сделки через API криптовалютной биржи и отслеживать производительность бота. Не забывайте постоянно анализировать и адаптировать своего бота к меняющимся рыночным условиям, чтобы максимизировать свой торговый успех.

Источник