File size: 10,739 Bytes
2dee41b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
"""
Fee calculation module for triangular arbitrage.
Handles trading fees (maker/taker) and slippage for different exchanges.
"""

from typing import Optional, Dict
from dataclasses import dataclass
import ccxt.async_support as ccxt


@dataclass
class ExchangeFees:
    """Fee structure for an exchange."""
    maker_fee: float  # Maker fee as decimal (e.g., 0.001 for 0.1%)
    taker_fee: float  # Taker fee as decimal (e.g., 0.001 for 0.1%)
    name: str


# Standard fee structures for exchanges
EXCHANGE_FEES: Dict[str, ExchangeFees] = {
    "binance": ExchangeFees(
        maker_fee=0.001,  # 0.1%
        taker_fee=0.001,  # 0.1%
        name="Binance"
    ),
    "binanceus": ExchangeFees(
        maker_fee=0.001,  # 0.1%
        taker_fee=0.001,  # 0.1%
        name="Binance US"
    ),
    "huobi": ExchangeFees(
        maker_fee=0.002,  # 0.2%
        taker_fee=0.002,  # 0.2%
        name="Huobi"
    ),
    "htx": ExchangeFees(  # Huobi rebranded as HTX
        maker_fee=0.002,  # 0.2%
        taker_fee=0.002,  # 0.2%
        name="HTX"
    ),
}


def get_exchange_fees(exchange_name: str) -> ExchangeFees:
    """
    Get fee structure for an exchange.
    
    Args:
        exchange_name: Name of the exchange (e.g., 'binance', 'huobi')
    
    Returns:
        ExchangeFees object with maker and taker fees
    """
    # Normalize exchange name
    exchange_name_lower = exchange_name.lower()
    
    # Check direct match
    if exchange_name_lower in EXCHANGE_FEES:
        return EXCHANGE_FEES[exchange_name_lower]
    
    # Check partial matches
    for key, fees in EXCHANGE_FEES.items():
        if key in exchange_name_lower or exchange_name_lower in key:
            return fees
    
    # Default fees (conservative estimate)
    return ExchangeFees(
        maker_fee=0.002,  # 0.2% default
        taker_fee=0.002,  # 0.2% default
        name=exchange_name
    )


async def fetch_user_fee_tier(
    exchange_name: str,
    api_key: Optional[str] = None,
    api_secret: Optional[str] = None
) -> Optional[ExchangeFees]:
    """
    Fetch actual fee tier from exchange API if API keys are provided.
    This allows for VIP tier discounts and BNB/HT holdings discounts.
    
    Args:
        exchange_name: Name of the exchange
        api_key: API key for authenticated requests
        api_secret: API secret for authenticated requests
    
    Returns:
        ExchangeFees with actual user fees, or None if not available
    """
    if not api_key or not api_secret:
        return None
    
    try:
        exchange_class = getattr(ccxt, exchange_name)
        exchange = exchange_class({
            'apiKey': api_key,
            'secret': api_secret,
            'enableRateLimit': True,
            'options': {
                'defaultType': 'spot',  # Ensure we're using spot trading
            }
        })
        
        # Try to fetch fee information
        exchange_name_lower = exchange_name.lower()
        
        if exchange_name_lower in ['binance', 'binanceus']:
            try:
                # Binance: fetch trading fees
                if hasattr(exchange, 'fetch_trading_fees'):
                    fees = await exchange.fetch_trading_fees()
                    if fees and 'trading' in fees:
                        trading_fees = fees['trading']
                        # Get fees for a common trading pair (e.g., BTC/USDT)
                        if 'BTC/USDT' in trading_fees:
                            btc_fees = trading_fees['BTC/USDT']
                            maker_fee = btc_fees.get('maker', 0.001)
                            taker_fee = btc_fees.get('taker', 0.001)
                            await exchange.close()
                            return ExchangeFees(
                                maker_fee=maker_fee,
                                taker_fee=taker_fee,
                                name=f"{exchange_name} (User Tier)"
                            )
                
                # Alternative: try fetchBalance which sometimes includes fee info
                balance = await exchange.fetch_balance()
                if 'info' in balance:
                    info = balance['info']
                    # Binance account info might contain fee tier
                    # This varies by exchange API version
                    pass
                    
            except Exception as e:
                # If specific method fails, try general approach
                pass
        
        elif exchange_name_lower in ['huobi', 'htx']:
            try:
                # Huobi: fetch trading fees
                if hasattr(exchange, 'fetch_trading_fees'):
                    fees = await exchange.fetch_trading_fees()
                    if fees and 'trading' in fees:
                        trading_fees = fees['trading']
                        if 'BTC/USDT' in trading_fees:
                            btc_fees = trading_fees['BTC/USDT']
                            maker_fee = btc_fees.get('maker', 0.002)
                            taker_fee = btc_fees.get('taker', 0.002)
                            await exchange.close()
                            return ExchangeFees(
                                maker_fee=maker_fee,
                                taker_fee=taker_fee,
                                name=f"{exchange_name} (User Tier)"
                            )
            except Exception:
                pass
        
        # Try generic fetchTradingFees if available
        try:
            if hasattr(exchange, 'fetch_trading_fees'):
                fees = await exchange.fetch_trading_fees()
                if fees:
                    # Extract maker/taker from first available trading pair
                    if isinstance(fees, dict) and 'trading' in fees:
                        trading_fees = fees['trading']
                        for pair, pair_fees in trading_fees.items():
                            if isinstance(pair_fees, dict):
                                maker_fee = pair_fees.get('maker')
                                taker_fee = pair_fees.get('taker')
                                if maker_fee is not None and taker_fee is not None:
                                    await exchange.close()
                                    return ExchangeFees(
                                        maker_fee=maker_fee,
                                        taker_fee=taker_fee,
                                        name=f"{exchange_name} (User Tier)"
                                    )
        except Exception:
            pass
        
        await exchange.close()
        
    except Exception:
        # If fetching fails, return None to use default fees
        pass
    
    return None


def calculate_trading_fees(
    amount: float,
    fee_rate: float,
    num_trades: int = 3
) -> float:
    """
    Calculate total trading fees for a triangular arbitrage cycle.
    
    Args:
        amount: Starting amount (e.g., 1.0 for calculating profit ratio)
        fee_rate: Fee rate per trade (as decimal, e.g., 0.001 for 0.1%)
        num_trades: Number of trades in the cycle (default 3 for triangular)
    
    Returns:
        Total fees as a multiplier (e.g., 0.003 for 0.3% total fees)
    """
    # For triangular arbitrage, we pay fees on each of the 3 trades
    # Fee is deducted from the amount received, so we multiply by (1 - fee_rate) for each trade
    return (1 - fee_rate) ** num_trades


def calculate_slippage(
    trade_size_usd: float = 1000.0,
    liquidity_factor: float = 0.0005  # 0.05% slippage per $1000 traded
) -> float:
    """
    Calculate slippage as a percentage based on trade size and liquidity.
    
    Args:
        trade_size_usd: Estimated trade size in USD (default 1000)
        liquidity_factor: Slippage factor per $1000 (default 0.0005 = 0.05%)
                         Higher liquidity = lower slippage
    
    Returns:
        Slippage multiplier (e.g., 0.9995 for 0.05% slippage)
    """
    # Slippage increases with trade size
    # Formula: slippage = 1 - (trade_size / 1000) * liquidity_factor
    slippage_percentage = min((trade_size_usd / 1000.0) * liquidity_factor, 0.01)  # Cap at 1%
    return 1 - slippage_percentage


def calculate_net_profit(
    gross_profit: float,
    exchange_fees: ExchangeFees,
    num_trades: int = 3,
    use_taker: bool = True,
    trade_size_usd: float = 1000.0,
    slippage_factor: float = 0.0005
) -> float:
    """
    Calculate net profit after fees and slippage.
    
    Args:
        gross_profit: Gross profit multiplier (e.g., 1.002 for 0.2% profit)
        exchange_fees: ExchangeFees object with maker/taker rates
        num_trades: Number of trades in the cycle (default 3)
        use_taker: Whether to use taker fees (True) or maker fees (False)
        trade_size_usd: Estimated trade size for slippage calculation
        slippage_factor: Slippage factor per $1000 traded
    
    Returns:
        Net profit multiplier after fees and slippage
    """
    # Select appropriate fee rate
    fee_rate = exchange_fees.taker_fee if use_taker else exchange_fees.maker_fee
    
    # Calculate fee impact
    fee_multiplier = calculate_trading_fees(1.0, fee_rate, num_trades)
    
    # Calculate slippage impact (applied to each trade)
    slippage_per_trade = calculate_slippage(trade_size_usd, slippage_factor)
    slippage_multiplier = slippage_per_trade ** num_trades
    
    # Net profit = gross profit * fee multiplier * slippage multiplier
    net_profit = gross_profit * fee_multiplier * slippage_multiplier
    
    return net_profit


def get_fee_breakdown(
    gross_profit: float,
    exchange_fees: ExchangeFees,
    num_trades: int = 3,
    use_taker: bool = True,
    trade_size_usd: float = 1000.0,
    slippage_factor: float = 0.0005
) -> Dict[str, float]:
    """
    Get detailed breakdown of fees and slippage.
    
    Returns:
        Dictionary with gross_profit, total_fees, total_slippage, net_profit
    """
    fee_rate = exchange_fees.taker_fee if use_taker else exchange_fees.maker_fee
    fee_multiplier = calculate_trading_fees(1.0, fee_rate, num_trades)
    slippage_per_trade = calculate_slippage(trade_size_usd, slippage_factor)
    slippage_multiplier = slippage_per_trade ** num_trades
    
    total_fees = 1 - fee_multiplier
    total_slippage = 1 - slippage_multiplier
    net_profit = gross_profit * fee_multiplier * slippage_multiplier
    
    return {
        "gross_profit": gross_profit - 1,
        "total_fees": total_fees,
        "total_slippage": total_slippage,
        "net_profit": net_profit - 1,
        "fee_rate_per_trade": fee_rate,
        "slippage_per_trade": 1 - slippage_per_trade,
    }