@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,512 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Flash loan simulation output formatters.
4
+
5
+ Handles all output formatting:
6
+ - Console tables and reports
7
+ - JSON export
8
+ - Summary cards
9
+ """
10
+
11
+ import json
12
+ from dataclasses import asdict
13
+ from decimal import Decimal
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from strategy_engine import StrategyResult, TransactionStep, StrategyType
17
+ from profit_calculator import ProfitBreakdown, GasEstimate
18
+ from risk_assessor import RiskAssessment, RiskFactor, RiskLevel
19
+ from protocol_adapters import ProviderInfo
20
+
21
+
22
+ class DecimalEncoder(json.JSONEncoder):
23
+ """JSON encoder that handles Decimal types."""
24
+
25
+ def default(self, obj):
26
+ if isinstance(obj, Decimal):
27
+ return float(obj)
28
+ if hasattr(obj, "value"): # Enum
29
+ return obj.value
30
+ return super().default(obj)
31
+
32
+
33
+ class ConsoleFormatter:
34
+ """Format output for console display."""
35
+
36
+ # Box drawing characters
37
+ BOX_H = "─"
38
+ BOX_V = "│"
39
+ BOX_TL = "┌"
40
+ BOX_TR = "┐"
41
+ BOX_BL = "└"
42
+ BOX_BR = "┘"
43
+ BOX_LT = "├"
44
+ BOX_RT = "┤"
45
+ BOX_TB = "┬"
46
+ BOX_BT = "┴"
47
+ BOX_X = "┼"
48
+
49
+ # Risk level indicators
50
+ RISK_INDICATORS = {
51
+ RiskLevel.LOW: ("🟢", "LOW"),
52
+ RiskLevel.MEDIUM: ("🟡", "MEDIUM"),
53
+ RiskLevel.HIGH: ("🟠", "HIGH"),
54
+ RiskLevel.CRITICAL: ("🔴", "CRITICAL"),
55
+ }
56
+
57
+ def __init__(self, width: int = 70):
58
+ """Initialize formatter with display width."""
59
+ self.width = width
60
+
61
+ def _header(self, title: str) -> str:
62
+ """Create a section header."""
63
+ padding = (self.width - len(title) - 2) // 2
64
+ return f"\n{'=' * padding} {title} {'=' * padding}\n"
65
+
66
+ def _subheader(self, title: str) -> str:
67
+ """Create a subsection header."""
68
+ return f"\n{'-' * self.width}\n{title}\n{'-' * self.width}\n"
69
+
70
+ def _box(self, lines: List[str], title: str = "") -> str:
71
+ """Create a box around content."""
72
+ inner_width = self.width - 4
73
+
74
+ result = []
75
+
76
+ # Top border with title
77
+ if title:
78
+ title_part = f" {title} "
79
+ border_left = (inner_width - len(title_part)) // 2
80
+ border_right = inner_width - len(title_part) - border_left
81
+ result.append(
82
+ f"{self.BOX_TL}{self.BOX_H * border_left}{title_part}"
83
+ f"{self.BOX_H * border_right}{self.BOX_TR}"
84
+ )
85
+ else:
86
+ result.append(f"{self.BOX_TL}{self.BOX_H * (inner_width + 2)}{self.BOX_TR}")
87
+
88
+ # Content
89
+ for line in lines:
90
+ # Truncate if too long
91
+ if len(line) > inner_width:
92
+ line = line[: inner_width - 3] + "..."
93
+ result.append(f"{self.BOX_V} {line:<{inner_width}} {self.BOX_V}")
94
+
95
+ # Bottom border
96
+ result.append(f"{self.BOX_BL}{self.BOX_H * (inner_width + 2)}{self.BOX_BR}")
97
+
98
+ return "\n".join(result)
99
+
100
+ def format_strategy_result(self, result: StrategyResult) -> str:
101
+ """Format strategy simulation result."""
102
+ lines = []
103
+
104
+ # Header
105
+ lines.append(self._header("FLASH LOAN SIMULATION"))
106
+
107
+ # Strategy summary box
108
+ summary = [
109
+ f"Strategy: {result.strategy_type.value}",
110
+ f"Loan: {result.loan_amount} {result.loan_asset}",
111
+ f"Provider: {result.provider}",
112
+ f"Profitable: {'YES ✓' if result.is_profitable else 'NO ✗'}",
113
+ ]
114
+ lines.append(self._box(summary, "SUMMARY"))
115
+
116
+ # Transaction steps
117
+ lines.append(self._subheader("TRANSACTION STEPS"))
118
+ for i, step in enumerate(result.steps, 1):
119
+ lines.append(f" {i}. [{step.protocol}] {step.action}")
120
+ lines.append(f" {step.asset_in} → {step.asset_out}")
121
+ lines.append(f" {step.amount_in:.6f} → {step.amount_out:.6f}")
122
+ lines.append(f" Gas: ~{step.gas_estimate:,} units")
123
+ lines.append("")
124
+
125
+ # Profit summary
126
+ lines.append(self._subheader("PROFIT BREAKDOWN"))
127
+ lines.append(f" Gross Profit: {result.gross_profit:+.6f} {result.loan_asset}")
128
+ lines.append(f" Flash Loan Fee: -{result.loan_fee:.6f} {result.loan_asset}")
129
+ lines.append(
130
+ f" Gas Cost: -{result.gas_cost_eth:.6f} ETH (${result.gas_cost_usd:.2f})"
131
+ )
132
+ lines.append(f" {'-' * 40}")
133
+ lines.append(
134
+ f" Net Profit: {result.net_profit:+.6f} {result.loan_asset} "
135
+ f"(${result.net_profit_usd:+.2f})"
136
+ )
137
+ lines.append(f" ROI: {result.roi_percent:.4f}%")
138
+
139
+ return "\n".join(lines)
140
+
141
+ def format_profit_breakdown(self, breakdown: ProfitBreakdown) -> str:
142
+ """Format detailed profit breakdown."""
143
+ lines = []
144
+
145
+ lines.append(self._header("PROFIT BREAKDOWN"))
146
+
147
+ # Revenue
148
+ lines.append(f"\n Gross Revenue: {breakdown.gross_revenue:.6f} ETH")
149
+
150
+ # Costs
151
+ lines.append("\n Costs:")
152
+ lines.append(f" Flash Loan Fee: -{breakdown.flash_loan_fee:.6f} ETH")
153
+ lines.append(
154
+ f" Gas Cost: -{breakdown.gas_cost_eth:.6f} ETH "
155
+ f"(${breakdown.gas_cost_usd:.2f})"
156
+ )
157
+ lines.append(f" Est. Slippage: -{breakdown.slippage_cost:.6f} ETH")
158
+ lines.append(f" DEX Fees: ~{breakdown.dex_fees:.6f} ETH (in price)")
159
+ lines.append(f" {'-' * 45}")
160
+ lines.append(f" Total Costs: -{breakdown.total_costs:.6f} ETH")
161
+
162
+ # Net
163
+ lines.append(
164
+ f"\n Net Profit: {breakdown.net_profit:+.6f} ETH "
165
+ f"(${breakdown.net_profit_usd:+.2f})"
166
+ )
167
+ lines.append(f" ROI: {breakdown.roi_percent:.4f}%")
168
+ lines.append(f" Breakeven Gas: {breakdown.breakeven_gas_price:.1f} gwei")
169
+
170
+ # Verdict
171
+ if breakdown.is_profitable:
172
+ lines.append("\n ✓ PROFITABLE")
173
+ else:
174
+ lines.append("\n ✗ NOT PROFITABLE")
175
+
176
+ return "\n".join(lines)
177
+
178
+ def format_risk_assessment(self, assessment: RiskAssessment) -> str:
179
+ """Format risk assessment report."""
180
+ lines = []
181
+
182
+ lines.append(self._header("RISK ASSESSMENT"))
183
+
184
+ # Overall rating
185
+ indicator, label = self.RISK_INDICATORS.get(
186
+ assessment.overall_level, ("⚪", "UNKNOWN")
187
+ )
188
+ lines.append(f"\n Overall Risk: {indicator} {label}")
189
+ lines.append(f" Risk Score: {assessment.overall_score:.0f}/100")
190
+ lines.append(f" Viability: {assessment.viability}")
191
+
192
+ # Individual factors
193
+ lines.append(self._subheader("RISK FACTORS"))
194
+
195
+ for factor in assessment.factors:
196
+ ind, lbl = self.RISK_INDICATORS.get(factor.level, ("⚪", "?"))
197
+ lines.append(f"\n {ind} {factor.name}: {lbl} ({factor.score:.0f})")
198
+ lines.append(f" {factor.description}")
199
+ lines.append(f" → {factor.mitigation}")
200
+
201
+ # Warnings
202
+ if assessment.warnings:
203
+ lines.append(self._subheader("WARNINGS"))
204
+ for warning in assessment.warnings:
205
+ lines.append(f" ⚠️ {warning}")
206
+
207
+ # Recommendations
208
+ lines.append(self._subheader("RECOMMENDATIONS"))
209
+ for rec in assessment.recommendations:
210
+ lines.append(f" • {rec}")
211
+
212
+ return "\n".join(lines)
213
+
214
+ def format_provider_comparison(
215
+ self, providers: List[ProviderInfo], asset: str, amount: Decimal
216
+ ) -> str:
217
+ """Format provider comparison table."""
218
+ lines = []
219
+
220
+ lines.append(self._header("PROVIDER COMPARISON"))
221
+ lines.append(f"\n Comparing {amount} {asset} flash loan:\n")
222
+
223
+ # Table header
224
+ lines.append(
225
+ f" {'Provider':<14} {'Fee %':<8} {'Fee Amount':<14} "
226
+ f"{'Gas OH':<10} {'Chains':<20}"
227
+ )
228
+ lines.append(f" {'-' * 66}")
229
+
230
+ # Table rows
231
+ for info in providers:
232
+ fee_pct = f"{float(info.fee_rate) * 100:.2f}%"
233
+ fee_amt = f"{info.fee_amount:.4f} {asset}"
234
+ gas_oh = f"{info.gas_overhead:,}"
235
+ chains = ", ".join(info.supported_chains[:2])
236
+ if len(info.supported_chains) > 2:
237
+ chains += "..."
238
+
239
+ lines.append(
240
+ f" {info.name:<14} {fee_pct:<8} {fee_amt:<14} "
241
+ f"{gas_oh:<10} {chains:<20}"
242
+ )
243
+
244
+ # Recommendation
245
+ if providers:
246
+ best = providers[0]
247
+ lines.append(f"\n Recommended: {best.name}")
248
+ if best.fee_amount == 0:
249
+ lines.append(" (FREE flash loan!)")
250
+ else:
251
+ lines.append(f" (Lowest fee: {best.fee_amount:.4f} {asset})")
252
+
253
+ return "\n".join(lines)
254
+
255
+ def format_quick_summary(
256
+ self, result: StrategyResult, assessment: Optional[RiskAssessment] = None
257
+ ) -> str:
258
+ """Format a quick one-box summary."""
259
+ lines = []
260
+
261
+ # Profit line
262
+ profit_emoji = "✓" if result.is_profitable else "✗"
263
+ lines.append(
264
+ f"Net Profit: {result.net_profit:+.6f} {result.loan_asset} "
265
+ f"(${result.net_profit_usd:+.2f}) {profit_emoji}"
266
+ )
267
+
268
+ # Provider
269
+ lines.append(f"Provider: {result.provider} (fee: {result.loan_fee:.6f})")
270
+
271
+ # Risk if available
272
+ if assessment:
273
+ ind, lbl = self.RISK_INDICATORS.get(
274
+ assessment.overall_level, ("⚪", "?")
275
+ )
276
+ lines.append(f"Risk: {ind} {lbl} | Viability: {assessment.viability}")
277
+
278
+ # Verdict
279
+ if result.is_profitable and (
280
+ assessment is None or assessment.viability != "NO-GO"
281
+ ):
282
+ lines.append("Verdict: PROCEED WITH CAUTION")
283
+ elif result.is_profitable:
284
+ lines.append("Verdict: HIGH RISK - RECONSIDER")
285
+ else:
286
+ lines.append("Verdict: DO NOT EXECUTE")
287
+
288
+ return self._box(lines, "QUICK SUMMARY")
289
+
290
+
291
+ class JSONFormatter:
292
+ """Format output as JSON."""
293
+
294
+ def format_full_report(
295
+ self,
296
+ result: StrategyResult,
297
+ breakdown: Optional[ProfitBreakdown] = None,
298
+ assessment: Optional[RiskAssessment] = None,
299
+ providers: Optional[List[ProviderInfo]] = None,
300
+ ) -> str:
301
+ """Format complete simulation report as JSON."""
302
+ report = {
303
+ "simulation": {
304
+ "strategy_type": result.strategy_type.value,
305
+ "loan_asset": result.loan_asset,
306
+ "loan_amount": float(result.loan_amount),
307
+ "provider": result.provider,
308
+ "is_profitable": result.is_profitable,
309
+ },
310
+ "profit": {
311
+ "gross_profit": float(result.gross_profit),
312
+ "loan_fee": float(result.loan_fee),
313
+ "gas_cost_eth": float(result.gas_cost_eth),
314
+ "gas_cost_usd": float(result.gas_cost_usd),
315
+ "net_profit": float(result.net_profit),
316
+ "net_profit_usd": float(result.net_profit_usd),
317
+ "roi_percent": result.roi_percent,
318
+ },
319
+ "steps": [
320
+ {
321
+ "protocol": s.protocol,
322
+ "action": s.action,
323
+ "asset_in": s.asset_in,
324
+ "asset_out": s.asset_out,
325
+ "amount_in": float(s.amount_in),
326
+ "amount_out": float(s.amount_out),
327
+ "gas_estimate": s.gas_estimate,
328
+ }
329
+ for s in result.steps
330
+ ],
331
+ }
332
+
333
+ if breakdown:
334
+ report["breakdown"] = {
335
+ "gross_revenue": float(breakdown.gross_revenue),
336
+ "flash_loan_fee": float(breakdown.flash_loan_fee),
337
+ "gas_cost_eth": float(breakdown.gas_cost_eth),
338
+ "gas_cost_usd": float(breakdown.gas_cost_usd),
339
+ "dex_fees": float(breakdown.dex_fees),
340
+ "slippage_cost": float(breakdown.slippage_cost),
341
+ "total_costs": float(breakdown.total_costs),
342
+ "breakeven_gas_price": breakdown.breakeven_gas_price,
343
+ }
344
+
345
+ if assessment:
346
+ report["risk"] = {
347
+ "overall_level": assessment.overall_level.value,
348
+ "overall_score": assessment.overall_score,
349
+ "viability": assessment.viability,
350
+ "factors": [
351
+ {
352
+ "name": f.name,
353
+ "level": f.level.value,
354
+ "score": f.score,
355
+ "description": f.description,
356
+ "mitigation": f.mitigation,
357
+ }
358
+ for f in assessment.factors
359
+ ],
360
+ "warnings": assessment.warnings,
361
+ "recommendations": assessment.recommendations,
362
+ }
363
+
364
+ if providers:
365
+ report["providers"] = [
366
+ {
367
+ "name": p.name,
368
+ "fee_rate": float(p.fee_rate),
369
+ "fee_amount": float(p.fee_amount),
370
+ "max_available": float(p.max_available),
371
+ "gas_overhead": p.gas_overhead,
372
+ "supported_chains": p.supported_chains,
373
+ }
374
+ for p in providers
375
+ ]
376
+
377
+ return json.dumps(report, indent=2, cls=DecimalEncoder)
378
+
379
+ def format_strategy_result(self, result: StrategyResult) -> str:
380
+ """Format just the strategy result as JSON."""
381
+ return self.format_full_report(result)
382
+
383
+
384
+ class MarkdownFormatter:
385
+ """Format output as Markdown (for reports/docs)."""
386
+
387
+ def format_simulation_report(
388
+ self,
389
+ result: StrategyResult,
390
+ breakdown: Optional[ProfitBreakdown] = None,
391
+ assessment: Optional[RiskAssessment] = None,
392
+ ) -> str:
393
+ """Format as Markdown report."""
394
+ lines = []
395
+
396
+ lines.append("# Flash Loan Simulation Report\n")
397
+
398
+ # Summary
399
+ lines.append("## Summary\n")
400
+ lines.append(f"- **Strategy**: {result.strategy_type.value}")
401
+ lines.append(f"- **Loan**: {result.loan_amount} {result.loan_asset}")
402
+ lines.append(f"- **Provider**: {result.provider}")
403
+ lines.append(
404
+ f"- **Profitable**: {'Yes ✓' if result.is_profitable else 'No ✗'}"
405
+ )
406
+ lines.append("")
407
+
408
+ # Profit
409
+ lines.append("## Profit Analysis\n")
410
+ lines.append("| Metric | Value |")
411
+ lines.append("|--------|-------|")
412
+ lines.append(f"| Gross Profit | {result.gross_profit:+.6f} {result.loan_asset} |")
413
+ lines.append(f"| Flash Loan Fee | -{result.loan_fee:.6f} {result.loan_asset} |")
414
+ lines.append(f"| Gas Cost | -{result.gas_cost_eth:.6f} ETH (${result.gas_cost_usd:.2f}) |")
415
+ lines.append(f"| **Net Profit** | **{result.net_profit:+.6f} {result.loan_asset}** |")
416
+ lines.append(f"| ROI | {result.roi_percent:.4f}% |")
417
+ lines.append("")
418
+
419
+ # Steps
420
+ lines.append("## Transaction Steps\n")
421
+ for i, step in enumerate(result.steps, 1):
422
+ lines.append(f"### Step {i}: {step.action} on {step.protocol}\n")
423
+ lines.append(f"- Input: {step.amount_in:.6f} {step.asset_in}")
424
+ lines.append(f"- Output: {step.amount_out:.6f} {step.asset_out}")
425
+ lines.append(f"- Gas: ~{step.gas_estimate:,} units")
426
+ lines.append("")
427
+
428
+ # Risk if available
429
+ if assessment:
430
+ lines.append("## Risk Assessment\n")
431
+ lines.append(f"- **Overall Level**: {assessment.overall_level.value}")
432
+ lines.append(f"- **Risk Score**: {assessment.overall_score:.0f}/100")
433
+ lines.append(f"- **Viability**: {assessment.viability}")
434
+ lines.append("")
435
+
436
+ lines.append("### Risk Factors\n")
437
+ lines.append("| Factor | Level | Score | Description |")
438
+ lines.append("|--------|-------|-------|-------------|")
439
+ for f in assessment.factors:
440
+ lines.append(f"| {f.name} | {f.level.value} | {f.score:.0f} | {f.description} |")
441
+ lines.append("")
442
+
443
+ if assessment.warnings:
444
+ lines.append("### Warnings\n")
445
+ for w in assessment.warnings:
446
+ lines.append(f"- ⚠️ {w}")
447
+ lines.append("")
448
+
449
+ lines.append("### Recommendations\n")
450
+ for r in assessment.recommendations:
451
+ lines.append(f"- {r}")
452
+ lines.append("")
453
+
454
+ # Disclaimer
455
+ lines.append("---\n")
456
+ lines.append("*This simulation is for educational purposes only. ")
457
+ lines.append("Do not execute without proper testing and risk assessment.*")
458
+
459
+ return "\n".join(lines)
460
+
461
+
462
+ def demo():
463
+ """Demonstrate formatters."""
464
+ from strategy_engine import StrategyFactory, StrategyType, ArbitrageParams
465
+ from profit_calculator import ProfitCalculator
466
+ from risk_assessor import RiskAssessor
467
+ from protocol_adapters import ProviderManager
468
+
469
+ # Run simulation
470
+ factory = StrategyFactory()
471
+ strategy = factory.create(StrategyType.SIMPLE_ARBITRAGE)
472
+
473
+ params = ArbitrageParams(
474
+ input_token="ETH",
475
+ output_token="USDC",
476
+ amount=Decimal("100"),
477
+ dex_buy="sushiswap",
478
+ dex_sell="uniswap",
479
+ provider="aave",
480
+ )
481
+
482
+ result = strategy.simulate(params)
483
+
484
+ # Calculate breakdown
485
+ calculator = ProfitCalculator(eth_price_usd=2500.0, gas_price_gwei=30.0)
486
+ breakdown = calculator.calculate_breakdown(result)
487
+
488
+ # Assess risk
489
+ assessor = RiskAssessor(eth_price_usd=2500.0)
490
+ assessment = assessor.assess(result)
491
+
492
+ # Get providers
493
+ manager = ProviderManager()
494
+ providers = manager.compare_providers("ETH", Decimal("100"))
495
+
496
+ # Console format
497
+ console = ConsoleFormatter()
498
+ print(console.format_strategy_result(result))
499
+ print(console.format_risk_assessment(assessment))
500
+ print(console.format_provider_comparison(providers, "ETH", Decimal("100")))
501
+ print(console.format_quick_summary(result, assessment))
502
+
503
+ # JSON format
504
+ print("\n" + "=" * 70)
505
+ print("JSON OUTPUT")
506
+ print("=" * 70)
507
+ json_fmt = JSONFormatter()
508
+ print(json_fmt.format_full_report(result, breakdown, assessment, providers))
509
+
510
+
511
+ if __name__ == "__main__":
512
+ demo()