Skip to content

Commit

Permalink
feat(trader): Add action for trading using arbie
Browse files Browse the repository at this point in the history
  • Loading branch information
owodunni committed Dec 6, 2020
1 parent d7846a8 commit 8fd32a0
Show file tree
Hide file tree
Showing 19 changed files with 324 additions and 106 deletions.
1 change: 1 addition & 0 deletions Arbie/Actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from Arbie.Actions.pool_finder import PoolFinder # noqa: F401
from Arbie.Actions.pool_updater import PoolUpdater # noqa: F401
from Arbie.Actions.redis_state import RedisState # noqa: F401
from Arbie.Actions.trader import Trader # noqa: F401
72 changes: 72 additions & 0 deletions Arbie/Actions/trader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Trader contains actions for executing trades."""

import logging

from Arbie.Actions import Action
from Arbie.Contracts import GenericToken
from Arbie.Variables import BigNumber, PoolType

logger = logging.getLogger()


def _is_trade_uni(trade):
for pool in trade.pools:
if pool.pool_type is not PoolType.uniswap:
return False
return True


async def get_balance(weth: GenericToken, web3, trader_address):
balance_pre = await weth.balance_of(trader_address)
account_balance = BigNumber.from_value(web3.eth.getBalance(trader_address))
return balance_pre.to_number() + account_balance.to_number()


def _perform_trade(trade, arbie, address, min_profit):
if not _is_trade_uni(trade):
return False

amount_out = arbie.check_out_given_in(trade)
if amount_out - trade.amount_in > min_profit:
logger.info(f"Executing trade with return: {amount_out}, trade: {trade}")
return arbie.swap(trade, address)
return False


def perform_trade(data, trader_address):
arbie = data.arbie()
min_profit = data.min_profit()
for trade in data.trades():
if _perform_trade(trade, arbie, trader_address, min_profit):
return True
return False


class Trader(Action):
"""Find optimal arbitrage opportunity for a list sorted trades.
Remove all trades that are not profitable.
[Settings]
input:
web3: web3
arbie: arbie
trades: filtered_trades
min_profit: 0.3
weth: weth
trader_address: trader_address
output:
profit: profit
"""

async def on_next(self, data):
trader_address = data.trader_address()
balance_pre = await get_balance(data.weth(), data.web3(), trader_address)

if not perform_trade(data, trader_address):
raise Exception("No trade performed")

balance_post = await get_balance(data.weth(), data.web3(), trader_address)

data.profit(balance_post - balance_pre)
logger.info("Finished trading")
7 changes: 5 additions & 2 deletions Arbie/Contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
from Arbie.Contracts.contract import Contract, ContractFactory, Network # noqa: F401
from Arbie.Contracts.event_filter import EventFilter # noqa: F401
from Arbie.Contracts.tokens import GenericToken, IERC20Token # noqa: F401
from Arbie.Contracts.uniswap import UniswapFactory, UniswapPair # noqa: F401
from Arbie.Contracts.uniswap_router import UniswapV2Router # noqa: F401
from Arbie.Contracts.uniswap import ( # noqa: F401
UniswapFactory,
UniswapPair,
UniswapV2Router,
)
13 changes: 11 additions & 2 deletions Arbie/Contracts/arbie.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,19 @@ class Arbie(Contract):
name = "Arbie"
protocol = "arbie"

def check_out_given_in(self, amount_in: BigNumber, trade: Trade):
def check_out_given_in(self, trade: Trade):
addresses, types = address_and_pool_type(trade.pools)
path_address = list(map(lambda t: t.address, trade.path))
amount_out = self.contract.functions.checkOutGivenIn(
amount_in.value, addresses, types, path_address
BigNumber(trade.amount_in).value, addresses, types, path_address
).call()
return BigNumber.from_value(amount_out).to_number()

def swap(self, trade, from_address=None):
addresses, types = address_and_pool_type(trade.pools)
path = list(map(lambda t: t.address, trade.path))
transaction = self.contract.functions.swap(
BigNumber(trade.amount_in).value, addresses, types, path
)

return self._transact_status(transaction, from_address)
10 changes: 6 additions & 4 deletions Arbie/Contracts/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ def __init__(self, w3, owner_address: str, contract):
def get_address(self) -> str:
return self.contract.address

def _transact(self, transaction):
return transact(self.w3, self.owner_address, transaction)
def _transact(self, transaction, from_address=None):
if from_address is None:
from_address = self.owner_address
return transact(self.w3, from_address, transaction)

def _transact_status(self, transaction) -> bool:
return self._transact(transaction).status
def _transact_status(self, transaction, from_address=None) -> bool:
return self._transact(transaction, from_address).status

def _transact_status_and_contract(self, transaction) -> Tuple[bool, str]:
tx_receipt = self._transact(transaction)
Expand Down
4 changes: 2 additions & 2 deletions Arbie/Contracts/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ def transfer(self, to: str, bg_number: BigNumber) -> bool:
transaction = self.contract.functions.transfer(to, bg_number.value)
return self._transact_status(transaction)

def approve(self, spender: str, bg_number: BigNumber) -> bool:
def approve(self, spender: str, bg_number: BigNumber, from_address=None) -> bool:
transaction = self.contract.functions.approve(spender, bg_number.value)
return self._transact_status(transaction)
return self._transact_status(transaction, from_address)

async def approve_owner(self):
bg = await self.balance_of(self.owner_address)
Expand Down
5 changes: 5 additions & 0 deletions Arbie/Contracts/uniswap.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ async def create_reserve(result: Tuple[float, GenericToken]):
return BigNumber.from_value(value, exp)


class UniswapV2Router(Contract):
name = "UniswapV2Router02"
protocol = "uniswap"


class UniswapPair(PoolContract):
name = "UniswapV2Pair"
protocol = "uniswap"
Expand Down
8 changes: 0 additions & 8 deletions Arbie/Contracts/uniswap_router.py

This file was deleted.

10 changes: 10 additions & 0 deletions Arbie/Variables/big_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ def __eq__(self, other):
return self.value == other.value and self.exp == other.exp
return self.value == other

def __lt__(self, other):
if isinstance(other, BigNumber):
return self.value < other.value
return self.value < other

def __gt__(self, other):
if isinstance(other, BigNumber):
return self.value > other.value
return self.value > other

def __truediv__(self, other):
return self.to_number() / other.to_number()

Expand Down
4 changes: 2 additions & 2 deletions Arbie/Variables/trades.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@


class Trade(object):
def __init__(self, pools: Pools, path: Tokens, ratio=None):
def __init__(self, pools: Pools, path: Tokens, ratio=None, amount_in=None):
self.pools = pools
self.path = path
self.amount_in = None
self.amount_in = amount_in
self.profit = None
self.balance = None
self.ratio = ratio
Expand Down
4 changes: 3 additions & 1 deletion Arbie/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from Arbie.settings_parser import SettingsParser

logger = logging.getLogger()


class App(object):
"""App is used for configuring and running Arbie."""
Expand All @@ -15,7 +17,7 @@ def __init__(self, config):

async def run(self):
if self.action_tree is None:
logging.getLogger().warning("No actions given in configuration")
logger.warning("No actions given in configuration")
return
await self.action_tree.run()

Expand Down
42 changes: 38 additions & 4 deletions Arbie/resources/contracts/arbie/Arbie.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion Arbie/settings_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from Arbie.Actions import ActionTree, RedisState, Store
from Arbie.Contracts import (
Arbie,
BalancerFactory,
ContractFactory,
IERC20Token,
Expand Down Expand Up @@ -79,6 +80,9 @@ def set_up_balancer(self, config):
def set_up_token(self, config):
return self._set_up_contracts(config, IERC20Token).create_token(1)

def set_up_arbie(self, config):
return self._set_up_contracts(config, Arbie)

def _create_variable(self, variable_config): # noqa: WPS321
variable_type = variable_config[Keys.type_key]
if variable_type == "UniswapFactory":
Expand All @@ -87,6 +91,8 @@ def _create_variable(self, variable_config): # noqa: WPS321
return self.set_up_balancer(variable_config)
if variable_type == "Token":
return self.set_up_token(variable_config)
if variable_type == "Arbie":
return self.set_up_arbie(variable_config)
if variable_type == "float":
return float(variable_config[Keys.value])
if variable_type == "int":
Expand All @@ -95,7 +101,7 @@ def _create_variable(self, variable_config): # noqa: WPS321
return str(variable_config[Keys.value])
raise TypeError(f"No rule for creating variable if type {variable_type}")

def _set_up_contracts(self, config, contract):
def _set_up_contracts(self, config, contract, *kwargs):
factory = ContractFactory(self.w3, contract)
if Keys.network in config:
return factory.load_contract(network=to_network(config[Keys.network]))
Expand Down
42 changes: 42 additions & 0 deletions tests/system/Actions/trader_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""System tests for PoolUpdater."""

import pytest

from Arbie.Actions import ActionTree, Store, Trader
from Arbie.Contracts import GenericToken
from Arbie.Variables import BigNumber


class TestTrader(object):
@pytest.fixture
def trader_address(self, w3, weth: GenericToken, arbie, dummy_address):
weth.transfer(dummy_address, BigNumber(2))
weth.approve(arbie.get_address(), BigNumber(2), dummy_address)
return dummy_address

@pytest.fixture
def trade_store(self, w3, arbie, trade, weth, trader_address):
store = Store()
store.add("arbie", arbie)
store.add("filtered_trades", [trade])
store.add("weth", weth)
store.add("web3", w3)
store.add("trader_address", trader_address)
return store

@pytest.mark.asyncio
async def test_on_next(self, trade_store, trade):
trade.amount_in = 1
trade_store.add("min_profit", 0)
tree = ActionTree(trade_store)
tree.add_action(Trader({"input": {"min_profit": "min_profit"}, "output": {}}))
await tree.run()
assert trade_store.get("profit") > 0.036 # noqa: WPS432

@pytest.mark.asyncio
async def test_no_profit(self, trade_store, trade):
trade.amount_in = 1
tree = ActionTree(trade_store)
tree.add_action(Trader())
with pytest.raises(Exception):
await tree.run()
75 changes: 17 additions & 58 deletions tests/system/Contracts/arbie_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,29 @@
import pytest

from Arbie.Actions.arbitrage import ArbitrageFinder
from Arbie.async_helpers import async_map
from Arbie.Contracts import Arbie, ContractFactory
from Arbie.Variables import BigNumber, Trade


async def create_pool(pool):
return await pool.create_pool()


async def create_token(token):
return await token.create_token()


large = 10e8
from Arbie.Contracts import Arbie, GenericToken
from Arbie.Variables import BigNumber


class TestArbie(object):
@pytest.fixture
def arbie(self, w3, deploy_address, uniswap_router):
return ContractFactory(w3, Arbie).deploy_contract(
deploy_address, uniswap_router.get_address()
)

@pytest.fixture
async def pairs(self, factory, weth, dai, wbtc):
p0 = await factory.setup_pair(
[weth, dai],
[
BigNumber(large / 300.0),
BigNumber(large),
],
)
@pytest.mark.asyncio
def test_out_given_in(self, arbie: Arbie, trade):
trade.amount_in = 1
amount_out = arbie.check_out_given_in(trade)
assert amount_out > 1

p1 = await factory.setup_pair(
[wbtc, dai],
[
BigNumber(large / 10000.0),
BigNumber(large),
],
)
profit = ArbitrageFinder(trade).calculate_profit(1)
assert pytest.approx(1, 1e-5) == amount_out - profit

p2 = await factory.setup_pair(
[wbtc, weth],
[
BigNumber(large / 10000.0),
BigNumber(large / 285.0),
],
)
return await async_map(create_pool, [p0, p1, p2])
@pytest.mark.asyncio
async def test_swap(self, trade, arbie, weth: GenericToken, deploy_address):
trade.amount_in = 1
balance_before = await weth.balance_of(deploy_address)

@pytest.fixture
async def path(self, weth, dai, wbtc):
raw_path = [weth, dai, wbtc, weth]
return await async_map(create_token, raw_path)
assert balance_before > 1

@pytest.fixture
def trade(self, pairs, path):
return Trade(pairs, path)
weth.approve(arbie.get_address(), BigNumber(2))

@pytest.mark.asyncio
def test_out_given_in(self, arbie: Arbie, trade):
amount_in = BigNumber(1)
amount_out = arbie.check_out_given_in(amount_in, trade)
assert amount_out > amount_in.to_number()
assert arbie.swap(trade)

profit = ArbitrageFinder(trade).calculate_profit(1)
assert pytest.approx(1, 1e-5) == amount_out - profit
assert await weth.balance_of(deploy_address) > balance_before
12 changes: 0 additions & 12 deletions tests/system/Contracts/conftest.py

This file was deleted.

Loading

0 comments on commit 8fd32a0

Please sign in to comment.