@intentsolutionsio/flash-loan-simulator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Flash loan strategy simulation engine.
4
+
5
+ Implements various flash loan strategies:
6
+ - Simple arbitrage (2 DEX)
7
+ - Triangular arbitrage (3+ DEX)
8
+ - Liquidation
9
+ - Collateral swap
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass, field
14
+ from decimal import Decimal
15
+ from enum import Enum
16
+ from typing import List, Optional, Dict, Any
17
+
18
+ from protocol_adapters import ProviderManager, FlashLoanProvider
19
+
20
+
21
+ class StrategyType(Enum):
22
+ """Types of flash loan strategies."""
23
+
24
+ SIMPLE_ARBITRAGE = "arbitrage"
25
+ TRIANGULAR_ARBITRAGE = "triangular"
26
+ LIQUIDATION = "liquidation"
27
+ COLLATERAL_SWAP = "collateral_swap"
28
+ SELF_LIQUIDATION = "self_liquidation"
29
+ DEBT_REFINANCING = "refinancing"
30
+
31
+
32
+ @dataclass
33
+ class DEXPrice:
34
+ """Price quote from a DEX."""
35
+
36
+ dex_name: str
37
+ input_token: str
38
+ output_token: str
39
+ input_amount: Decimal
40
+ output_amount: Decimal
41
+ price: Decimal # output per input
42
+ price_impact: float # percentage
43
+ liquidity: Decimal # available liquidity
44
+
45
+
46
+ @dataclass
47
+ class TransactionStep:
48
+ """Single step in a flash loan transaction."""
49
+
50
+ step_number: int
51
+ action: str # "borrow", "swap", "repay", etc.
52
+ protocol: str
53
+ input_token: str
54
+ input_amount: Decimal
55
+ output_token: str
56
+ output_amount: Decimal
57
+ fee: Decimal
58
+ gas_estimate: int
59
+ description: str
60
+
61
+
62
+ @dataclass
63
+ class StrategyResult:
64
+ """Result of a strategy simulation."""
65
+
66
+ strategy_type: StrategyType
67
+ success: bool
68
+ steps: List[TransactionStep]
69
+ loan_amount: Decimal
70
+ loan_asset: str
71
+ loan_provider: str
72
+ loan_fee: Decimal
73
+ gross_profit: Decimal
74
+ total_fees: Decimal
75
+ gas_cost_eth: Decimal
76
+ gas_cost_usd: Decimal
77
+ net_profit: Decimal
78
+ net_profit_usd: Decimal
79
+ roi_percent: float
80
+ execution_path: str
81
+ warnings: List[str] = field(default_factory=list)
82
+ metadata: Dict[str, Any] = field(default_factory=dict)
83
+
84
+
85
+ @dataclass
86
+ class ArbitrageParams:
87
+ """Parameters for arbitrage simulation."""
88
+
89
+ input_token: str
90
+ output_token: str
91
+ amount: Decimal
92
+ dex_buy: str # DEX with lower price (buy here)
93
+ dex_sell: str # DEX with higher price (sell here)
94
+ provider: str = "aave"
95
+ chain: str = "ethereum"
96
+ slippage: float = 0.5 # percentage
97
+
98
+
99
+ @dataclass
100
+ class LiquidationParams:
101
+ """Parameters for liquidation simulation."""
102
+
103
+ protocol: str # "aave", "compound"
104
+ borrower: Optional[str] = None
105
+ health_factor_threshold: float = 1.0
106
+ debt_asset: str = "USDC"
107
+ collateral_asset: str = "ETH"
108
+ provider: str = "aave"
109
+ chain: str = "ethereum"
110
+
111
+
112
+ class FlashLoanStrategy(ABC):
113
+ """Abstract base class for flash loan strategies."""
114
+
115
+ def __init__(self, provider_manager: ProviderManager):
116
+ """Initialize with provider manager."""
117
+ self.provider_manager = provider_manager
118
+
119
+ @property
120
+ @abstractmethod
121
+ def strategy_type(self) -> StrategyType:
122
+ """Get strategy type."""
123
+ pass
124
+
125
+ @abstractmethod
126
+ def simulate(self, params: Any) -> StrategyResult:
127
+ """Run strategy simulation."""
128
+ pass
129
+
130
+ def get_provider(self, name: str) -> Optional[FlashLoanProvider]:
131
+ """Get flash loan provider by name."""
132
+ return self.provider_manager.get_provider(name)
133
+
134
+
135
+ class SimpleArbitrageStrategy(FlashLoanStrategy):
136
+ """
137
+ Simple two-DEX arbitrage strategy.
138
+
139
+ 1. Flash borrow asset A
140
+ 2. Sell A for B on DEX with high A price
141
+ 3. Buy A with B on DEX with low A price
142
+ 4. Repay flash loan + fee
143
+ 5. Keep profit
144
+ """
145
+
146
+ # Mock DEX prices for simulation
147
+ # In production, these would be fetched from actual DEXs
148
+ MOCK_PRICES = {
149
+ "uniswap": {
150
+ ("ETH", "USDC"): Decimal("2543.22"),
151
+ ("USDC", "ETH"): Decimal("1") / Decimal("2543.22"),
152
+ ("WBTC", "ETH"): Decimal("15.5"),
153
+ ("ETH", "WBTC"): Decimal("1") / Decimal("15.5"),
154
+ },
155
+ "sushiswap": {
156
+ ("ETH", "USDC"): Decimal("2538.50"),
157
+ ("USDC", "ETH"): Decimal("1") / Decimal("2538.50"),
158
+ ("WBTC", "ETH"): Decimal("15.48"),
159
+ ("ETH", "WBTC"): Decimal("1") / Decimal("15.48"),
160
+ },
161
+ "curve": {
162
+ ("ETH", "USDC"): Decimal("2540.00"),
163
+ ("USDC", "ETH"): Decimal("1") / Decimal("2540.00"),
164
+ },
165
+ }
166
+
167
+ # Gas costs per DEX
168
+ DEX_GAS = {
169
+ "uniswap": 150000,
170
+ "sushiswap": 140000,
171
+ "curve": 200000,
172
+ "balancer": 180000,
173
+ }
174
+
175
+ @property
176
+ def strategy_type(self) -> StrategyType:
177
+ return StrategyType.SIMPLE_ARBITRAGE
178
+
179
+ def get_dex_price(
180
+ self, dex: str, from_token: str, to_token: str
181
+ ) -> Optional[Decimal]:
182
+ """Get price from DEX (mock data)."""
183
+ dex_prices = self.MOCK_PRICES.get(dex.lower(), {})
184
+ return dex_prices.get((from_token.upper(), to_token.upper()))
185
+
186
+ def simulate(self, params: ArbitrageParams) -> StrategyResult:
187
+ """
188
+ Simulate simple arbitrage.
189
+
190
+ Flow:
191
+ 1. Borrow {amount} {input_token} from flash loan
192
+ 2. Sell on {dex_sell} → get {output_token}
193
+ 3. Buy on {dex_buy} → get back {input_token}
194
+ 4. Repay loan + fee
195
+ """
196
+ steps = []
197
+ warnings = []
198
+
199
+ # Get provider
200
+ provider = self.get_provider(params.provider)
201
+ if not provider:
202
+ return self._error_result(f"Unknown provider: {params.provider}")
203
+
204
+ # Step 1: Flash borrow
205
+ loan_fee = provider.get_fee(params.input_token, params.amount)
206
+ steps.append(
207
+ TransactionStep(
208
+ step_number=1,
209
+ action="flash_borrow",
210
+ protocol=provider.name,
211
+ input_token=params.input_token,
212
+ input_amount=Decimal("0"),
213
+ output_token=params.input_token,
214
+ output_amount=params.amount,
215
+ fee=loan_fee,
216
+ gas_estimate=provider.get_gas_overhead(),
217
+ description=f"Flash borrow {params.amount} {params.input_token} from {provider.name}",
218
+ )
219
+ )
220
+
221
+ # Step 2: Sell on high-price DEX
222
+ sell_price = self.get_dex_price(
223
+ params.dex_sell, params.input_token, params.output_token
224
+ )
225
+ if not sell_price:
226
+ warnings.append(f"No price data for {params.dex_sell}")
227
+ sell_price = Decimal("2540") # Fallback
228
+
229
+ sell_output = params.amount * sell_price
230
+ sell_gas = self.DEX_GAS.get(params.dex_sell.lower(), 150000)
231
+
232
+ steps.append(
233
+ TransactionStep(
234
+ step_number=2,
235
+ action="swap",
236
+ protocol=params.dex_sell,
237
+ input_token=params.input_token,
238
+ input_amount=params.amount,
239
+ output_token=params.output_token,
240
+ output_amount=sell_output,
241
+ fee=Decimal("0"), # DEX fee included in price
242
+ gas_estimate=sell_gas,
243
+ description=f"Sell {params.amount} {params.input_token} on {params.dex_sell}",
244
+ )
245
+ )
246
+
247
+ # Step 3: Buy on low-price DEX
248
+ buy_price = self.get_dex_price(
249
+ params.dex_buy, params.output_token, params.input_token
250
+ )
251
+ if not buy_price:
252
+ warnings.append(f"No price data for {params.dex_buy}")
253
+ buy_price = Decimal("1") / Decimal("2538") # Fallback
254
+
255
+ buy_output = sell_output * buy_price
256
+ buy_gas = self.DEX_GAS.get(params.dex_buy.lower(), 150000)
257
+
258
+ steps.append(
259
+ TransactionStep(
260
+ step_number=3,
261
+ action="swap",
262
+ protocol=params.dex_buy,
263
+ input_token=params.output_token,
264
+ input_amount=sell_output,
265
+ output_token=params.input_token,
266
+ output_amount=buy_output,
267
+ fee=Decimal("0"),
268
+ gas_estimate=buy_gas,
269
+ description=f"Buy {params.input_token} on {params.dex_buy}",
270
+ )
271
+ )
272
+
273
+ # Step 4: Repay flash loan
274
+ repay_amount = params.amount + loan_fee
275
+ steps.append(
276
+ TransactionStep(
277
+ step_number=4,
278
+ action="flash_repay",
279
+ protocol=provider.name,
280
+ input_token=params.input_token,
281
+ input_amount=repay_amount,
282
+ output_token=params.input_token,
283
+ output_amount=Decimal("0"),
284
+ fee=Decimal("0"),
285
+ gas_estimate=50000,
286
+ description=f"Repay {repay_amount} {params.input_token} to {provider.name}",
287
+ )
288
+ )
289
+
290
+ # Calculate profit
291
+ gross_profit = buy_output - params.amount
292
+ total_gas = sum(s.gas_estimate for s in steps)
293
+
294
+ # Assume 30 gwei gas price and $2500 ETH
295
+ gas_price_gwei = 30
296
+ eth_price = Decimal("2500")
297
+ gas_cost_eth = Decimal(total_gas * gas_price_gwei) / Decimal("1e9")
298
+ gas_cost_usd = gas_cost_eth * eth_price
299
+
300
+ net_profit = gross_profit - loan_fee - gas_cost_eth
301
+ net_profit_usd = net_profit * eth_price
302
+
303
+ # Check profitability
304
+ if net_profit < 0:
305
+ warnings.append("Strategy is NOT profitable after costs")
306
+
307
+ roi = float(net_profit / params.amount * 100) if params.amount > 0 else 0
308
+
309
+ return StrategyResult(
310
+ strategy_type=self.strategy_type,
311
+ success=net_profit > 0,
312
+ steps=steps,
313
+ loan_amount=params.amount,
314
+ loan_asset=params.input_token,
315
+ loan_provider=provider.name,
316
+ loan_fee=loan_fee,
317
+ gross_profit=gross_profit,
318
+ total_fees=loan_fee,
319
+ gas_cost_eth=gas_cost_eth,
320
+ gas_cost_usd=gas_cost_usd,
321
+ net_profit=net_profit,
322
+ net_profit_usd=net_profit_usd,
323
+ roi_percent=roi,
324
+ execution_path=f"{params.dex_sell} → {params.dex_buy}",
325
+ warnings=warnings,
326
+ metadata={
327
+ "sell_price": float(sell_price),
328
+ "buy_price": float(buy_price),
329
+ "price_spread": float((sell_price - Decimal("1") / buy_price) / sell_price * 100),
330
+ },
331
+ )
332
+
333
+ def _error_result(self, message: str) -> StrategyResult:
334
+ """Create error result."""
335
+ return StrategyResult(
336
+ strategy_type=self.strategy_type,
337
+ success=False,
338
+ steps=[],
339
+ loan_amount=Decimal("0"),
340
+ loan_asset="",
341
+ loan_provider="",
342
+ loan_fee=Decimal("0"),
343
+ gross_profit=Decimal("0"),
344
+ total_fees=Decimal("0"),
345
+ gas_cost_eth=Decimal("0"),
346
+ gas_cost_usd=Decimal("0"),
347
+ net_profit=Decimal("0"),
348
+ net_profit_usd=Decimal("0"),
349
+ roi_percent=0,
350
+ execution_path="",
351
+ warnings=[message],
352
+ )
353
+
354
+
355
+ class LiquidationStrategy(FlashLoanStrategy):
356
+ """
357
+ Liquidation strategy using flash loans.
358
+
359
+ 1. Flash borrow debt asset
360
+ 2. Repay borrower's debt on lending protocol
361
+ 3. Receive collateral + liquidation bonus
362
+ 4. Swap collateral back to debt asset
363
+ 5. Repay flash loan
364
+ 6. Keep liquidation bonus
365
+ """
366
+
367
+ # Liquidation bonuses by protocol
368
+ LIQUIDATION_BONUSES = {
369
+ "aave": Decimal("0.05"), # 5% bonus
370
+ "compound": Decimal("0.08"), # 8% bonus
371
+ }
372
+
373
+ @property
374
+ def strategy_type(self) -> StrategyType:
375
+ return StrategyType.LIQUIDATION
376
+
377
+ def simulate(self, params: LiquidationParams) -> StrategyResult:
378
+ """Simulate liquidation strategy."""
379
+ steps = []
380
+ warnings = []
381
+
382
+ # Get provider
383
+ provider = self.get_provider(params.provider)
384
+ if not provider:
385
+ return self._error_result(f"Unknown provider: {params.provider}")
386
+
387
+ # Mock position data
388
+ # In production, this would be fetched from the lending protocol
389
+ debt_amount = Decimal("10000") # 10K USDC debt
390
+ collateral_amount = Decimal("5") # 5 ETH collateral
391
+ collateral_price = Decimal("2500") # $2500/ETH
392
+ collateral_value = collateral_amount * collateral_price # $12,500
393
+
394
+ liquidation_bonus = self.LIQUIDATION_BONUSES.get(
395
+ params.protocol.lower(), Decimal("0.05")
396
+ )
397
+
398
+ # Step 1: Flash borrow debt asset
399
+ loan_fee = provider.get_fee(params.debt_asset, debt_amount)
400
+ steps.append(
401
+ TransactionStep(
402
+ step_number=1,
403
+ action="flash_borrow",
404
+ protocol=provider.name,
405
+ input_token=params.debt_asset,
406
+ input_amount=Decimal("0"),
407
+ output_token=params.debt_asset,
408
+ output_amount=debt_amount,
409
+ fee=loan_fee,
410
+ gas_estimate=provider.get_gas_overhead(),
411
+ description=f"Flash borrow {debt_amount} {params.debt_asset}",
412
+ )
413
+ )
414
+
415
+ # Step 2: Liquidate position
416
+ collateral_received = collateral_amount * (1 + liquidation_bonus)
417
+ steps.append(
418
+ TransactionStep(
419
+ step_number=2,
420
+ action="liquidate",
421
+ protocol=params.protocol,
422
+ input_token=params.debt_asset,
423
+ input_amount=debt_amount,
424
+ output_token=params.collateral_asset,
425
+ output_amount=collateral_received,
426
+ fee=Decimal("0"),
427
+ gas_estimate=300000,
428
+ description=f"Liquidate position, receive {collateral_received} {params.collateral_asset}",
429
+ )
430
+ )
431
+
432
+ # Step 3: Swap collateral to debt asset
433
+ swap_output = collateral_received * collateral_price
434
+ steps.append(
435
+ TransactionStep(
436
+ step_number=3,
437
+ action="swap",
438
+ protocol="Uniswap",
439
+ input_token=params.collateral_asset,
440
+ input_amount=collateral_received,
441
+ output_token=params.debt_asset,
442
+ output_amount=swap_output,
443
+ fee=Decimal("0"),
444
+ gas_estimate=150000,
445
+ description=f"Swap {params.collateral_asset} to {params.debt_asset}",
446
+ )
447
+ )
448
+
449
+ # Step 4: Repay flash loan
450
+ repay_amount = debt_amount + loan_fee
451
+ steps.append(
452
+ TransactionStep(
453
+ step_number=4,
454
+ action="flash_repay",
455
+ protocol=provider.name,
456
+ input_token=params.debt_asset,
457
+ input_amount=repay_amount,
458
+ output_token=params.debt_asset,
459
+ output_amount=Decimal("0"),
460
+ fee=Decimal("0"),
461
+ gas_estimate=50000,
462
+ description=f"Repay flash loan",
463
+ )
464
+ )
465
+
466
+ # Calculate profit
467
+ gross_profit = swap_output - debt_amount
468
+ total_gas = sum(s.gas_estimate for s in steps)
469
+
470
+ gas_price_gwei = 30
471
+ eth_price = Decimal("2500")
472
+ gas_cost_eth = Decimal(total_gas * gas_price_gwei) / Decimal("1e9")
473
+ gas_cost_usd = gas_cost_eth * eth_price
474
+
475
+ # For USDC-denominated profit
476
+ gas_cost_in_debt = gas_cost_usd
477
+ net_profit = gross_profit - loan_fee - gas_cost_in_debt
478
+ net_profit_usd = net_profit # Already in USD
479
+
480
+ roi = float(net_profit / debt_amount * 100) if debt_amount > 0 else 0
481
+
482
+ if net_profit < 0:
483
+ warnings.append("Liquidation not profitable after costs")
484
+
485
+ return StrategyResult(
486
+ strategy_type=self.strategy_type,
487
+ success=net_profit > 0,
488
+ steps=steps,
489
+ loan_amount=debt_amount,
490
+ loan_asset=params.debt_asset,
491
+ loan_provider=provider.name,
492
+ loan_fee=loan_fee,
493
+ gross_profit=gross_profit,
494
+ total_fees=loan_fee,
495
+ gas_cost_eth=gas_cost_eth,
496
+ gas_cost_usd=gas_cost_usd,
497
+ net_profit=net_profit,
498
+ net_profit_usd=net_profit_usd,
499
+ roi_percent=roi,
500
+ execution_path=f"{params.protocol} liquidation",
501
+ warnings=warnings,
502
+ metadata={
503
+ "debt_amount": float(debt_amount),
504
+ "collateral_received": float(collateral_received),
505
+ "liquidation_bonus": float(liquidation_bonus * 100),
506
+ },
507
+ )
508
+
509
+ def _error_result(self, message: str) -> StrategyResult:
510
+ """Create error result."""
511
+ return StrategyResult(
512
+ strategy_type=self.strategy_type,
513
+ success=False,
514
+ steps=[],
515
+ loan_amount=Decimal("0"),
516
+ loan_asset="",
517
+ loan_provider="",
518
+ loan_fee=Decimal("0"),
519
+ gross_profit=Decimal("0"),
520
+ total_fees=Decimal("0"),
521
+ gas_cost_eth=Decimal("0"),
522
+ gas_cost_usd=Decimal("0"),
523
+ net_profit=Decimal("0"),
524
+ net_profit_usd=Decimal("0"),
525
+ roi_percent=0,
526
+ execution_path="",
527
+ warnings=[message],
528
+ )
529
+
530
+
531
+ class StrategyFactory:
532
+ """Factory for creating strategy instances."""
533
+
534
+ def __init__(self):
535
+ """Initialize with provider manager."""
536
+ self.provider_manager = ProviderManager()
537
+ self._strategies = {
538
+ StrategyType.SIMPLE_ARBITRAGE: SimpleArbitrageStrategy,
539
+ StrategyType.LIQUIDATION: LiquidationStrategy,
540
+ }
541
+
542
+ def create(self, strategy_type: StrategyType) -> Optional[FlashLoanStrategy]:
543
+ """Create strategy instance."""
544
+ strategy_class = self._strategies.get(strategy_type)
545
+ if strategy_class:
546
+ return strategy_class(self.provider_manager)
547
+ return None
548
+
549
+ def list_strategies(self) -> List[StrategyType]:
550
+ """List available strategies."""
551
+ return list(self._strategies.keys())
552
+
553
+
554
+ def demo():
555
+ """Demonstrate strategy simulation."""
556
+ factory = StrategyFactory()
557
+
558
+ print("=" * 60)
559
+ print("FLASH LOAN STRATEGY SIMULATION")
560
+ print("=" * 60)
561
+
562
+ # Simulate simple arbitrage
563
+ arbitrage = factory.create(StrategyType.SIMPLE_ARBITRAGE)
564
+ if arbitrage:
565
+ params = ArbitrageParams(
566
+ input_token="ETH",
567
+ output_token="USDC",
568
+ amount=Decimal("100"),
569
+ dex_buy="sushiswap",
570
+ dex_sell="uniswap",
571
+ provider="aave",
572
+ )
573
+
574
+ result = arbitrage.simulate(params)
575
+
576
+ print(f"\nStrategy: {result.strategy_type.value}")
577
+ print(f"Loan: {result.loan_amount} {result.loan_asset} from {result.loan_provider}")
578
+ print(f"Path: {result.execution_path}")
579
+ print("-" * 40)
580
+ print(f"Gross Profit: {result.gross_profit:.6f} {result.loan_asset}")
581
+ print(f"Loan Fee: -{result.loan_fee:.6f} {result.loan_asset}")
582
+ print(f"Gas Cost: -{result.gas_cost_eth:.6f} ETH (${result.gas_cost_usd:.2f})")
583
+ print("-" * 40)
584
+ print(f"Net Profit: {result.net_profit:.6f} {result.loan_asset}")
585
+ print(f" (${result.net_profit_usd:.2f})")
586
+ print(f"ROI: {result.roi_percent:.4f}%")
587
+ print(f"Profitable: {'YES' if result.success else 'NO'}")
588
+
589
+ if result.warnings:
590
+ print("\nWarnings:")
591
+ for w in result.warnings:
592
+ print(f" ⚠️ {w}")
593
+
594
+
595
+ if __name__ == "__main__":
596
+ demo()