Skip to content

Commit 368a785

Browse files
authored
Merge pull request #19 from ruidazeng/master
Implemented Cycle Detection
2 parents dc83079 + 01a6cdf commit 368a785

File tree

4 files changed

+108
-38
lines changed

4 files changed

+108
-38
lines changed

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
<img src="illustration.jpeg" width="250px" height="250px" alt="Triangular illustration">
33
</p>
44

5-
# Triangular Arbitrage by OctoBot [1.1.1](https://github.com/Drakkar-Software/Triangular-Arbitrage/blob/master/CHANGELOG.md)
5+
# Arbitrage Opportunity Detection by OctoBot [1.1.1](https://github.com/Drakkar-Software/Triangular-Arbitrage/blob/master/CHANGELOG.md)
66
[![PyPI](https://img.shields.io/pypi/v/OctoBot-Triangular-Arbitrage.svg)](https://pypi.python.org/pypi/OctoBot-Triangular-Arbitrage/)
77
[![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/octobot-triangular-arbitrage.svg?logo=docker)](https://hub.docker.com/r/drakkarsoftware/octobot-triangular-arbitrage)
88

9-
This Python-based project utilizes the [ccxt library](https://github.com/ccxt/ccxt) and [OctoBot library](https://github.com/Drakkar-Software/OctoBot) to detect potential triangular arbitrage opportunities in cryptocurrency markets.
9+
This Python-based project utilizes the [ccxt library](https://github.com/ccxt/ccxt) and the [OctoBot library](https://github.com/Drakkar-Software/OctoBot) to detect potential arbitrage opportunities across multiple assets in cryptocurrency markets. It identifies profitable cycles where you can trade through a series of assets and return to the original asset with a potential gain, making it applicable for arbitrage strategies beyond just triangular cycles.
1010

1111
## Description
1212

13-
Triangular arbitrage is a process where you trade from one currency to another, and then to another, and finally back to the original currency. The goal is to exploit differences in prices between the three currencies to make a profit. For example, you could start with USD, buy BTC, then use the BTC to buy ETH, and finally sell the ETH for USD. If the prices are right, you could end up with more USD than you started with. This project provides a method to identify the best triangular arbitrage opportunity given a list of last prices for different cryptocurrency pairs. It's a simple and effective tool for anyone interested in cryptocurrency trading and arbitrage strategies.
13+
Arbitrage trading is a process where you trade from one asset or currency to another, and then continue trading through a series of assets until you eventually return to the original asset or currency. The goal is to exploit price differences between multiple assets to generate a profit. For example, you could start with USD, buy BTC, use the BTC to buy ETH, trade the ETH for XRP, and finally sell the XRP back to USD. If the prices are favorable throughout the cycle, you could end up with more USD than you started with. This project provides a method to identify the best arbitrage opportunities in a multi-asset cycle, given a list of last prices for different cryptocurrency pairs. It's a versatile and effective tool for anyone interested in cryptocurrency trading and arbitrage strategies across various currencies and assets.
1414

1515
## Getting Started
1616

@@ -33,10 +33,14 @@ python3 main.py
3333
Example output on Binance:
3434
```
3535
-------------------------------------------
36-
New 1.0354% binance opportunity:
37-
1. sell WIN/BNB
38-
2. sell BNB/BRL
39-
3. buy WIN/BRL
36+
New 2.33873% binanceus opportunity:
37+
# 1. buy DOGE to BTC at 552486.18785
38+
# 2. sell DOGE to USDT at 0.12232
39+
# 3. buy ETH to USDT at 0.00038
40+
# 4. buy ADA to ETH at 7570.02271
41+
# 5. sell ADA to USDC at 0.35000
42+
# 6. buy SOL to USDC at 0.00662
43+
# 7. sell SOL to BTC at 0.00226
4044
-------------------------------------------
4145
```
4246

main.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
# start arbitrage detection
1616
print("Scanning...")
17-
exchange_name = "binance"
17+
exchange_name = "binance" # allow pickable exchange_id from https://github.com/ccxt/ccxt/wiki/manual#exchanges
18+
1819
best_opportunities, best_profit = asyncio.run(detector.run_detection(exchange_name))
1920

2021

@@ -29,9 +30,26 @@ def get_order_side(opportunity: detector.ShortTicker):
2930
if best_opportunities is not None:
3031
# Display arbitrage detection result
3132
print("-------------------------------------------")
32-
print(f"New {round(best_profit - 1, 5) * 100}% {exchange_name} opportunity:")
33-
for i in range(3):
34-
print(f"{i + 1}. {get_order_side(best_opportunities[i])} {str(best_opportunities[i].symbol)}")
33+
total_profit_percentage = round((best_profit - 1) * 100, 5)
34+
print(f"New {total_profit_percentage}% {exchange_name} opportunity:")
35+
for i, opportunity in enumerate(best_opportunities):
36+
# Get the base and quote currencies
37+
base_currency = opportunity.symbol.base
38+
quote_currency = opportunity.symbol.quote
39+
40+
# Format the output as below (real live example):
41+
# -------------------------------------------
42+
# New 2.33873% binanceus opportunity:
43+
# 1. buy DOGE to BTC at 552486.18785
44+
# 2. sell DOGE to USDT at 0.12232
45+
# 3. buy ETH to USDT at 0.00038
46+
# 4. buy ADA to ETH at 7570.02271
47+
# 5. sell ADA to USDC at 0.35000
48+
# 6. buy SOL to USDC at 0.00662
49+
# 7. sell SOL to BTC at 0.00226
50+
# -------------------------------------------
51+
order_side = get_order_side(opportunity)
52+
print(f"{i+1}. {order_side} {base_currency} to {quote_currency} at {opportunity.last_price:.5f}")
3553
print("-------------------------------------------")
3654
else:
3755
print("No opportunity detected")

tests/test_detector.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
import octobot_commons.symbols as symbols
3-
from triangular_arbitrage.detector import ShortTicker, get_best_opportunity
3+
from triangular_arbitrage.detector import ShortTicker, get_best_triangular_opportunity, get_best_opportunity
44

55

66
@pytest.fixture
@@ -14,16 +14,28 @@ def sample_tickers():
1414
]
1515

1616

17+
def test_get_best_triangular_opportunity_handles_empty_tickers():
18+
best_opportunity, best_profit = get_best_triangular_opportunity([])
19+
assert best_profit == 1
20+
assert best_opportunity is None
21+
22+
23+
def test_get_best_triangular_opportunity_handles_no_cycle_opportunity(sample_tickers):
24+
sample_tickers.append(ShortTicker(symbol=symbols.Symbol('DOT/USDT'), last_price=0.05))
25+
best_opportunity, best_profit = get_best_triangular_opportunity(sample_tickers)
26+
assert best_profit == 1
27+
assert best_opportunity is None
28+
1729
def test_get_best_opportunity_handles_empty_tickers():
1830
best_opportunity, best_profit = get_best_opportunity([])
19-
assert best_profit == 0
31+
assert best_profit == 1
2032
assert best_opportunity is None
2133

2234

2335
def test_get_best_opportunity_handles_no_triplet_opportunity(sample_tickers):
2436
sample_tickers.append(ShortTicker(symbol=symbols.Symbol('DOT/USDT'), last_price=0.05))
2537
best_opportunity, best_profit = get_best_opportunity(sample_tickers)
26-
assert best_profit == 0
38+
assert best_profit == 1
2739
assert best_opportunity is None
2840

2941

@@ -33,7 +45,7 @@ def test_get_best_opportunity_returns_correct_triplet_with_correct_tickers():
3345
ShortTicker(symbol=symbols.Symbol('ETH/BTC'), last_price=0.3),
3446
ShortTicker(symbol=symbols.Symbol('ETH/USDT'), last_price=2000),
3547
]
36-
best_opportunity, best_profit = get_best_opportunity(tickers)
48+
best_opportunity, best_profit = get_best_triangular_opportunity(tickers)
3749
assert len(best_opportunity) == 3
3850
assert best_profit == 4.5
3951
assert all(isinstance(ticker, ShortTicker) for ticker in best_opportunity)
@@ -51,7 +63,36 @@ def test_get_best_opportunity_returns_correct_triplet_with_multiple_tickers():
5163
ShortTicker(symbol=symbols.Symbol('ETH/TUSD'), last_price=1950),
5264
ShortTicker(symbol=symbols.Symbol('BTC/TUSD'), last_price=32500),
5365
]
54-
best_opportunity, best_profit = get_best_opportunity(tickers)
66+
best_opportunity, best_profit = get_best_triangular_opportunity(tickers)
5567
assert len(best_opportunity) == 3
56-
assert best_profit == 5.526315789473684
68+
assert round(best_profit, 3) == 5.526 # 5.526315789473684
69+
assert all(isinstance(ticker, ShortTicker) for ticker in best_opportunity)
70+
71+
def test_get_best_opportunity_returns_correct_cycle_with_correct_tickers():
72+
tickers = [
73+
ShortTicker(symbol=symbols.Symbol('BTC/USDT'), last_price=30000),
74+
ShortTicker(symbol=symbols.Symbol('ETH/BTC'), last_price=0.3),
75+
ShortTicker(symbol=symbols.Symbol('ETH/USDT'), last_price=2000),
76+
]
77+
best_opportunity, best_profit = get_best_opportunity(tickers)
78+
assert len(best_opportunity) >= 3 # Handling cycles with more than 3 tickers
79+
assert best_profit == 4.5
80+
assert all(isinstance(ticker, ShortTicker) for ticker in best_opportunity)
81+
82+
83+
def test_get_best_opportunity_returns_correct_cycle_with_multiple_tickers():
84+
tickers = [
85+
ShortTicker(symbol=symbols.Symbol('BTC/USDT'), last_price=30000),
86+
ShortTicker(symbol=symbols.Symbol('ETH/BTC'), last_price=0.3),
87+
ShortTicker(symbol=symbols.Symbol('ETH/USDT'), last_price=2000),
88+
ShortTicker(symbol=symbols.Symbol('ETH/USDC'), last_price=1900),
89+
ShortTicker(symbol=symbols.Symbol('BTC/USDC'), last_price=35000),
90+
ShortTicker(symbol=symbols.Symbol('USDC/USDT'), last_price=1.1),
91+
ShortTicker(symbol=symbols.Symbol('USDC/TUSD'), last_price=0.95),
92+
ShortTicker(symbol=symbols.Symbol('ETH/TUSD'), last_price=1950),
93+
ShortTicker(symbol=symbols.Symbol('BTC/TUSD'), last_price=32500),
94+
]
95+
best_opportunity, best_profit = get_best_opportunity(tickers)
96+
assert len(best_opportunity) >= 3 # Handling cycles with more than 3 tickers
97+
assert round(best_profit, 3) == 5.775
5798
assert all(isinstance(ticker, ShortTicker) for ticker in best_opportunity)

triangular_arbitrage/detector.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ def get_last_prices(exchange_time, tickers, ignored_symbols, whitelisted_symbols
4545
]
4646

4747

48-
def get_best_opportunity(tickers: List[ShortTicker]) -> Tuple[List[ShortTicker], float]:
48+
def get_best_triangular_opportunity(tickers: List[ShortTicker]) -> Tuple[List[ShortTicker], float]:
49+
# Build a directed graph of currencies
50+
return get_best_opportunity(tickers, 3)
51+
52+
def get_best_opportunity(tickers: List[ShortTicker], max_cycle: int = 10) -> Tuple[List[ShortTicker], float]:
4953
# Build a directed graph of currencies
5054
graph = nx.DiGraph()
5155

@@ -56,34 +60,37 @@ def get_best_opportunity(tickers: List[ShortTicker]) -> Tuple[List[ShortTicker],
5660
ticker=ShortTicker(symbols.Symbol(f"{ticker.symbol.quote}/{ticker.symbol.base}"),
5761
1 / ticker.last_price, reversed=True))
5862

59-
best_profit = 0
60-
best_triplet = None
63+
best_profit = 1
64+
best_cycle = None
6165

66+
# Find all cycles in the graph with a length <= max_cycle
6267
for cycle in nx.simple_cycles(graph):
63-
if len(cycle) != 3:
64-
continue
68+
if len(cycle) > max_cycle:
69+
continue # Skip cycles longer than max_cycle
6570

66-
a, b, c = cycle
67-
a_to_b = graph[a][b]['ticker']
68-
b_to_c = graph[b][c]['ticker']
69-
c_to_a = graph[c][a]['ticker']
71+
profit = 1
72+
tickers_in_cycle = []
7073

71-
profit = a_to_b.last_price * b_to_c.last_price * c_to_a.last_price
74+
# Calculate the profits along the cycle
75+
for i, base in enumerate(cycle):
76+
quote = cycle[(i + 1) % len(cycle)] # Wrap around to complete the cycle
77+
ticker = graph[base][quote]['ticker']
78+
tickers_in_cycle.append(ticker)
79+
profit *= ticker.last_price
7280

7381
if profit > best_profit:
7482
best_profit = profit
75-
best_triplet = [a_to_b, b_to_c, c_to_a]
76-
77-
if best_triplet is not None:
78-
# restore original symbols for reversed pairs
79-
best_triplet = [
80-
ShortTicker(symbols.Symbol(f"{triplet.symbol.quote}/{triplet.symbol.base}"), triplet.last_price,
81-
reversed=True)
82-
if triplet.reversed else triplet
83-
for triplet in best_triplet
83+
best_cycle = tickers_in_cycle
84+
85+
if best_cycle is not None:
86+
best_cycle = [
87+
ShortTicker(symbols.Symbol(f"{ticker.symbol.quote}/{ticker.symbol.base}"), ticker.last_price, reversed=True)
88+
if ticker.reversed else ticker
89+
for ticker in best_cycle
8490
]
8591

86-
return best_triplet, best_profit
92+
return best_cycle, best_profit
93+
8794

8895

8996
async def get_exchange_data(exchange_name):
@@ -103,5 +110,5 @@ async def get_exchange_last_prices(exchange_name, ignored_symbols, whitelisted_s
103110

104111
async def run_detection(exchange_name, ignored_symbols=None, whitelisted_symbols=None):
105112
last_prices = await get_exchange_last_prices(exchange_name, ignored_symbols or [], whitelisted_symbols)
106-
best_opportunity, best_profit = get_best_opportunity(last_prices)
113+
best_opportunity, best_profit = get_best_opportunity(last_prices) # default is best opportunity for all cycles
107114
return best_opportunity, best_profit

0 commit comments

Comments
 (0)