disLodge commited on
Commit
77198ce
·
1 Parent(s): 82b9a25

Adding implementation for arbitrage

Browse files
README.md CHANGED
@@ -1,13 +1,12 @@
1
  ---
2
- title: Triangular Arbitrage
3
- emoji: 🏃
4
- colorFrom: red
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.5.1
8
  app_file: app.py
9
  pinned: false
10
- short_description: Code to detect triangular arbitrage opportunities on binance
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: Triangular Arbitrage Scanner
3
+ emoji: 💱
 
 
4
  sdk: gradio
5
+ sdk_version: "4.0"
6
  app_file: app.py
7
  pinned: false
 
8
  ---
9
 
10
+ # Triangular Arbitrage Opportunity Detector
11
+
12
+ Scans Binance for triangular arbitrage opportunities.
app.py CHANGED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import asyncio
3
+ import os
4
+ os.environ["USE_MINIMAL_LIBS"] = "true"
5
+
6
+ import gradio as gr
7
+ import triangular_arbitrage.detector as detector
8
+
9
+ EXCHANGE = "binance" # or "binanceus"
10
+ REFRESH_INTERVAL = 30 # seconds
11
+
12
+ def run_detection_sync():
13
+ best_opps, best_profit = asyncio.run(detector.run_detection(EXCHANGE, max_cycle=3))
14
+ if best_opps:
15
+ profit_pct = round((best_profit - 1) * 100, 5)
16
+ lines = [f"**{profit_pct}% opportunity found**\n"]
17
+ for i, opp in enumerate(best_opps):
18
+ side = 'buy' if opp.reversed else 'sell'
19
+ conn = 'with' if side == 'buy' else 'for'
20
+ lines.append(f"{i+1}. {side} {opp.symbol.base} {conn} {opp.symbol.quote} @ {opp.last_price:.5f}")
21
+ return "\n".join(lines)
22
+ return "No opportunity detected. Scanning..."
23
+
24
+ with gr.Blocks() as demo:
25
+ output = gr.Markdown(value="Loading...")
26
+ demo.load(run_detection_sync, inputs=None, outputs=output)
27
+ # Auto-refresh every REFRESH_INTERVAL seconds
28
+ demo.load(run_detection_sync, inputs=None, outputs=output, every=REFRESH_INTERVAL)
29
+
30
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ccxt
2
+ networkx[default]>=3.4, <3.5
3
+ OctoBot-Commons>=1.9, <1.10
4
+ gradio>=4.0
triangular_arbitrage/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ PROJECT_NAME = "OctoBot-Triangular-Arbitrage"
2
+ VERSION = "1.2.2"
triangular_arbitrage/detector.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pylint: disable=W0702, C0325
2
+
3
+ import ccxt.async_support as ccxt
4
+ from typing import List, Tuple
5
+ from dataclasses import dataclass
6
+ import networkx as nx
7
+
8
+ import octobot_commons.symbols as symbols
9
+ import octobot_commons.constants as constants
10
+
11
+
12
+ @dataclass
13
+ class ShortTicker:
14
+ symbol: symbols.Symbol
15
+ last_price: float
16
+ reversed: bool = False
17
+
18
+ def __repr__(self):
19
+ return f"ShortTicker(symbol={str(self.symbol)}, last_price={self.last_price}, reversed={self.reversed})"
20
+
21
+
22
+ async def fetch_tickers(exchange):
23
+ return await exchange.fetch_tickers() if exchange.has['fetchTickers'] else {}
24
+
25
+
26
+ def get_symbol_from_key(key_symbol: str) -> symbols.Symbol:
27
+ try:
28
+ return symbols.parse_symbol(key_symbol)
29
+ except:
30
+ return None
31
+
32
+
33
+ def is_delisted_symbols(exchange_time, ticker,
34
+ threshold=1 * constants.DAYS_TO_SECONDS * constants.MSECONDS_TO_SECONDS) -> bool:
35
+ ticker_time = ticker['timestamp']
36
+ return ticker_time is not None and not (exchange_time - ticker_time <= threshold)
37
+
38
+
39
+ def get_last_prices(exchange_time, tickers, ignored_symbols, whitelisted_symbols=None):
40
+ return [
41
+ ShortTicker(symbol=get_symbol_from_key(key),
42
+ last_price=tickers[key]['close'])
43
+ for key, _ in tickers.items()
44
+ if tickers[key]['close'] is not None
45
+ and not is_delisted_symbols(exchange_time, tickers[key])
46
+ and str(get_symbol_from_key(key)) not in ignored_symbols
47
+ and get_symbol_from_key(key).is_spot()
48
+ and (whitelisted_symbols is None or str(get_symbol_from_key(key)) in whitelisted_symbols)
49
+ ]
50
+
51
+
52
+ def get_best_triangular_opportunity(tickers: List[ShortTicker]) -> Tuple[List[ShortTicker], float]:
53
+ # Build a directed graph of currencies
54
+ return get_best_opportunity(tickers, 3)
55
+
56
+
57
+ def get_best_opportunity(tickers: List[ShortTicker], max_cycle: int = 10) -> Tuple[List[ShortTicker], float]:
58
+ # Build a directed graph of currencies
59
+ graph = nx.DiGraph()
60
+
61
+ for ticker in tickers:
62
+ if ticker.symbol is not None:
63
+ graph.add_edge(ticker.symbol.base, ticker.symbol.quote, ticker=ticker)
64
+ graph.add_edge(ticker.symbol.quote, ticker.symbol.base,
65
+ ticker=ShortTicker(symbols.Symbol(f"{ticker.symbol.quote}/{ticker.symbol.base}"),
66
+ 1 / ticker.last_price, reversed=True))
67
+
68
+ best_profit = 1
69
+ best_cycle = None
70
+
71
+ # Find all cycles in the graph with a length <= max_cycle
72
+ for cycle in nx.simple_cycles(graph):
73
+ if len(cycle) > max_cycle:
74
+ continue # Skip cycles longer than max_cycle
75
+
76
+ profit = 1
77
+ tickers_in_cycle = []
78
+
79
+ # Calculate the profits along the cycle
80
+ for i, base in enumerate(cycle):
81
+ quote = cycle[(i + 1) % len(cycle)] # Wrap around to complete the cycle
82
+ ticker = graph[base][quote]['ticker']
83
+ tickers_in_cycle.append(ticker)
84
+ profit *= ticker.last_price
85
+
86
+ if profit > best_profit:
87
+ best_profit = profit
88
+ best_cycle = tickers_in_cycle
89
+
90
+ if best_cycle is not None:
91
+ best_cycle = [
92
+ ShortTicker(symbols.Symbol(f"{ticker.symbol.quote}/{ticker.symbol.base}"), ticker.last_price, reversed=True)
93
+ if ticker.reversed else ticker
94
+ for ticker in best_cycle
95
+ ]
96
+
97
+ return best_cycle, best_profit
98
+
99
+
100
+ async def get_exchange_data(exchange_name):
101
+ exchange_class = getattr(ccxt, exchange_name)
102
+ exchange = exchange_class()
103
+ tickers = await fetch_tickers(exchange)
104
+ filtered_tickers = {
105
+ symbol: ticker
106
+ for symbol, ticker in tickers.items()
107
+ if exchange.markets.get(symbol, {}).get(
108
+ "active", True
109
+ ) is True
110
+ }
111
+ exchange_time = exchange.milliseconds()
112
+ await exchange.close()
113
+ return filtered_tickers, exchange_time
114
+
115
+
116
+ async def get_exchange_last_prices(exchange_name, ignored_symbols, whitelisted_symbols=None):
117
+ tickers, exchange_time = await get_exchange_data(exchange_name)
118
+ last_prices = get_last_prices(exchange_time, tickers, ignored_symbols, whitelisted_symbols)
119
+ return last_prices
120
+
121
+
122
+ async def run_detection(exchange_name, ignored_symbols=None, whitelisted_symbols=None, max_cycle=10):
123
+ last_prices = await get_exchange_last_prices(exchange_name, ignored_symbols or [], whitelisted_symbols)
124
+ # default is the best opportunity for all cycles
125
+ best_opportunity, best_profit = get_best_opportunity(last_prices, max_cycle=max_cycle)
126
+ return best_opportunity, best_profit