disLodge commited on
Commit
2dee41b
·
1 Parent(s): 1fc7165

Adding fees inclusion

Browse files
app.py CHANGED
@@ -2,26 +2,51 @@ import gradio as gr
2
  import asyncio
3
  from triangular_arbitrage import detector
4
 
5
- async def run_detection_ui(exchange_name):
 
 
 
 
 
 
 
 
6
  try:
7
- best_opps, best_profit = await detector.run_detection(
8
  exchange_name=exchange_name,
9
  max_cycle=3, # fixed triangular only
 
 
 
 
 
 
10
  )
11
 
12
  if not best_opps:
13
  return "No arbitrage opportunity found."
14
 
15
  result_lines = []
16
- for ticker in best_opps:
 
17
  result_lines.append(
18
- f"{ticker.symbol} | price: {ticker.last_price:.8f}"
19
  )
20
 
21
- result_text = "\n".join(result_lines)
22
- result_text += f"\n\n**Total cycle profit:** {best_profit:.6f}"
 
 
 
 
 
 
 
 
 
 
23
 
24
- return result_text
25
 
26
  except Exception as e:
27
  return f"Error while scanning: {str(e)}"
@@ -29,24 +54,71 @@ async def run_detection_ui(exchange_name):
29
 
30
  with gr.Blocks(title="Triangular Arbitrage Scanner") as demo:
31
  gr.Markdown("# Triangular Arbitrage Scanner")
 
32
 
33
- exchange = gr.Dropdown(
34
- choices=[
35
- "binanceus", # safer default for Spaces
36
- # "kraken",
37
- # "bitget",
38
- # "bybit",
39
- # "okx",
40
- # "coinbase",
41
- # "kucoin",
42
- # "gateio",
43
- "huobi",
44
- ],
45
- value="kraken",
46
- label="Exchange",
47
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- scan_btn = gr.Button("Scan for opportunities", variant="primary")
50
 
51
  output = gr.Markdown(
52
  value="Select exchange and click **Scan**.",
@@ -55,7 +127,15 @@ with gr.Blocks(title="Triangular Arbitrage Scanner") as demo:
55
 
56
  scan_btn.click(
57
  fn=run_detection_ui,
58
- inputs=exchange,
 
 
 
 
 
 
 
 
59
  outputs=output,
60
  )
61
 
 
2
  import asyncio
3
  from triangular_arbitrage import detector
4
 
5
+ async def run_detection_ui(
6
+ exchange_name,
7
+ include_fees,
8
+ use_taker,
9
+ trade_size_usd,
10
+ slippage_factor,
11
+ api_key,
12
+ api_secret
13
+ ):
14
  try:
15
+ best_opps, best_profit, fee_breakdown = await detector.run_detection(
16
  exchange_name=exchange_name,
17
  max_cycle=3, # fixed triangular only
18
+ include_fees=include_fees,
19
+ use_taker=use_taker,
20
+ trade_size_usd=trade_size_usd,
21
+ slippage_factor=slippage_factor,
22
+ api_key=api_key if api_key else None,
23
+ api_secret=api_secret if api_secret else None,
24
  )
25
 
26
  if not best_opps:
27
  return "No arbitrage opportunity found."
28
 
29
  result_lines = []
30
+ result_lines.append("## Trading Cycle:")
31
+ for i, ticker in enumerate(best_opps, 1):
32
  result_lines.append(
33
+ f"{i}. {ticker.symbol} | price: {ticker.last_price:.8f}"
34
  )
35
 
36
+ result_lines.append("\n## Profit Analysis:")
37
+
38
+ if include_fees and fee_breakdown:
39
+ result_lines.append(f"**Gross Profit:** {fee_breakdown['gross_profit']*100:.4f}%")
40
+ result_lines.append(f"**Trading Fees:** {fee_breakdown['total_fees']*100:.4f}% ({fee_breakdown['fee_rate_per_trade']*100:.2f}% per trade)")
41
+ result_lines.append(f"**Slippage:** {fee_breakdown['total_slippage']*100:.4f}% ({fee_breakdown['slippage_per_trade']*100:.4f}% per trade)")
42
+ result_lines.append(f"**Net Profit:** {fee_breakdown['net_profit']*100:.4f}%")
43
+ result_lines.append(f"\n**Net Profit Multiplier:** {best_profit:.8f}")
44
+ else:
45
+ result_lines.append(f"**Gross Profit:** {(best_profit - 1)*100:.4f}%")
46
+ result_lines.append(f"**Profit Multiplier:** {best_profit:.8f}")
47
+ result_lines.append("\n*Note: Fees and slippage not included. Enable 'Include Fees' to see net profit.*")
48
 
49
+ return "\n".join(result_lines)
50
 
51
  except Exception as e:
52
  return f"Error while scanning: {str(e)}"
 
54
 
55
  with gr.Blocks(title="Triangular Arbitrage Scanner") as demo:
56
  gr.Markdown("# Triangular Arbitrage Scanner")
57
+ gr.Markdown("Scan for triangular arbitrage opportunities with fee and slippage calculations.")
58
 
59
+ with gr.Row():
60
+ with gr.Column():
61
+ exchange = gr.Dropdown(
62
+ choices=[
63
+ "binanceus",
64
+ "binance",
65
+ "huobi",
66
+ "htx",
67
+ ],
68
+ value="binanceus",
69
+ label="Exchange",
70
+ )
71
+
72
+ include_fees = gr.Checkbox(
73
+ value=True,
74
+ label="Include Fees & Slippage",
75
+ info="Calculate net profit after trading fees and slippage"
76
+ )
77
+
78
+ use_taker = gr.Radio(
79
+ choices=[("Taker Fees", True), ("Maker Fees", False)],
80
+ value=True,
81
+ label="Fee Type",
82
+ info="Taker fees are typically higher but execute immediately"
83
+ )
84
+
85
+ trade_size_usd = gr.Slider(
86
+ minimum=100,
87
+ maximum=100000,
88
+ value=1000,
89
+ step=100,
90
+ label="Trade Size (USD)",
91
+ info="Estimated trade size for slippage calculation"
92
+ )
93
+
94
+ slippage_factor = gr.Slider(
95
+ minimum=0.0001,
96
+ maximum=0.002,
97
+ value=0.0005,
98
+ step=0.0001,
99
+ label="Slippage Factor",
100
+ info="Slippage per $1000 traded (0.0005 = 0.05% per $1000)"
101
+ )
102
+
103
+ with gr.Column():
104
+ gr.Markdown("### API Keys (Optional)")
105
+ gr.Markdown("Provide API keys to fetch your actual fee tier (VIP discounts, etc.)")
106
+
107
+ api_key = gr.Textbox(
108
+ label="API Key",
109
+ type="password",
110
+ placeholder="Enter your API key (optional)",
111
+ info="Used to fetch your actual fee tier"
112
+ )
113
+
114
+ api_secret = gr.Textbox(
115
+ label="API Secret",
116
+ type="password",
117
+ placeholder="Enter your API secret (optional)",
118
+ info="Used to fetch your actual fee tier"
119
+ )
120
 
121
+ scan_btn = gr.Button("Scan for opportunities", variant="primary", size="lg")
122
 
123
  output = gr.Markdown(
124
  value="Select exchange and click **Scan**.",
 
127
 
128
  scan_btn.click(
129
  fn=run_detection_ui,
130
+ inputs=[
131
+ exchange,
132
+ include_fees,
133
+ use_taker,
134
+ trade_size_usd,
135
+ slippage_factor,
136
+ api_key,
137
+ api_secret
138
+ ],
139
  outputs=output,
140
  )
141
 
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  ccxt
2
  networkx[default]>=3.4, <3.5
3
- OctoBot-Commons>=1.9, <1.10
 
 
1
  ccxt
2
  networkx[default]>=3.4, <3.5
3
+ OctoBot-Commons>=1.9, <1.10
4
+ gradio
triangular_arbitrage/detector.py CHANGED
@@ -1,12 +1,19 @@
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
@@ -54,7 +61,33 @@ def get_best_triangular_opportunity(tickers: List[ShortTicker]) -> Tuple[List[Sh
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
 
@@ -67,25 +100,53 @@ def get_best_opportunity(tickers: List[ShortTicker], max_cycle: int = 10) -> Tup
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 = [
@@ -94,7 +155,7 @@ def get_best_opportunity(tickers: List[ShortTicker], max_cycle: int = 10) -> Tup
94
  for ticker in best_cycle
95
  ]
96
 
97
- return best_cycle, best_profit
98
 
99
 
100
  async def get_exchange_data(exchange_name):
@@ -119,8 +180,60 @@ async def get_exchange_last_prices(exchange_name, ignored_symbols, whitelisted_s
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # pylint: disable=W0702, C0325
2
 
3
  import ccxt.async_support as ccxt
4
+ from typing import List, Tuple, Optional, Dict
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
+ from triangular_arbitrage.fees import (
11
+ get_exchange_fees,
12
+ calculate_net_profit,
13
+ get_fee_breakdown,
14
+ ExchangeFees,
15
+ fetch_user_fee_tier
16
+ )
17
 
18
 
19
  @dataclass
 
61
  return get_best_opportunity(tickers, 3)
62
 
63
 
64
+ def get_best_opportunity(
65
+ tickers: List[ShortTicker],
66
+ max_cycle: int = 10,
67
+ exchange_fees: Optional[ExchangeFees] = None,
68
+ use_taker: bool = True,
69
+ trade_size_usd: float = 1000.0,
70
+ slippage_factor: float = 0.0005,
71
+ include_fees: bool = True
72
+ ) -> Tuple[List[ShortTicker], float, Optional[Dict[str, float]]]:
73
+ """
74
+ Find the best arbitrage opportunity.
75
+
76
+ Args:
77
+ tickers: List of tickers with prices
78
+ max_cycle: Maximum cycle length to consider
79
+ exchange_fees: Exchange fee structure (if None, fees won't be applied)
80
+ use_taker: Whether to use taker fees (True) or maker fees (False)
81
+ trade_size_usd: Estimated trade size for slippage calculation
82
+ slippage_factor: Slippage factor per $1000 traded
83
+ include_fees: Whether to calculate net profit with fees and slippage
84
+
85
+ Returns:
86
+ Tuple of (best_cycle, best_profit, fee_breakdown)
87
+ - best_cycle: List of tickers in the best cycle
88
+ - best_profit: Best profit multiplier (gross if include_fees=False, net if include_fees=True)
89
+ - fee_breakdown: Dictionary with fee details (None if include_fees=False)
90
+ """
91
  # Build a directed graph of currencies
92
  graph = nx.DiGraph()
93
 
 
100
 
101
  best_profit = 1
102
  best_cycle = None
103
+ best_gross_profit = 1
104
+ best_fee_breakdown = None
105
 
106
  # Find all cycles in the graph with a length <= max_cycle
107
  for cycle in nx.simple_cycles(graph):
108
  if len(cycle) > max_cycle:
109
  continue # Skip cycles longer than max_cycle
110
 
111
+ gross_profit = 1
112
  tickers_in_cycle = []
113
 
114
+ # Calculate the gross profits along the cycle
115
  for i, base in enumerate(cycle):
116
  quote = cycle[(i + 1) % len(cycle)] # Wrap around to complete the cycle
117
  ticker = graph[base][quote]['ticker']
118
  tickers_in_cycle.append(ticker)
119
+ gross_profit *= ticker.last_price
120
+
121
+ # Calculate net profit if fees should be included
122
+ if include_fees and exchange_fees is not None:
123
+ net_profit = calculate_net_profit(
124
+ gross_profit,
125
+ exchange_fees,
126
+ num_trades=len(cycle),
127
+ use_taker=use_taker,
128
+ trade_size_usd=trade_size_usd,
129
+ slippage_factor=slippage_factor
130
+ )
131
+ profit_to_compare = net_profit
132
+ else:
133
+ profit_to_compare = gross_profit
134
+
135
+ if profit_to_compare > best_profit:
136
+ best_profit = profit_to_compare
137
+ best_gross_profit = gross_profit
138
  best_cycle = tickers_in_cycle
139
+
140
+ # Calculate fee breakdown if fees are included
141
+ if include_fees and exchange_fees is not None:
142
+ best_fee_breakdown = get_fee_breakdown(
143
+ gross_profit,
144
+ exchange_fees,
145
+ num_trades=len(cycle),
146
+ use_taker=use_taker,
147
+ trade_size_usd=trade_size_usd,
148
+ slippage_factor=slippage_factor
149
+ )
150
 
151
  if best_cycle is not None:
152
  best_cycle = [
 
155
  for ticker in best_cycle
156
  ]
157
 
158
+ return best_cycle, best_profit, best_fee_breakdown
159
 
160
 
161
  async def get_exchange_data(exchange_name):
 
180
  return last_prices
181
 
182
 
183
+ async def run_detection(
184
+ exchange_name,
185
+ ignored_symbols=None,
186
+ whitelisted_symbols=None,
187
+ max_cycle=10,
188
+ include_fees=True,
189
+ use_taker=True,
190
+ trade_size_usd=1000.0,
191
+ slippage_factor=0.0005,
192
+ api_key=None,
193
+ api_secret=None
194
+ ):
195
+ """
196
+ Run arbitrage detection with optional fee calculation.
197
+
198
+ Args:
199
+ exchange_name: Name of the exchange
200
+ ignored_symbols: List of symbols to ignore
201
+ whitelisted_symbols: List of symbols to include (None = all)
202
+ max_cycle: Maximum cycle length
203
+ include_fees: Whether to include fees and slippage in profit calculation
204
+ use_taker: Whether to use taker fees (True) or maker fees (False)
205
+ trade_size_usd: Estimated trade size for slippage calculation
206
+ slippage_factor: Slippage factor per $1000 traded
207
+ api_key: Optional API key for fetching actual fee tiers
208
+ api_secret: Optional API secret for fetching actual fee tiers
209
+
210
+ Returns:
211
+ Tuple of (best_opportunity, best_profit, fee_breakdown)
212
+ """
213
  last_prices = await get_exchange_last_prices(exchange_name, ignored_symbols or [], whitelisted_symbols)
214
+
215
+ # Get exchange fees
216
+ exchange_fees = None
217
+ if include_fees:
218
+ # Try to fetch user-specific fees if API keys are provided
219
+ if api_key and api_secret:
220
+ user_fees = await fetch_user_fee_tier(exchange_name, api_key, api_secret)
221
+ if user_fees:
222
+ exchange_fees = user_fees
223
+
224
+ # Fall back to standard fees
225
+ if exchange_fees is None:
226
+ exchange_fees = get_exchange_fees(exchange_name)
227
+
228
+ # Find best opportunity with fees
229
+ best_opportunity, best_profit, fee_breakdown = get_best_opportunity(
230
+ last_prices,
231
+ max_cycle=max_cycle,
232
+ exchange_fees=exchange_fees,
233
+ use_taker=use_taker,
234
+ trade_size_usd=trade_size_usd,
235
+ slippage_factor=slippage_factor,
236
+ include_fees=include_fees
237
+ )
238
+
239
+ return best_opportunity, best_profit, fee_breakdown
triangular_arbitrage/fees.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fee calculation module for triangular arbitrage.
3
+ Handles trading fees (maker/taker) and slippage for different exchanges.
4
+ """
5
+
6
+ from typing import Optional, Dict
7
+ from dataclasses import dataclass
8
+ import ccxt.async_support as ccxt
9
+
10
+
11
+ @dataclass
12
+ class ExchangeFees:
13
+ """Fee structure for an exchange."""
14
+ maker_fee: float # Maker fee as decimal (e.g., 0.001 for 0.1%)
15
+ taker_fee: float # Taker fee as decimal (e.g., 0.001 for 0.1%)
16
+ name: str
17
+
18
+
19
+ # Standard fee structures for exchanges
20
+ EXCHANGE_FEES: Dict[str, ExchangeFees] = {
21
+ "binance": ExchangeFees(
22
+ maker_fee=0.001, # 0.1%
23
+ taker_fee=0.001, # 0.1%
24
+ name="Binance"
25
+ ),
26
+ "binanceus": ExchangeFees(
27
+ maker_fee=0.001, # 0.1%
28
+ taker_fee=0.001, # 0.1%
29
+ name="Binance US"
30
+ ),
31
+ "huobi": ExchangeFees(
32
+ maker_fee=0.002, # 0.2%
33
+ taker_fee=0.002, # 0.2%
34
+ name="Huobi"
35
+ ),
36
+ "htx": ExchangeFees( # Huobi rebranded as HTX
37
+ maker_fee=0.002, # 0.2%
38
+ taker_fee=0.002, # 0.2%
39
+ name="HTX"
40
+ ),
41
+ }
42
+
43
+
44
+ def get_exchange_fees(exchange_name: str) -> ExchangeFees:
45
+ """
46
+ Get fee structure for an exchange.
47
+
48
+ Args:
49
+ exchange_name: Name of the exchange (e.g., 'binance', 'huobi')
50
+
51
+ Returns:
52
+ ExchangeFees object with maker and taker fees
53
+ """
54
+ # Normalize exchange name
55
+ exchange_name_lower = exchange_name.lower()
56
+
57
+ # Check direct match
58
+ if exchange_name_lower in EXCHANGE_FEES:
59
+ return EXCHANGE_FEES[exchange_name_lower]
60
+
61
+ # Check partial matches
62
+ for key, fees in EXCHANGE_FEES.items():
63
+ if key in exchange_name_lower or exchange_name_lower in key:
64
+ return fees
65
+
66
+ # Default fees (conservative estimate)
67
+ return ExchangeFees(
68
+ maker_fee=0.002, # 0.2% default
69
+ taker_fee=0.002, # 0.2% default
70
+ name=exchange_name
71
+ )
72
+
73
+
74
+ async def fetch_user_fee_tier(
75
+ exchange_name: str,
76
+ api_key: Optional[str] = None,
77
+ api_secret: Optional[str] = None
78
+ ) -> Optional[ExchangeFees]:
79
+ """
80
+ Fetch actual fee tier from exchange API if API keys are provided.
81
+ This allows for VIP tier discounts and BNB/HT holdings discounts.
82
+
83
+ Args:
84
+ exchange_name: Name of the exchange
85
+ api_key: API key for authenticated requests
86
+ api_secret: API secret for authenticated requests
87
+
88
+ Returns:
89
+ ExchangeFees with actual user fees, or None if not available
90
+ """
91
+ if not api_key or not api_secret:
92
+ return None
93
+
94
+ try:
95
+ exchange_class = getattr(ccxt, exchange_name)
96
+ exchange = exchange_class({
97
+ 'apiKey': api_key,
98
+ 'secret': api_secret,
99
+ 'enableRateLimit': True,
100
+ 'options': {
101
+ 'defaultType': 'spot', # Ensure we're using spot trading
102
+ }
103
+ })
104
+
105
+ # Try to fetch fee information
106
+ exchange_name_lower = exchange_name.lower()
107
+
108
+ if exchange_name_lower in ['binance', 'binanceus']:
109
+ try:
110
+ # Binance: fetch trading fees
111
+ if hasattr(exchange, 'fetch_trading_fees'):
112
+ fees = await exchange.fetch_trading_fees()
113
+ if fees and 'trading' in fees:
114
+ trading_fees = fees['trading']
115
+ # Get fees for a common trading pair (e.g., BTC/USDT)
116
+ if 'BTC/USDT' in trading_fees:
117
+ btc_fees = trading_fees['BTC/USDT']
118
+ maker_fee = btc_fees.get('maker', 0.001)
119
+ taker_fee = btc_fees.get('taker', 0.001)
120
+ await exchange.close()
121
+ return ExchangeFees(
122
+ maker_fee=maker_fee,
123
+ taker_fee=taker_fee,
124
+ name=f"{exchange_name} (User Tier)"
125
+ )
126
+
127
+ # Alternative: try fetchBalance which sometimes includes fee info
128
+ balance = await exchange.fetch_balance()
129
+ if 'info' in balance:
130
+ info = balance['info']
131
+ # Binance account info might contain fee tier
132
+ # This varies by exchange API version
133
+ pass
134
+
135
+ except Exception as e:
136
+ # If specific method fails, try general approach
137
+ pass
138
+
139
+ elif exchange_name_lower in ['huobi', 'htx']:
140
+ try:
141
+ # Huobi: fetch trading fees
142
+ if hasattr(exchange, 'fetch_trading_fees'):
143
+ fees = await exchange.fetch_trading_fees()
144
+ if fees and 'trading' in fees:
145
+ trading_fees = fees['trading']
146
+ if 'BTC/USDT' in trading_fees:
147
+ btc_fees = trading_fees['BTC/USDT']
148
+ maker_fee = btc_fees.get('maker', 0.002)
149
+ taker_fee = btc_fees.get('taker', 0.002)
150
+ await exchange.close()
151
+ return ExchangeFees(
152
+ maker_fee=maker_fee,
153
+ taker_fee=taker_fee,
154
+ name=f"{exchange_name} (User Tier)"
155
+ )
156
+ except Exception:
157
+ pass
158
+
159
+ # Try generic fetchTradingFees if available
160
+ try:
161
+ if hasattr(exchange, 'fetch_trading_fees'):
162
+ fees = await exchange.fetch_trading_fees()
163
+ if fees:
164
+ # Extract maker/taker from first available trading pair
165
+ if isinstance(fees, dict) and 'trading' in fees:
166
+ trading_fees = fees['trading']
167
+ for pair, pair_fees in trading_fees.items():
168
+ if isinstance(pair_fees, dict):
169
+ maker_fee = pair_fees.get('maker')
170
+ taker_fee = pair_fees.get('taker')
171
+ if maker_fee is not None and taker_fee is not None:
172
+ await exchange.close()
173
+ return ExchangeFees(
174
+ maker_fee=maker_fee,
175
+ taker_fee=taker_fee,
176
+ name=f"{exchange_name} (User Tier)"
177
+ )
178
+ except Exception:
179
+ pass
180
+
181
+ await exchange.close()
182
+
183
+ except Exception:
184
+ # If fetching fails, return None to use default fees
185
+ pass
186
+
187
+ return None
188
+
189
+
190
+ def calculate_trading_fees(
191
+ amount: float,
192
+ fee_rate: float,
193
+ num_trades: int = 3
194
+ ) -> float:
195
+ """
196
+ Calculate total trading fees for a triangular arbitrage cycle.
197
+
198
+ Args:
199
+ amount: Starting amount (e.g., 1.0 for calculating profit ratio)
200
+ fee_rate: Fee rate per trade (as decimal, e.g., 0.001 for 0.1%)
201
+ num_trades: Number of trades in the cycle (default 3 for triangular)
202
+
203
+ Returns:
204
+ Total fees as a multiplier (e.g., 0.003 for 0.3% total fees)
205
+ """
206
+ # For triangular arbitrage, we pay fees on each of the 3 trades
207
+ # Fee is deducted from the amount received, so we multiply by (1 - fee_rate) for each trade
208
+ return (1 - fee_rate) ** num_trades
209
+
210
+
211
+ def calculate_slippage(
212
+ trade_size_usd: float = 1000.0,
213
+ liquidity_factor: float = 0.0005 # 0.05% slippage per $1000 traded
214
+ ) -> float:
215
+ """
216
+ Calculate slippage as a percentage based on trade size and liquidity.
217
+
218
+ Args:
219
+ trade_size_usd: Estimated trade size in USD (default 1000)
220
+ liquidity_factor: Slippage factor per $1000 (default 0.0005 = 0.05%)
221
+ Higher liquidity = lower slippage
222
+
223
+ Returns:
224
+ Slippage multiplier (e.g., 0.9995 for 0.05% slippage)
225
+ """
226
+ # Slippage increases with trade size
227
+ # Formula: slippage = 1 - (trade_size / 1000) * liquidity_factor
228
+ slippage_percentage = min((trade_size_usd / 1000.0) * liquidity_factor, 0.01) # Cap at 1%
229
+ return 1 - slippage_percentage
230
+
231
+
232
+ def calculate_net_profit(
233
+ gross_profit: float,
234
+ exchange_fees: ExchangeFees,
235
+ num_trades: int = 3,
236
+ use_taker: bool = True,
237
+ trade_size_usd: float = 1000.0,
238
+ slippage_factor: float = 0.0005
239
+ ) -> float:
240
+ """
241
+ Calculate net profit after fees and slippage.
242
+
243
+ Args:
244
+ gross_profit: Gross profit multiplier (e.g., 1.002 for 0.2% profit)
245
+ exchange_fees: ExchangeFees object with maker/taker rates
246
+ num_trades: Number of trades in the cycle (default 3)
247
+ use_taker: Whether to use taker fees (True) or maker fees (False)
248
+ trade_size_usd: Estimated trade size for slippage calculation
249
+ slippage_factor: Slippage factor per $1000 traded
250
+
251
+ Returns:
252
+ Net profit multiplier after fees and slippage
253
+ """
254
+ # Select appropriate fee rate
255
+ fee_rate = exchange_fees.taker_fee if use_taker else exchange_fees.maker_fee
256
+
257
+ # Calculate fee impact
258
+ fee_multiplier = calculate_trading_fees(1.0, fee_rate, num_trades)
259
+
260
+ # Calculate slippage impact (applied to each trade)
261
+ slippage_per_trade = calculate_slippage(trade_size_usd, slippage_factor)
262
+ slippage_multiplier = slippage_per_trade ** num_trades
263
+
264
+ # Net profit = gross profit * fee multiplier * slippage multiplier
265
+ net_profit = gross_profit * fee_multiplier * slippage_multiplier
266
+
267
+ return net_profit
268
+
269
+
270
+ def get_fee_breakdown(
271
+ gross_profit: float,
272
+ exchange_fees: ExchangeFees,
273
+ num_trades: int = 3,
274
+ use_taker: bool = True,
275
+ trade_size_usd: float = 1000.0,
276
+ slippage_factor: float = 0.0005
277
+ ) -> Dict[str, float]:
278
+ """
279
+ Get detailed breakdown of fees and slippage.
280
+
281
+ Returns:
282
+ Dictionary with gross_profit, total_fees, total_slippage, net_profit
283
+ """
284
+ fee_rate = exchange_fees.taker_fee if use_taker else exchange_fees.maker_fee
285
+ fee_multiplier = calculate_trading_fees(1.0, fee_rate, num_trades)
286
+ slippage_per_trade = calculate_slippage(trade_size_usd, slippage_factor)
287
+ slippage_multiplier = slippage_per_trade ** num_trades
288
+
289
+ total_fees = 1 - fee_multiplier
290
+ total_slippage = 1 - slippage_multiplier
291
+ net_profit = gross_profit * fee_multiplier * slippage_multiplier
292
+
293
+ return {
294
+ "gross_profit": gross_profit - 1,
295
+ "total_fees": total_fees,
296
+ "total_slippage": total_slippage,
297
+ "net_profit": net_profit - 1,
298
+ "fee_rate_per_trade": fee_rate,
299
+ "slippage_per_trade": 1 - slippage_per_trade,
300
+ }