-
Notifications
You must be signed in to change notification settings - Fork 33
Implemented Cycle Detection #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a9c9c2f
0b661bd
176e7c4
45a3d6e
f55795e
5d4e1ae
3c2b439
db3f092
46e331f
05e5b00
dc6545e
586b350
2b49a7a
f07321b
3b7e6ee
01a6cdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
import pytest | ||
import octobot_commons.symbols as symbols | ||
from triangular_arbitrage.detector import ShortTicker, get_best_opportunity | ||
from triangular_arbitrage.detector import ShortTicker, get_best_triangular_opportunity, get_best_opportunity | ||
|
||
|
||
@pytest.fixture | ||
|
@@ -14,16 +14,28 @@ def sample_tickers(): | |
] | ||
|
||
|
||
def test_get_best_triangular_opportunity_handles_empty_tickers(): | ||
best_opportunity, best_profit = get_best_triangular_opportunity([]) | ||
assert best_profit == 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is necessary since the we need to multiply all the stock prices by the number. The reason why the old test (with However, for our new code, even if there is 0, 1, or 2 tickers, the multiplication occurs, and the base line for profit will be 1 since we are multiplying 1 by the exchange rates to maintain identity. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, you're right it's better without |
||
assert best_opportunity is None | ||
|
||
|
||
def test_get_best_triangular_opportunity_handles_no_cycle_opportunity(sample_tickers): | ||
sample_tickers.append(ShortTicker(symbol=symbols.Symbol('DOT/USDT'), last_price=0.05)) | ||
best_opportunity, best_profit = get_best_triangular_opportunity(sample_tickers) | ||
assert best_profit == 1 | ||
assert best_opportunity is None | ||
|
||
def test_get_best_opportunity_handles_empty_tickers(): | ||
best_opportunity, best_profit = get_best_opportunity([]) | ||
assert best_profit == 0 | ||
assert best_profit == 1 | ||
assert best_opportunity is None | ||
|
||
|
||
def test_get_best_opportunity_handles_no_triplet_opportunity(sample_tickers): | ||
sample_tickers.append(ShortTicker(symbol=symbols.Symbol('DOT/USDT'), last_price=0.05)) | ||
best_opportunity, best_profit = get_best_opportunity(sample_tickers) | ||
assert best_profit == 0 | ||
assert best_profit == 1 | ||
assert best_opportunity is None | ||
|
||
|
||
|
@@ -33,7 +45,7 @@ def test_get_best_opportunity_returns_correct_triplet_with_correct_tickers(): | |
ShortTicker(symbol=symbols.Symbol('ETH/BTC'), last_price=0.3), | ||
ShortTicker(symbol=symbols.Symbol('ETH/USDT'), last_price=2000), | ||
] | ||
best_opportunity, best_profit = get_best_opportunity(tickers) | ||
best_opportunity, best_profit = get_best_triangular_opportunity(tickers) | ||
assert len(best_opportunity) == 3 | ||
assert best_profit == 4.5 | ||
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(): | |
ShortTicker(symbol=symbols.Symbol('ETH/TUSD'), last_price=1950), | ||
ShortTicker(symbol=symbols.Symbol('BTC/TUSD'), last_price=32500), | ||
] | ||
best_opportunity, best_profit = get_best_opportunity(tickers) | ||
best_opportunity, best_profit = get_best_triangular_opportunity(tickers) | ||
assert len(best_opportunity) == 3 | ||
assert best_profit == 5.526315789473684 | ||
assert round(best_profit, 3) == 5.526 # 5.526315789473684 | ||
assert all(isinstance(ticker, ShortTicker) for ticker in best_opportunity) | ||
|
||
def test_get_best_opportunity_returns_correct_cycle_with_correct_tickers(): | ||
tickers = [ | ||
ShortTicker(symbol=symbols.Symbol('BTC/USDT'), last_price=30000), | ||
ShortTicker(symbol=symbols.Symbol('ETH/BTC'), last_price=0.3), | ||
ShortTicker(symbol=symbols.Symbol('ETH/USDT'), last_price=2000), | ||
] | ||
best_opportunity, best_profit = get_best_opportunity(tickers) | ||
assert len(best_opportunity) >= 3 # Handling cycles with more than 3 tickers | ||
assert best_profit == 4.5 | ||
assert all(isinstance(ticker, ShortTicker) for ticker in best_opportunity) | ||
|
||
|
||
def test_get_best_opportunity_returns_correct_cycle_with_multiple_tickers(): | ||
tickers = [ | ||
ShortTicker(symbol=symbols.Symbol('BTC/USDT'), last_price=30000), | ||
ShortTicker(symbol=symbols.Symbol('ETH/BTC'), last_price=0.3), | ||
ShortTicker(symbol=symbols.Symbol('ETH/USDT'), last_price=2000), | ||
ShortTicker(symbol=symbols.Symbol('ETH/USDC'), last_price=1900), | ||
ShortTicker(symbol=symbols.Symbol('BTC/USDC'), last_price=35000), | ||
ShortTicker(symbol=symbols.Symbol('USDC/USDT'), last_price=1.1), | ||
ShortTicker(symbol=symbols.Symbol('USDC/TUSD'), last_price=0.95), | ||
ShortTicker(symbol=symbols.Symbol('ETH/TUSD'), last_price=1950), | ||
ShortTicker(symbol=symbols.Symbol('BTC/TUSD'), last_price=32500), | ||
] | ||
best_opportunity, best_profit = get_best_opportunity(tickers) | ||
assert len(best_opportunity) >= 3 # Handling cycles with more than 3 tickers | ||
assert round(best_profit, 3) == 5.775 | ||
assert all(isinstance(ticker, ShortTicker) for ticker in best_opportunity) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,7 +45,11 @@ def get_last_prices(exchange_time, tickers, ignored_symbols, whitelisted_symbols | |
] | ||
|
||
|
||
def get_best_opportunity(tickers: List[ShortTicker]) -> Tuple[List[ShortTicker], float]: | ||
def get_best_triangular_opportunity(tickers: List[ShortTicker]) -> Tuple[List[ShortTicker], float]: | ||
# Build a directed graph of currencies | ||
return get_best_opportunity(tickers, 3) | ||
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's great! |
||
|
||
def get_best_opportunity(tickers: List[ShortTicker], max_cycle: int = 10) -> Tuple[List[ShortTicker], float]: | ||
# Build a directed graph of currencies | ||
graph = nx.DiGraph() | ||
|
||
|
@@ -56,34 +60,37 @@ def get_best_opportunity(tickers: List[ShortTicker]) -> Tuple[List[ShortTicker], | |
ticker=ShortTicker(symbols.Symbol(f"{ticker.symbol.quote}/{ticker.symbol.base}"), | ||
1 / ticker.last_price, reversed=True)) | ||
|
||
best_profit = 0 | ||
best_triplet = None | ||
best_profit = 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. base case must be = 1, since 100%-100% means no profit, and 1 is the multiplicative identity (we are multiplying the exchange rates, and not adding, the original best_profit = 0 does not make sense) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree |
||
best_cycle = None | ||
|
||
# Find all cycles in the graph with a length <= max_cycle | ||
for cycle in nx.simple_cycles(graph): | ||
if len(cycle) != 3: | ||
continue | ||
if len(cycle) > max_cycle: | ||
continue # Skip cycles longer than max_cycle | ||
|
||
a, b, c = cycle | ||
a_to_b = graph[a][b]['ticker'] | ||
b_to_c = graph[b][c]['ticker'] | ||
c_to_a = graph[c][a]['ticker'] | ||
profit = 1 | ||
tickers_in_cycle = [] | ||
|
||
profit = a_to_b.last_price * b_to_c.last_price * c_to_a.last_price | ||
# Calculate the profits along the cycle | ||
for i, base in enumerate(cycle): | ||
quote = cycle[(i + 1) % len(cycle)] # Wrap around to complete the cycle | ||
ticker = graph[base][quote]['ticker'] | ||
tickers_in_cycle.append(ticker) | ||
profit *= ticker.last_price | ||
|
||
if profit > best_profit: | ||
best_profit = profit | ||
best_triplet = [a_to_b, b_to_c, c_to_a] | ||
|
||
if best_triplet is not None: | ||
# restore original symbols for reversed pairs | ||
best_triplet = [ | ||
ShortTicker(symbols.Symbol(f"{triplet.symbol.quote}/{triplet.symbol.base}"), triplet.last_price, | ||
reversed=True) | ||
if triplet.reversed else triplet | ||
for triplet in best_triplet | ||
best_cycle = tickers_in_cycle | ||
|
||
if best_cycle is not None: | ||
best_cycle = [ | ||
ShortTicker(symbols.Symbol(f"{ticker.symbol.quote}/{ticker.symbol.base}"), ticker.last_price, reversed=True) | ||
if ticker.reversed else ticker | ||
for ticker in best_cycle | ||
] | ||
|
||
return best_triplet, best_profit | ||
return best_cycle, best_profit | ||
|
||
|
||
|
||
async def get_exchange_data(exchange_name): | ||
|
@@ -103,5 +110,5 @@ async def get_exchange_last_prices(exchange_name, ignored_symbols, whitelisted_s | |
|
||
async def run_detection(exchange_name, ignored_symbols=None, whitelisted_symbols=None): | ||
last_prices = await get_exchange_last_prices(exchange_name, ignored_symbols or [], whitelisted_symbols) | ||
best_opportunity, best_profit = get_best_opportunity(last_prices) | ||
best_opportunity, best_profit = get_best_opportunity(last_prices) # default is best opportunity for all cycles | ||
return best_opportunity, best_profit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great example!