From 08b90634870517bb9c42e134c660bb68c9e0fe8e Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Tue, 21 Jul 2026 00:14:46 +0800 Subject: [PATCH] =?UTF-8?q?QWEN3.8=E5=88=9D=E6=AC=A1=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 32 ++ Cargo.toml | 73 +++ README.md | 349 ++++++++++++++ benches/bonding_curve_bench.rs | 60 +++ config/mainnet.toml | 98 ++++ src/bonding_curve/account.rs | 241 ++++++++++ src/bonding_curve/math.rs | 638 +++++++++++++++++++++++++ src/bonding_curve/mod.rs | 17 + src/bonding_curve/simulator.rs | 276 +++++++++++ src/config.rs | 184 +++++++ src/decision/mod.rs | 107 +++++ src/decision/position_sizer.rs | 115 +++++ src/decision/rug_filter.rs | 182 +++++++ src/decision/scorer.rs | 115 +++++ src/detection/bonding_curve_monitor.rs | 103 ++++ src/detection/mod.rs | 12 + src/detection/streamer.rs | 283 +++++++++++ src/error.rs | 104 ++++ src/execution/jito.rs | 165 +++++++ src/execution/mod.rs | 125 +++++ src/execution/submitter.rs | 107 +++++ src/execution/tx_builder.rs | 380 +++++++++++++++ src/main.rs | 264 ++++++++++ src/monitoring/metrics.rs | 120 +++++ src/monitoring/mod.rs | 39 ++ src/position/exit_strategy.rs | 208 ++++++++ src/position/manager.rs | 187 ++++++++ src/position/mod.rs | 7 + src/risk/circuit_breaker.rs | 197 ++++++++ src/risk/mod.rs | 5 + 30 files changed, 4793 insertions(+) create mode 100644 .env.example create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 benches/bonding_curve_bench.rs create mode 100644 config/mainnet.toml create mode 100644 src/bonding_curve/account.rs create mode 100644 src/bonding_curve/math.rs create mode 100644 src/bonding_curve/mod.rs create mode 100644 src/bonding_curve/simulator.rs create mode 100644 src/config.rs create mode 100644 src/decision/mod.rs create mode 100644 src/decision/position_sizer.rs create mode 100644 src/decision/rug_filter.rs create mode 100644 src/decision/scorer.rs create mode 100644 src/detection/bonding_curve_monitor.rs create mode 100644 src/detection/mod.rs create mode 100644 src/detection/streamer.rs create mode 100644 src/error.rs create mode 100644 src/execution/jito.rs create mode 100644 src/execution/mod.rs create mode 100644 src/execution/submitter.rs create mode 100644 src/execution/tx_builder.rs create mode 100644 src/main.rs create mode 100644 src/monitoring/metrics.rs create mode 100644 src/monitoring/mod.rs create mode 100644 src/position/exit_strategy.rs create mode 100644 src/position/manager.rs create mode 100644 src/position/mod.rs create mode 100644 src/risk/circuit_breaker.rs create mode 100644 src/risk/mod.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..329771d --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# ============================================================ +# Pump.fun Sniper Bot 环境变量配置 +# 复制此文件为 .env 并填入真实值 +# ============================================================ + +# === Solana RPC === +# 推荐使用专用 RPC 节点 (Helius / QuickNode / 自建) +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_WS_URL=wss://api.mainnet-beta.solana.com + +# === Yellowstone gRPC (事件流) === +# 推荐: Triton / RPC Fast / 自建 Yellowstone 节点 +YELLOWSTONE_GRPC_URL=https://grpc.example.com +YELLOWSTONE_X_TOKEN=your_x_token_here + +# === Jito Block Engine (MEV 保护) === +JITO_BLOCK_ENGINE_URL=https://mainnet.block-engine.jito.wtf +JITO_TIP_LAMPORTS=10000 + +# === 钱包 === +# 逗号分隔的钱包文件路径 +WALLET_PATHS=wallets/wallet_0.json,wallets/wallet_1.json,wallets/wallet_2.json + +# === 风控 === +MAX_POSITION_SOL=0.1 +DAILY_LOSS_LIMIT_SOL=1.0 +MAX_CONCURRENT_POSITIONS=5 +MAX_CONSECUTIVE_FAILURES=5 + +# === 监控 === +PROMETHEUS_PORT=9090 +LOG_LEVEL=info diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..90707ce --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,73 @@ +[package] +name = "pumpfun-sniper-bot" +version = "0.1.0" +edition = "2021" +authors = ["Gavin"] +description = "Pump.fun Bonding Curve Sniper Bot - 新币狙击交易机器人" +license = "MIT" + +[[bin]] +name = "sniper" +path = "src/main.rs" + +[dependencies] +# === Solana 核心 === +solana-sdk = "3.0" +solana-client = "3.1" +solana-transaction-status = "3.1" + +# === Pump.fun SDK (后期安装) === +# sol-trade-sdk = "2.0" +# solana-streamer-sdk = { version = "2.0", features = ["sdk-parse-zero-copy"] } + +# === 异步运行时 === +tokio = { version = "1.50", features = ["full"] } + +# === 序列化 === +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.8" +borsh = "1.6" +bincode = "2.0" + +# === HTTP 客户端 (Jito API) === +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } + +# === 并发 === +crossbeam-queue = "0.3" +dashmap = "6.1" +parking_lot = "0.12" + +# === 监控 === +prometheus = "0.13" + +# === 日志 === +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +# === 工具 === +anyhow = "1.0" +thiserror = "2.0" +chrono = { version = "0.4", features = ["serde"] } +rand = "0.8" +base64 = "0.22" +hex = "0.4" +sha2 = "0.10" +bs58 = "0.5" + +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "bonding_curve_bench" +harness = false + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +[profile.dev] +opt-level = 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..05fc14e --- /dev/null +++ b/README.md @@ -0,0 +1,349 @@ +# 🎯 Pump.fun Sniper Bot + +Pump.fun 新币狙击交易机器人 — 基于 Bonding Curve 数学模型的自动化交易系统。 + +## ⚠️ 风险警告 + +> **本项目仅供学习和研究使用。链上交易存在极高风险,99%+ 的 Pump.fun 代币归零。 +> 永远不要投入你无法承受全部损失的资金。** + +--- + +## 一、系统架构 +┌─────────────────────────────────────────────────────────────┐ +│ Layer 1: 事件检测 (detection/) │ +│ Yellowstone gRPC / RPC 轮询 → 新币创建 / 进度更新 / 毕业 │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 2: 决策引擎 (decision/) │ +│ Rug 过滤 → 信号评分 → 仓位计算 → GO/NO-GO │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 3: 交易执行 (execution/) │ +│ 交易构建 → 模拟预检 → 多中继提交 (RPC + Jito) │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 4: 仓位管理 (position/) │ +│ 止盈(分批) → 止损 → 时间止损 → 毕业处理 │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 5: 风控 (risk/) │ +│ 熔断器 → 日亏损限制 → 连续失败保护 │ +├─────────────────────────────────────────────────────────────┤ +│ Layer 6: 监控 (monitoring/) │ +│ Prometheus 指标 → 日志 → 告警 │ +└─────────────────────────────────────────────────────────────┘ + +--- + +## 二、环境要求 + +### 2.1 系统要求 + +| 组件 | 最低要求 | 推荐 | +|------|---------|------| +| OS | Linux (Ubuntu 22.04+) / Windows 11 | Linux 裸金属 | +| CPU | 4 核 | 8 核+ (低延迟) | +| RAM | 8 GB | 16 GB+ | +| 磁盘 | 20 GB SSD | NVMe SSD | +| 网络 | 100 Mbps | 1 Gbps+ (共置) | + +### 2.2 软件要求 + +| 软件 | 版本 | 安装方式 | +|------|------|---------| +| **Rust** | 1.75+ | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh` | +| **Solana CLI** | 1.18+ | `sh -c "$(curl -sSfL https://release.solana.com/stable/install)"` | +| **Git** | 2.0+ | 系统包管理器 | + +### 2.3 Rust 工具链安装 + +```bash +# 安装 Rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env + +# 验证 +rustc --version # >= 1.75 +cargo --version + +# 安装 Solana CLI +sh -c "$(curl -sSfL https://release.solana.com/stable/install)" +solana --version +``` + +## 三、安装步骤 + +### 3.1 克隆项目 + +```bash +cd pumpfun-sniper-bot +``` + +### 3.2 配置环境变量 + +```bash +cp .env.example .env +# 编辑 .env 填入真实配置 +``` + +### 3.3 配置 TOML + +```bash +# 编辑 config/mainnet.toml +# 关键配置项: +# - detection.yellowstone_grpc_url: 你的 gRPC 端点 +# - execution.rpc_url: 你的 RPC 端点 +# - execution.jito_block_engine_url: Jito 端点 +# - position.max_position_sol: 单次最大仓位 +# - risk.daily_loss_limit_sol: 日亏损限制 +``` + +### 3.4 创建钱包 + +```bash +# 创建钱包目录 +mkdir -p wallets + +# 生成钱包 (建议 3-5 个轮换) +solana-keygen new -o wallets/wallet_0.json +solana-keygen new -o wallets/wallet_1.json +solana-keygen new -o wallets/wallet_2.json + +# 查看地址 +solana-keygen pubkey wallets/wallet_0.json + +# 转入 SOL (仅使用你能承受损失的资金!) +solana transfer 1.0 --url mainnet-beta +``` + +### 3.5 编译 + +```bash +# 开发编译 +cargo build + +# 生产编译 (优化) +cargo build --release + +# 运行测试 +cargo test + +# 运行基准测试 +cargo bench +``` + +### 3.6 运行 + +```bash +# 开发模式 (dry_run = true, 不实际交易) +cargo run -- config/mainnet.toml + +# 生产模式 (修改 config 中 dry_run = false) +./target/release/sniper config/mainnet.toml +``` + +## 四、SDK 安装 (后期) + +当前项目使用内置实现,不依赖外部 SDK。当你准备好后,可以安装以下 SDK 获得更好的性能: + +### 4.1 sol-trade-sdk (交易构建) + +```bash +# Cargo.toml 中取消注释: +sol-trade-sdk = "2.0" +``` + +安装后替换 `src/execution/tx_builder.rs` 中的手动指令构建为: + +```rust +use sol_trade_sdk::protocols::pumpfun::{self, PumpFunBuyParams}; + +let params = PumpFunBuyParams { + bonding_curve: bonding_curve_pubkey, + user: wallet.pubkey(), + mint: mint_pubkey, + sol_amount: lamports, + slippage_bps, +}; +let buy_ix = pumpfun::build_buy(¶ms)?; +``` + +### 4.2 solana-streamer-sdk (事件流) + +```bash +# Cargo.toml 中取消注释: +solana-streamer-sdk = { version = "2.0", features = ["sdk-parse-zero-copy"] } +``` + +安装后替换 `src/detection/streamer.rs` 中的 RPC 轮询为 gRPC 流: + +```rust +use solana_streamer_sdk::prelude::*; + +let streamer = SolanaStreamer::builder() + .grpc_endpoint(&config.yellowstone_grpc_url) + .x_token(config.yellowstone_x_token.as_deref()) + .subscribe_program(PUMP_FUN_PROGRAM_ID) + .order_mode(OrderMode::Unordered) + .build()?; +``` + +## 五、配置说明 + +### 5.1 关键参数 + +| 参数 | 默认值 | 说明 | 建议 | +|------|-------|------|------| +| bot.mode | "sniper" | 运行模式 | 新手用 "sniper" | +| bot.dry_run | true | 模拟模式 | 首次必须 true | +| position.max_position_sol | 0.1 | 单次最大仓位 | 总资金的 1-2% | +| position.stop_loss_pct | 0.3 | 止损比例 | 30% | +| risk.daily_loss_limit_sol | 1.0 | 日亏损限制 | 总资金的 5% | +| risk.max_consecutive_failures | 5 | 熔断阈值 | 5 次 | +| decision.min_score_threshold | 60.0 | 最低评分 | 60-70 | +| decision.slippage_bps | 1000 | 滑点 (10%) | 新币需要高滑点 | + +### 5.2 推荐 RPC 服务 + +| 服务 | 用途 | 价格 | +|------|------|------| +| Helius | RPC + gRPC | $49-$499/月 | +| QuickNode | RPC | $49-$299/月 | +| Triton | Yellowstone gRPC | 联系销售 | +| Jito | Block Engine | 免费 (tip 另算) | + +## 六、项目结构 + +``` +pumpfun-sniper-bot/ +├── Cargo.toml # 项目配置 +├── .env.example # 环境变量模板 +├── README.md # 本文档 +├── config/ +│ └── mainnet.toml # 主网配置 +├── wallets/ # 钱包文件 (不要提交到 git!) +│ ├── wallet_0.json +│ ├── wallet_1.json +│ └── wallet_2.json +├── scripts/ # 辅助脚本 +├── src/ +│ ├── main.rs # 主入口 + 事件循环 +│ ├── config.rs # 配置加载 +│ ├── error.rs # 统一错误类型 +│ ├── bonding_curve/ # Bonding Curve 数学模型 +│ │ ├── mod.rs +│ │ ├── math.rs # 核心公式 (含测试) +│ │ ├── account.rs # 链上账户解析 (含测试) +│ │ └── simulator.rs # 交易模拟器 (含测试) +│ ├── detection/ # 事件检测 +│ │ ├── mod.rs +│ │ ├── streamer.rs # gRPC / RPC 事件流 +│ │ └── bonding_curve_monitor.rs # 曲线状态监控 +│ ├── decision/ # 决策引擎 +│ │ ├── mod.rs +│ │ ├── rug_filter.rs # Rug Pull 过滤 +│ │ ├── scorer.rs # 信号评分 +│ │ └── position_sizer.rs # 仓位计算 +│ ├── execution/ # 交易执行 +│ │ ├── mod.rs +│ │ ├── tx_builder.rs # 交易构建 +│ │ ├── submitter.rs # 多中继提交 +│ │ └── jito.rs # Jito Bundle +│ ├── position/ # 仓位管理 +│ │ ├── mod.rs +│ │ ├── manager.rs # 仓位管理器 +│ │ └── exit_strategy.rs # 退出策略 +│ ├── risk/ # 风控 +│ │ ├── mod.rs +│ │ └── circuit_breaker.rs # 熔断器 +│ └── monitoring/ # 监控 +│ ├── mod.rs +│ └── metrics.rs # Prometheus 指标 +└── tests/ # 集成测试 +``` + +## 七、开发路线图 + +| 阶段 | 状态 | 内容 | +|------|------|------| +| Phase 1: 数学模型 | ✅ | Bonding Curve 公式 + 账户解析 + 模拟器 | +| Phase 2: 事件检测 | ✅ | RPC 轮询 (gRPC 待 SDK 安装) | +| Phase 3: 决策引擎 | ✅ | Rug 过滤 + 评分 + 仓位计算 | +| Phase 4: 交易执行 | ✅ | 交易构建 + 多中继 + Jito | +| Phase 5: 仓位管理 | ✅ | 止盈止损 + 退出策略 | +| Phase 6: 风控 | ✅ | 熔断器 + 日亏损限制 | +| Phase 7: 监控 | ✅ | Prometheus + 日志 | +| Phase 8: SDK 集成 | ⏳ | 安装 sol-trade-sdk + solana-streamer-sdk | +| Phase 9: 优化 | ⏳ | 预构建模板 + PDA 缓存 + 延迟优化 | +| Phase 10: 实盘 | ⏳ | Devnet 测试 → Mainnet 小资金 | + +## 八、安全注意事项 + +- 永远不要将 `wallets/` 目录提交到 Git +- 永远不要在代码中硬编码私钥 +- 永远先在 devnet 测试 2 周以上 +- 永远先使用 `dry_run = true` 验证逻辑 +- 永远设置日亏损限制和熔断器 +- 定期检查钱包余额和交易记录 + +```gitignore +# .gitignore 必须包含: +wallets/ +.env +*.json # 钱包文件 +``` + +## 九、常见问题 + +**Q:** 编译报错 solana-sdk 版本冲突? +**A:** 确保 solana-sdk 和 solana-client 版本一致 (都是 3.x)。 + +**Q:** 如何获取 Yellowstone gRPC 端点? +**A:** 推荐使用 Triton 或 Helius 的 gRPC 服务。 + +**Q:** Jito tip 应该设多少? +**A:** 起步 10,000 lamports (0.00001 SOL),根据网络拥堵动态调整。 + +**Q:** 为什么 dry_run 模式下交易不发送? +**A:** 这是安全设计。确认策略逻辑正确后,将 bot.dry_run 改为 false。 + +## 十、许可证 + +MIT License - 仅供学习研究,使用风险自担。 + +--- + +## 19. `.gitignore` + +```gitignore +# 钱包 (绝对不能提交!) +wallets/ +*.json +!Cargo.lock + +# 环境变量 +.env + +# 编译产物 +target/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# 日志 +logs/ +*.log + +# OS +.DS_Store +Thumbs.db +``` + +## 总结 + +- 安装 Rust 工具链:rustup + cargo +- 编译验证:cargo build 确认无编译错误 +- 运行测试:cargo test 确认数学模型正确 +- 配置环境:复制 .env.example → .env,填入你的 RPC/gRPC 端点 +- Dry Run:cargo run -- config/mainnet.toml(默认 dry_run=true) +- 后期安装 sol-trade-sdk 和 solana-streamer-sdk 后,只需替换 tx_builder.rs 和 streamer.rs 中标记了 TODO 的部分即可。架构不需要改动。 \ No newline at end of file diff --git a/benches/bonding_curve_bench.rs b/benches/bonding_curve_bench.rs new file mode 100644 index 0000000..153be5e --- /dev/null +++ b/benches/bonding_curve_bench.rs @@ -0,0 +1,60 @@ +//! Bonding Curve 数学计算基准测试 + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +// 注意: 需要 bonding_curve 模块可访问 +// 实际使用时可能需要调整路径 + +fn bench_calculate_buy(c: &mut Criterion) { + let virtual_sol = 30_000_000_000u64; // 30 SOL + let virtual_token = 1_073_000_191_000_000u64; + let real_token = 793_100_000_000_000u64; + let sol_amount = 1_000_000_000u64; // 1 SOL + + c.bench_function("calculate_buy_1_sol", |b| { + b.iter(|| { + // 内联计算 (避免模块路径问题) + let input_after_fee = (sol_amount - 1) * 10000 / (125 + 10000) + 1; + let numerator = input_after_fee as u128 * virtual_token as u128; + let denominator = virtual_sol as u128 + input_after_fee as u128; + let tokens_out = (numerator / denominator) as u64; + black_box(tokens_out.min(real_token)) + }) + }); +} + +fn bench_calculate_sell(c: &mut Criterion) { + let virtual_sol = 31_000_000_000u64; + let virtual_token = 1_038_000_000_000_000u64; + let token_amount = 34_000_000_000_000u64; + + c.bench_function("calculate_sell", |b| { + b.iter(|| { + let numerator = token_amount as u128 * virtual_sol as u128; + let denominator = virtual_token as u128 + token_amount as u128; + let sol_out_raw = (numerator / denominator) as u64; + let sol_out = sol_out_raw * (10000 - 125) / 10000; + black_box(sol_out) + }) + }); +} + +fn bench_price_calculation(c: &mut Criterion) { + let virtual_sol = 50_000_000_000u64; + let virtual_token = 643_800_000_000_000u64; + + c.bench_function("spot_price", |b| { + b.iter(|| { + let price = virtual_sol as f64 / virtual_token as f64; + black_box(price * 1_000_000.0 / 1_000_000_000.0) + }) + }); +} + +criterion_group!( + benches, + bench_calculate_buy, + bench_calculate_sell, + bench_price_calculation, +); +criterion_main!(benches); \ No newline at end of file diff --git a/config/mainnet.toml b/config/mainnet.toml new file mode 100644 index 0000000..e3ccc60 --- /dev/null +++ b/config/mainnet.toml @@ -0,0 +1,98 @@ +# ============================================================ +# Pump.fun Sniper Bot 主网配置 +# ============================================================ + +[bot] +# 运行模式: "sniper" (新币狙击) | "graduation" (毕业狙击) | "both" +mode = "sniper" +# 是否启用模拟模式 (不实际发送交易) +dry_run = true +# 日志级别: "trace" | "debug" | "info" | "warn" | "error" +log_level = "info" + +[detection] +# Yellowstone gRPC 端点 +yellowstone_grpc_url = "https://grpc.example.com" +# Yellowstone X-Token (认证) +yellowstone_x_token = "" +# 备用 RPC WebSocket (gRPC 断开时的降级方案) +backup_ws_url = "wss://api.mainnet-beta.solana.com" +# 毕业触发阈值 (SOL) - 当 real_sol >= 此值时触发毕业狙击 +graduation_trigger_sol = 84.5 +# 事件通道容量 +event_channel_capacity = 10000 + +[decision] +# 最小综合评分阈值 (0-100) +min_score_threshold = 60.0 +# 最大价格影响 (basis points, 100 = 1%) +max_price_impact_bps = 500 +# 滑点容忍度 (basis points) +slippage_bps = 1000 +# 创建者黑名单 (逗号分隔的 Pubkey) +blacklisted_creators = [] +# 创建者最小历史成功率 +min_creator_success_rate = 0.05 +# 创建者最大历史发币数 (超过视为批量发币) +max_creator_total_tokens = 50 + +[execution] +# Solana RPC URL +rpc_url = "https://api.mainnet-beta.solana.com" +# Jito Block Engine URL +jito_block_engine_url = "https://mainnet.block-engine.jito.wtf" +# Jito Tip (lamports) +jito_tip_lamports = 10000 +# 默认 Priority Fee (micro-lamports per CU) +default_priority_fee = 50000 +# Compute Unit Limit +compute_unit_limit = 200000 +# 交易确认超时 (秒) +confirm_timeout_secs = 30 +# 最大重试次数 +max_retries = 3 +# 是否启用交易模拟预检 +enable_simulation = true +# 是否启用多中继提交 +enable_multi_relay = true + +[position] +# 单次最大仓位 (SOL) +max_position_sol = 0.1 +# 最小仓位 (SOL) +min_position_sol = 0.01 +# 最大并发持仓数 +max_concurrent_positions = 5 +# 止盈 1: 收益率 (倍数) → 卖出比例 +take_profit_1_roi = 2.0 +take_profit_1_sell_pct = 0.5 +# 止盈 2: 收益率 (倍数) → 卖出比例 +take_profit_2_roi = 5.0 +take_profit_2_sell_pct = 0.3 +# 止盈 3: 收益率 (倍数) → 卖出比例 (剩余全部) +take_profit_3_roi = 10.0 +take_profit_3_sell_pct = 1.0 +# 止损: 最大亏损比例 +stop_loss_pct = 0.3 +# 时间止损: 最大持仓时间 (秒) +time_stop_secs = 600 + +[risk] +# 日亏损限制 (SOL) +daily_loss_limit_sol = 1.0 +# 最大连续失败次数 → 触发熔断 +max_consecutive_failures = 5 +# 熔断冷却时间 (分钟) +circuit_breaker_cooldown_minutes = 30 +# 单笔最大亏损 (SOL) +max_single_loss_sol = 0.5 +# 最大日交易次数 +max_daily_trades = 100 + +[monitoring] +# Prometheus 指标端口 +prometheus_port = 9090 +# 是否启用 JSON 日志格式 +json_logs = false +# 指标刷新间隔 (秒) +metrics_flush_interval_secs = 15 diff --git a/src/bonding_curve/account.rs b/src/bonding_curve/account.rs new file mode 100644 index 0000000..de673a1 --- /dev/null +++ b/src/bonding_curve/account.rs @@ -0,0 +1,241 @@ +//! Bonding Curve 链上账户解析 +//! +//! 账户布局 (73 bytes): +//! ```text +//! [0x00..0x08] discriminator: u64 +//! [0x08..0x10] virtual_token_reserves: u64 +//! [0x10..0x18] virtual_sol_reserves: u64 +//! [0x18..0x20] real_token_reserves: u64 +//! [0x20..0x28] real_sol_reserves: u64 +//! [0x28..0x30] token_total_supply: u64 +//! [0x30..0x31] complete: bool +//! [0x31..0x51] creator: Pubkey (32 bytes) +//! ``` + +use super::math::*; +use crate::error::{SniperError, SniperResult}; + +/// Bonding Curve 链上状态 +#[derive(Debug, Clone)] +pub struct BondingCurveState { + pub discriminator: u64, + pub virtual_token_reserves: u64, + pub virtual_sol_reserves: u64, + pub real_token_reserves: u64, + pub real_sol_reserves: u64, + pub token_total_supply: u64, + pub complete: bool, + pub creator: [u8; 32], +} + +impl BondingCurveState { + /// 账户数据总长度 + pub const ACCOUNT_SIZE: usize = 81; // 8 + 8*6 + 1 + 32 + + /// 从原始字节反序列化 + pub fn from_bytes(data: &[u8]) -> SniperResult { + if data.len() < Self::ACCOUNT_SIZE { + return Err(SniperError::InvalidAccountData { + expected: Self::ACCOUNT_SIZE, + actual: data.len(), + }); + } + + // 验证 discriminator + let discriminator = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let expected_disc = u64::from_le_bytes(BONDING_CURVE_DISCRIMINATOR); + if discriminator != expected_disc { + return Err(SniperError::InvalidDiscriminator { + expected: expected_disc, + actual: discriminator, + }); + } + + let mut creator = [0u8; 32]; + creator.copy_from_slice(&data[49..81]); + + Ok(Self { + discriminator, + virtual_token_reserves: u64::from_le_bytes(data[8..16].try_into().unwrap()), + virtual_sol_reserves: u64::from_le_bytes(data[16..24].try_into().unwrap()), + real_token_reserves: u64::from_le_bytes(data[24..32].try_into().unwrap()), + real_sol_reserves: u64::from_le_bytes(data[32..40].try_into().unwrap()), + token_total_supply: u64::from_le_bytes(data[40..48].try_into().unwrap()), + complete: data[48] != 0, + creator, + }) + } + + /// 创建初始状态 (代币刚创建) + pub fn new_initial(creator: [u8; 32]) -> Self { + Self { + discriminator: u64::from_le_bytes(BONDING_CURVE_DISCRIMINATOR), + virtual_token_reserves: INITIAL_VIRTUAL_TOKENS, + virtual_sol_reserves: INITIAL_VIRTUAL_SOL, + real_token_reserves: INITIAL_REAL_TOKENS, + real_sol_reserves: 0, + token_total_supply: 1_000_000_000 * TOKEN_SCALING, + complete: false, + creator, + } + } + + // ================================================================ + // 便捷计算方法 + // ================================================================ + + /// 当前价格 (SOL/token) + #[inline(always)] + pub fn price(&self) -> f64 { + BondingCurveMath::spot_price_sol_per_token( + self.virtual_sol_reserves, + self.virtual_token_reserves, + ) + } + + /// 当前市值 (SOL) + #[inline(always)] + pub fn market_cap_sol(&self) -> f64 { + BondingCurveMath::market_cap_sol( + self.virtual_sol_reserves, + self.virtual_token_reserves, + self.token_total_supply, + ) + } + + /// Bonding Curve 进度 (0.0 ~ 1.0) + #[inline(always)] + pub fn progress(&self) -> f64 { + BondingCurveMath::progress(self.real_token_reserves) + } + + /// 距离毕业还需多少 SOL (lamports) + #[inline(always)] + pub fn sol_to_graduation(&self) -> u64 { + BondingCurveMath::sol_to_graduation(self.real_sol_reserves) + } + + /// 是否已毕业 + #[inline(always)] + pub fn is_graduated(&self) -> bool { + BondingCurveMath::is_graduated(self.complete, self.real_token_reserves) + } + + /// 买入计算 + #[inline(always)] + pub fn calculate_buy(&self, sol_amount: u64) -> BuyResult { + BondingCurveMath::calculate_buy( + sol_amount, + self.virtual_sol_reserves, + self.virtual_token_reserves, + self.real_token_reserves, + ) + } + + /// 卖出计算 + #[inline(always)] + pub fn calculate_sell(&self, token_amount: u64) -> SellResult { + BondingCurveMath::calculate_sell( + token_amount, + self.virtual_sol_reserves, + self.virtual_token_reserves, + ) + } + + /// 验证 k 不变量 (用于数据完整性检查) + pub fn verify_invariant(&self) -> SniperResult { + let k_current = + self.virtual_sol_reserves as u128 * self.virtual_token_reserves as u128; + let deviation = if k_current > K_INVARIANT { + (k_current - K_INVARIANT) as f64 / K_INVARIANT as f64 + } else { + (K_INVARIANT - k_current) as f64 / K_INVARIANT as f64 + }; + + // 允许 0.001% 的偏差 (整数除法舍入) + if deviation > 0.00001 { + return Err(SniperError::InvariantViolation { + deviation_pct: deviation * 100.0, + }); + } + + Ok(deviation * 100.0) + } + + /// 获取创建者地址 (base58 编码) + pub fn creator_base58(&self) -> String { + bs58::encode(&self.creator).into_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_initial_state() { + let creator = [1u8; 32]; + let state = BondingCurveState::new_initial(creator); + + assert_eq!(state.virtual_sol_reserves, INITIAL_VIRTUAL_SOL); + assert_eq!(state.virtual_token_reserves, INITIAL_VIRTUAL_TOKENS); + assert_eq!(state.real_token_reserves, INITIAL_REAL_TOKENS); + assert_eq!(state.real_sol_reserves, 0); + assert!(!state.complete); + assert_eq!(state.progress(), 0.0); + assert!(!state.is_graduated()); + } + + #[test] + fn test_price_at_genesis() { + let state = BondingCurveState::new_initial([0u8; 32]); + let price = state.price(); + assert!( + (price - 2.7959e-8).abs() < 1e-12, + "Genesis price mismatch: {}", + price + ); + } + + #[test] + fn test_market_cap_at_genesis() { + let state = BondingCurveState::new_initial([0u8; 32]); + let mcap = state.market_cap_sol(); + // 初始市值 ≈ 27.96 SOL + assert!( + (mcap - 27.96).abs() < 0.01, + "Genesis market cap mismatch: {}", + mcap + ); + } + + #[test] + fn test_serialization_roundtrip() { + let creator = [42u8; 32]; + let state = BondingCurveState::new_initial(creator); + + // 手动构建字节 + let mut data = Vec::with_capacity(BondingCurveState::ACCOUNT_SIZE); + data.extend_from_slice(&state.discriminator.to_le_bytes()); + data.extend_from_slice(&state.virtual_token_reserves.to_le_bytes()); + data.extend_from_slice(&state.virtual_sol_reserves.to_le_bytes()); + data.extend_from_slice(&state.real_token_reserves.to_le_bytes()); + data.extend_from_slice(&state.real_sol_reserves.to_le_bytes()); + data.extend_from_slice(&state.token_total_supply.to_le_bytes()); + data.push(0); // complete = false + data.extend_from_slice(&creator); + + let parsed = BondingCurveState::from_bytes(&data).unwrap(); + assert_eq!(parsed.virtual_sol_reserves, state.virtual_sol_reserves); + assert_eq!(parsed.virtual_token_reserves, state.virtual_token_reserves); + assert_eq!(parsed.real_token_reserves, state.real_token_reserves); + assert_eq!(parsed.creator, creator); + } + + #[test] + fn test_invariant_verification() { + let state = BondingCurveState::new_initial([0u8; 32]); + let deviation = state.verify_invariant().unwrap(); + assert!(deviation < 0.00001, "Initial k deviation: {}%", deviation); + } +} diff --git a/src/bonding_curve/math.rs b/src/bonding_curve/math.rs new file mode 100644 index 0000000..dc754a4 --- /dev/null +++ b/src/bonding_curve/math.rs @@ -0,0 +1,638 @@ +//! Pump.fun Bonding Curve 核心数学计算 +//! +//! 基于虚拟恒定乘积 AMM (x·y=k) 的精确实现。 +//! 所有计算使用 u128 中间值避免 u64 溢出,整数除法模拟链上行为。 +//! +//! # 核心公式 +//! - 买入: tokens_out = input_net * virtual_token / (virtual_sol + input_net) +//! - 卖出: sol_out = tokens * virtual_sol / (virtual_token + tokens) +//! - 价格: price = virtual_sol / virtual_token +//! - 市值: mcap = virtual_sol * total_supply / virtual_token +//! - 进度: progress = 1 - real_token / initial_real_token + +/// SOL 精度: 1 SOL = 10^9 lamports +pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; + +/// Token 精度: 6 decimals +pub const TOKEN_DECIMALS: u32 = 6; + +/// Token 缩放因子: 10^6 +pub const TOKEN_SCALING: u64 = 1_000_000; + +/// 初始虚拟 SOL 储备 (30 SOL) +pub const INITIAL_VIRTUAL_SOL: u64 = 30 * LAMPORTS_PER_SOL; + +/// 初始虚拟代币储备 (1,073,000,191 tokens) +pub const INITIAL_VIRTUAL_TOKENS: u64 = 1_073_000_191 * TOKEN_SCALING; + +/// 初始真实代币储备 (793,100,000 tokens) +pub const INITIAL_REAL_TOKENS: u64 = 793_100_000 * TOKEN_SCALING; + +/// 虚拟合成代币 = 虚拟总 - 真实 (279,900,191 tokens) +pub const VIRTUAL_SYNTHETIC_TOKENS: u64 = INITIAL_VIRTUAL_TOKENS - INITIAL_REAL_TOKENS; + +/// PumpSwap 预留代币 (206,900,000 tokens) +pub const PUMPSWAP_RESERVED_TOKENS: u64 = 206_900_000 * TOKEN_SCALING; + +/// 恒定乘积 k = 30 SOL × 1,073,000,191 tokens +pub const K_INVARIANT: u128 = INITIAL_VIRTUAL_SOL as u128 * INITIAL_VIRTUAL_TOKENS as u128; + +/// 毕业阈值: virtual_sol = 115 SOL (30 virtual + 85 real) +pub const GRADUATION_VIRTUAL_SOL: u64 = 115 * LAMPORTS_PER_SOL; + +/// 毕业阈值: real_sol = 85 SOL +pub const GRADUATION_REAL_SOL: u64 = 85 * LAMPORTS_PER_SOL; + +/// 协议费 (basis points): 0.95% +pub const PROTOCOL_FEE_BPS: u64 = 95; + +/// 创建者费 (basis points): 0.30% +pub const CREATOR_FEE_BPS: u64 = 30; + +/// 总费用 (basis points): 1.25% +pub const TOTAL_FEE_BPS: u64 = PROTOCOL_FEE_BPS + CREATOR_FEE_BPS; + +/// Pump.fun 程序 ID +pub const PUMP_FUN_PROGRAM_ID: &str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; + +/// Bonding Curve 账户 discriminator: sha256("account:BondingCurve")[0..8] +pub const BONDING_CURVE_DISCRIMINATOR: [u8; 8] = [0x17, 0xb7, 0xf8, 0x37, 0x60, 0xd8, 0xac, 0x60]; + +/// Bonding Curve 数学计算引擎 +/// +/// 所有方法均为纯函数,无 IO,无状态,可在热路径中安全调用。 +/// 使用 u128 中间计算避免 u64 溢出。 +pub struct BondingCurveMath; + +impl BondingCurveMath { + // ================================================================ + // 买入计算 + // ================================================================ + + /// 计算买入获得的代币数量 + /// + /// # 参数 + /// - `sol_amount`: 投入的 SOL 数量 (lamports) + /// - `virtual_sol`: 当前虚拟 SOL 储备 (lamports) + /// - `virtual_token`: 当前虚拟代币储备 (raw units) + /// - `real_token`: 当前真实代币储备 (raw units) + /// + /// # 公式 + /// ```text + /// input_net = (sol_amount - 1) * 10000 / (fee_bps + 10000) + 1 + /// tokens_out = input_net * virtual_token / (virtual_sol + input_net) + /// result = min(tokens_out, real_token) + /// ``` + #[inline(always)] + pub fn calculate_buy( + sol_amount: u64, + virtual_sol: u64, + virtual_token: u64, + real_token: u64, + ) -> BuyResult { + if sol_amount == 0 || real_token == 0 { + return BuyResult { + tokens_out: 0, + input_after_fee: 0, + fee_amount: 0, + price_impact_bps: 0, + }; + } + + // Step 1: 扣除费用 (整数除法,与链上行为一致) + let input_after_fee = Self::deduct_fee(sol_amount); + let fee_amount = sol_amount - input_after_fee; + + // Step 2: 恒定乘积公式 (u128 防溢出) + let numerator = input_after_fee as u128 * virtual_token as u128; + let denominator = virtual_sol as u128 + input_after_fee as u128; + let tokens_out = (numerator / denominator) as u64; + + // Step 3: 不超过真实储备 + let tokens_out = tokens_out.min(real_token); + + // 计算价格影响 + let price_impact_bps = + Self::calculate_price_impact_bps(input_after_fee, virtual_sol, virtual_token); + + BuyResult { + tokens_out, + input_after_fee, + fee_amount, + price_impact_bps, + } + } + + /// 计算买入指定数量代币所需的 SOL + /// + /// # 公式 + /// ```text + /// sol_cost = tokens * virtual_sol / (virtual_token - tokens) + 1 + /// total_cost = sol_cost + fees(sol_cost) + /// ``` + #[inline(always)] + pub fn calculate_sol_for_tokens( + token_amount: u64, + virtual_sol: u64, + virtual_token: u64, + real_token: u64, + ) -> Option { + let capped_amount = token_amount.min(real_token); + if capped_amount == 0 || capped_amount >= virtual_token { + return None; + } + + // sol_cost = tokens * virtual_sol / (virtual_token - tokens) + 1 + let numerator = capped_amount as u128 * virtual_sol as u128; + let denominator = virtual_token as u128 - capped_amount as u128; + let sol_cost = (numerator / denominator) as u64 + 1; + + // 加上费用 + let total_cost = Self::add_fee(sol_cost); + let fee_amount = total_cost - sol_cost; + + Some(SolForTokensResult { + sol_cost, + total_cost, + fee_amount, + }) + } + + // ================================================================ + // 卖出计算 + // ================================================================ + + /// 计算卖出代币获得的 SOL 数量 + /// + /// # 公式 + /// ```text + /// sol_out_raw = token_amount * virtual_sol / (virtual_token + token_amount) + /// sol_out = sol_out_raw * (10000 - fee_bps) / 10000 + /// ``` + #[inline(always)] + pub fn calculate_sell( + token_amount: u64, + virtual_sol: u64, + virtual_token: u64, + ) -> SellResult { + if token_amount == 0 { + return SellResult { + sol_out: 0, + sol_out_before_fee: 0, + fee_amount: 0, + }; + } + + // Step 1: 恒定乘积 + let numerator = token_amount as u128 * virtual_sol as u128; + let denominator = virtual_token as u128 + token_amount as u128; + let sol_out_raw = (numerator / denominator) as u64; + + // Step 2: 扣除费用 + let sol_out = sol_out_raw * (10000 - TOTAL_FEE_BPS) / 10000; + let fee_amount = sol_out_raw - sol_out; + + SellResult { + sol_out, + sol_out_before_fee: sol_out_raw, + fee_amount, + } + } + + // ================================================================ + // 价格与市值 + // ================================================================ + + /// 计算当前边际价格 (lamports per raw token unit) + #[inline(always)] + pub fn spot_price(virtual_sol: u64, virtual_token: u64) -> f64 { + if virtual_token == 0 { + return 0.0; + } + virtual_sol as f64 / virtual_token as f64 + } + + /// 计算当前边际价格 (SOL per token, 人类可读) + #[inline(always)] + pub fn spot_price_sol_per_token(virtual_sol: u64, virtual_token: u64) -> f64 { + Self::spot_price(virtual_sol, virtual_token) + * TOKEN_SCALING as f64 + / LAMPORTS_PER_SOL as f64 + } + + /// 计算市值 (lamports) + /// + /// # 公式 + /// ```text + /// market_cap = virtual_sol * total_supply / virtual_token + /// ``` + #[inline(always)] + pub fn market_cap(virtual_sol: u64, virtual_token: u64, total_supply: u64) -> u64 { + if virtual_token == 0 { + return 0; + } + (virtual_sol as u128 * total_supply as u128 / virtual_token as u128) as u64 + } + + /// 计算市值 (SOL) + #[inline(always)] + pub fn market_cap_sol(virtual_sol: u64, virtual_token: u64, total_supply: u64) -> f64 { + Self::market_cap(virtual_sol, virtual_token, total_supply) as f64 / LAMPORTS_PER_SOL as f64 + } + + // ================================================================ + // 进度与毕业 + // ================================================================ + + /// 计算 Bonding Curve 进度 (0.0 ~ 1.0) + /// + /// # 公式 + /// ```text + /// progress = 1 - real_token_reserves / INITIAL_REAL_TOKENS + /// ``` + #[inline(always)] + pub fn progress(real_token_reserves: u64) -> f64 { + if real_token_reserves >= INITIAL_REAL_TOKENS { + return 0.0; + } + 1.0 - (real_token_reserves as f64 / INITIAL_REAL_TOKENS as f64) + } + + /// 计算距离毕业还需多少 SOL (lamports) + #[inline(always)] + pub fn sol_to_graduation(real_sol_reserves: u64) -> u64 { + GRADUATION_REAL_SOL.saturating_sub(real_sol_reserves) + } + + /// 判断是否已毕业 + #[inline(always)] + pub fn is_graduated(complete: bool, real_token_reserves: u64) -> bool { + complete || real_token_reserves == 0 + } + + // ================================================================ + // 价格影响 + // ================================================================ + + /// 计算买入的价格影响 (basis points) + #[inline(always)] + pub fn calculate_price_impact_bps( + input_amount: u64, + virtual_sol: u64, + virtual_token: u64, + ) -> u64 { + if virtual_sol == 0 || virtual_token == 0 || input_amount == 0 { + return 0; + } + + // 交易前价格 (放大 10^6 精度) + let price_before = virtual_sol as u128 * 1_000_000 / virtual_token as u128; + + // 交易后价格 + let new_virtual_sol = virtual_sol as u128 + input_amount as u128; + let new_virtual_token = (virtual_sol as u128 * virtual_token as u128) / new_virtual_sol; + let price_after = new_virtual_sol * 1_000_000 / new_virtual_token; + + // 价格影响 (bps) + if price_before == 0 { + return 0; + } + ((price_after - price_before) * 10_000 / price_before) as u64 + } + + // ================================================================ + // 费用计算 + // ================================================================ + + /// 扣除费用 (与链上整数除法行为一致) + /// + /// # 公式 + /// ```text + /// net = (amount - 1) * 10000 / (fee_bps + 10000) + 1 + /// ``` + #[inline(always)] + pub fn deduct_fee(amount: u64) -> u64 { + if amount <= 1 { + return amount; + } + (amount - 1) * 10000 / (TOTAL_FEE_BPS + 10000) + 1 + } + + /// 加上费用 (反向计算) + /// + /// # 公式 + /// ```text + /// gross = net * (10000 + fee_bps) / 10000 + 1 + /// ``` + #[inline(always)] + pub fn add_fee(net_amount: u64) -> u64 { + net_amount * (10000 + TOTAL_FEE_BPS) / 10000 + 1 + } + + /// 计算费用金额 + #[inline(always)] + pub fn fee_amount(gross_amount: u64) -> u64 { + gross_amount - Self::deduct_fee(gross_amount) + } + + // ================================================================ + // 滑点保护 + // ================================================================ + + /// 计算最小输出代币数 (滑点保护) + /// + /// # 参数 + /// - `expected_tokens`: 期望获得的代币数 + /// - `slippage_bps`: 滑点容忍度 (basis points, 100 = 1%) + #[inline(always)] + pub fn min_tokens_out(expected_tokens: u64, slippage_bps: u64) -> u64 { + expected_tokens * (10000 - slippage_bps) / 10000 + } + + /// 计算最小输出 SOL 数 (滑点保护) + #[inline(always)] + pub fn min_sol_out(expected_sol: u64, slippage_bps: u64) -> u64 { + expected_sol * (10000 - slippage_bps) / 10000 + } + + // ================================================================ + // 模拟:给定 SOL 预算,计算买入后的新状态 + // ================================================================ + + /// 计算在给定状态下,投入 sol_amount 后的新状态 + #[inline(always)] + pub fn simulate_buy( + sol_amount: u64, + virtual_sol: u64, + virtual_token: u64, + real_token: u64, + ) -> SimulatedState { + let result = Self::calculate_buy(sol_amount, virtual_sol, virtual_token, real_token); + + SimulatedState { + virtual_sol: virtual_sol + result.input_after_fee, + virtual_token: virtual_token - result.tokens_out, + real_token: real_token - result.tokens_out, + real_sol: (virtual_sol + result.input_after_fee) - INITIAL_VIRTUAL_SOL, + tokens_received: result.tokens_out, + fee_paid: result.fee_amount, + } + } + + /// 估算在指定进度买入 1 SOL,毕业时卖出的收益倍数 + /// + /// 基于预计算数据的快速查表 + 线性插值 + #[inline(always)] + pub fn estimate_roi_at_graduation(progress: f64) -> f64 { + // 预计算数据点 (投入 1 SOL, 毕业时卖出) + const DATA: [(f64, f64); 9] = [ + (0.0, 12.36), + (0.1, 10.79), + (0.2, 9.30), + (0.3, 7.88), + (0.5, 5.32), + (0.7, 3.19), + (0.8, 2.31), + (0.9, 1.57), + (1.0, 1.00), + ]; + + let p = progress.clamp(0.0, 1.0); + + // 线性插值 + for i in 0..DATA.len() - 1 { + let (p0, r0) = DATA[i]; + let (p1, r1) = DATA[i + 1]; + if p >= p0 && p <= p1 { + let t = (p - p0) / (p1 - p0); + return r0 + t * (r1 - r0); + } + } + + 1.0 + } +} + +// ================================================================ +// 结果结构体 +// ================================================================ + +/// 买入计算结果 +#[derive(Debug, Clone, Copy)] +pub struct BuyResult { + /// 获得的代币数量 (raw units) + pub tokens_out: u64, + /// 扣除费用后的实际输入 (lamports) + pub input_after_fee: u64, + /// 费用 (lamports) + pub fee_amount: u64, + /// 价格影响 (basis points) + pub price_impact_bps: u64, +} + +/// 卖出计算结果 +#[derive(Debug, Clone, Copy)] +pub struct SellResult { + /// 获得的 SOL (lamports, 扣费后) + pub sol_out: u64, + /// 获得的 SOL (lamports, 扣费前) + pub sol_out_before_fee: u64, + /// 费用 (lamports) + pub fee_amount: u64, +} + +/// 买入指定代币数所需 SOL 的计算结果 +#[derive(Debug, Clone, Copy)] +pub struct SolForTokensResult { + /// SOL 成本 (lamports, 不含费) + pub sol_cost: u64, + /// 总成本 (lamports, 含费) + pub total_cost: u64, + /// 费用 (lamports) + pub fee_amount: u64, +} + +/// 模拟买入后的状态 +#[derive(Debug, Clone, Copy)] +pub struct SimulatedState { + pub virtual_sol: u64, + pub virtual_token: u64, + pub real_token: u64, + pub real_sol: u64, + pub tokens_received: u64, + pub fee_paid: u64, +} + +// ================================================================ +// 单元测试 +// ================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_initial_price() { + let price = BondingCurveMath::spot_price_sol_per_token( + INITIAL_VIRTUAL_SOL, + INITIAL_VIRTUAL_TOKENS, + ); + // 初始价格 ≈ 2.796 × 10^-8 SOL/token + assert!( + (price - 2.7959e-8).abs() < 1e-12, + "Initial price mismatch: {}", + price + ); + } + + #[test] + fn test_buy_1_sol_at_genesis() { + let result = BondingCurveMath::calculate_buy( + LAMPORTS_PER_SOL, + INITIAL_VIRTUAL_SOL, + INITIAL_VIRTUAL_TOKENS, + INITIAL_REAL_TOKENS, + ); + + // 应该获得约 34.2M tokens + let tokens_human = result.tokens_out as f64 / TOKEN_SCALING as f64; + assert!( + (tokens_human - 34_199_209.0).abs() < 100.0, + "Tokens out mismatch: {}", + tokens_human + ); + + // 费用约 1.235% + let fee_pct = result.fee_amount as f64 / LAMPORTS_PER_SOL as f64; + assert!( + (fee_pct - 0.01235).abs() < 0.001, + "Fee percentage mismatch: {}", + fee_pct + ); + } + + #[test] + fn test_round_trip_loss() { + // 买入 1 SOL + let buy = BondingCurveMath::calculate_buy( + LAMPORTS_PER_SOL, + INITIAL_VIRTUAL_SOL, + INITIAL_VIRTUAL_TOKENS, + INITIAL_REAL_TOKENS, + ); + + // 立即卖出 + let sell = BondingCurveMath::calculate_sell( + buy.tokens_out, + INITIAL_VIRTUAL_SOL + buy.input_after_fee, + INITIAL_VIRTUAL_TOKENS - buy.tokens_out, + ); + + // 往返损失约 5% + let loss_pct = 1.0 - sell.sol_out as f64 / LAMPORTS_PER_SOL as f64; + assert!( + loss_pct > 0.04 && loss_pct < 0.06, + "Round trip loss mismatch: {:.4}%", + loss_pct * 100.0 + ); + } + + #[test] + fn test_graduation_threshold() { + // 毕业时 virtual_sol = 115 SOL + let virtual_token_at_grad = K_INVARIANT / GRADUATION_VIRTUAL_SOL as u128; + let price = GRADUATION_VIRTUAL_SOL as f64 / virtual_token_at_grad as f64; + let price_sol = price * TOKEN_SCALING as f64 / LAMPORTS_PER_SOL as f64; + + // 毕业价格 ≈ 4.108 × 10^-7 SOL/token + assert!( + (price_sol - 4.108e-7).abs() < 1e-10, + "Graduation price mismatch: {}", + price_sol + ); + } + + #[test] + fn test_progress_calculation() { + // 初始进度 = 0% + assert_eq!(BondingCurveMath::progress(INITIAL_REAL_TOKENS), 0.0); + + // 一半售出 = 50% + let half = INITIAL_REAL_TOKENS / 2; + assert!( + (BondingCurveMath::progress(half) - 0.5).abs() < 0.001, + "Progress at half mismatch" + ); + + // 全部售出 = 100% + assert_eq!(BondingCurveMath::progress(0), 1.0); + } + + #[test] + fn test_fee_calculation() { + // 1 SOL 的费用 + let fee = BondingCurveMath::fee_amount(LAMPORTS_PER_SOL); + let fee_pct = fee as f64 / LAMPORTS_PER_SOL as f64; + assert!( + (fee_pct - 0.01235).abs() < 0.001, + "Fee calculation mismatch: {:.4}%", + fee_pct * 100.0 + ); + } + + #[test] + fn test_min_tokens_out() { + let expected = 1_000_000; + let slippage_bps = 500; // 5% + let min = BondingCurveMath::min_tokens_out(expected, slippage_bps); + assert_eq!(min, 950_000); + } + + #[test] + fn test_roi_estimation() { + // 进度 0% → ROI ≈ 12.36x + let roi_0 = BondingCurveMath::estimate_roi_at_graduation(0.0); + assert!((roi_0 - 12.36).abs() < 0.01); + + // 进度 50% → ROI ≈ 5.32x + let roi_50 = BondingCurveMath::estimate_roi_at_graduation(0.5); + assert!((roi_50 - 5.32).abs() < 0.01); + + // 进度 100% → ROI = 1.0x + let roi_100 = BondingCurveMath::estimate_roi_at_graduation(1.0); + assert!((roi_100 - 1.0).abs() < 0.01); + } + + #[test] + fn test_invariant_preservation() { + let mut v_sol = INITIAL_VIRTUAL_SOL; + let mut v_token = INITIAL_VIRTUAL_TOKENS; + let mut r_token = INITIAL_REAL_TOKENS; + + // 执行 100 次 0.1 SOL 买入 + for _ in 0..100 { + let result = BondingCurveMath::calculate_buy( + LAMPORTS_PER_SOL / 10, + v_sol, + v_token, + r_token, + ); + v_sol += result.input_after_fee; + v_token -= result.tokens_out; + r_token -= result.tokens_out; + } + + // k 偏差应 < 0.001% + let k_current = v_sol as u128 * v_token as u128; + let deviation = if k_current > K_INVARIANT { + (k_current - K_INVARIANT) as f64 / K_INVARIANT as f64 + } else { + (K_INVARIANT - k_current) as f64 / K_INVARIANT as f64 + }; + assert!( + deviation < 0.00001, + "k deviation too large: {:.10}%", + deviation * 100.0 + ); + } +} diff --git a/src/bonding_curve/mod.rs b/src/bonding_curve/mod.rs new file mode 100644 index 0000000..25be548 --- /dev/null +++ b/src/bonding_curve/mod.rs @@ -0,0 +1,17 @@ +//! Pump.fun Bonding Curve 模块 +//! +//! 提供完整的 Bonding Curve 数学模型、链上账户解析和交易模拟。 + +pub mod account; +pub mod math; +pub mod simulator; + +pub use account::BondingCurveState; +pub use math::{ + BondingCurveMath, BuyResult, SellResult, SolForTokensResult, SimulatedState, + INITIAL_REAL_TOKENS, INITIAL_VIRTUAL_SOL, INITIAL_VIRTUAL_TOKENS, + K_INVARIANT, LAMPORTS_PER_SOL, TOKEN_SCALING, + GRADUATION_REAL_SOL, GRADUATION_VIRTUAL_SOL, + TOTAL_FEE_BPS, PUMP_FUN_PROGRAM_ID, +}; +pub use simulator::BondingCurveSimulator; diff --git a/src/bonding_curve/simulator.rs b/src/bonding_curve/simulator.rs new file mode 100644 index 0000000..a441608 --- /dev/null +++ b/src/bonding_curve/simulator.rs @@ -0,0 +1,276 @@ +//! Bonding Curve 完整交易模拟器 +//! +//! 用于: +//! 1. 策略回测: 模拟从创建到毕业的完整交易序列 +//! 2. 收益计算: 给定买入时机,计算毕业时卖出收益 +//! 3. 最优策略: 搜索最优买入/卖出时机 + +use super::math::*; + +/// Bonding Curve 模拟器 +pub struct BondingCurveSimulator { + pub virtual_sol: u64, + pub virtual_token: u64, + pub real_token: u64, + pub real_sol: u64, + pub total_supply: u64, + pub trade_history: Vec, +} + +/// 交易记录 +#[derive(Debug, Clone)] +pub struct TradeRecord { + pub trade_type: TradeType, + pub sol_amount: u64, + pub token_amount: u64, + pub fee_paid: u64, + pub price_before: f64, + pub price_after: f64, + pub progress_before: f64, + pub progress_after: f64, +} + +/// 交易类型 +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TradeType { + Buy, + Sell, +} + +impl BondingCurveSimulator { + /// 创建新的模拟器 (代币刚创建) + pub fn new() -> Self { + Self { + virtual_sol: INITIAL_VIRTUAL_SOL, + virtual_token: INITIAL_VIRTUAL_TOKENS, + real_token: INITIAL_REAL_TOKENS, + real_sol: 0, + total_supply: 1_000_000_000 * TOKEN_SCALING, + trade_history: Vec::new(), + } + } + + /// 从链上状态恢复模拟器 + pub fn from_state(state: &super::account::BondingCurveState) -> Self { + Self { + virtual_sol: state.virtual_sol_reserves, + virtual_token: state.virtual_token_reserves, + real_token: state.real_token_reserves, + real_sol: state.real_sol_reserves, + total_supply: state.token_total_supply, + trade_history: Vec::new(), + } + } + + /// 执行买入 + pub fn buy(&mut self, sol_amount: u64) -> BuyResult { + let price_before = self.price(); + let progress_before = self.progress(); + + let result = BondingCurveMath::calculate_buy( + sol_amount, + self.virtual_sol, + self.virtual_token, + self.real_token, + ); + + // 更新状态 + self.virtual_sol += result.input_after_fee; + self.virtual_token -= result.tokens_out; + self.real_token -= result.tokens_out; + self.real_sol += result.input_after_fee; + + let price_after = self.price(); + let progress_after = self.progress(); + + self.trade_history.push(TradeRecord { + trade_type: TradeType::Buy, + sol_amount, + token_amount: result.tokens_out, + fee_paid: result.fee_amount, + price_before, + price_after, + progress_before, + progress_after, + }); + + result + } + + /// 执行卖出 + pub fn sell(&mut self, token_amount: u64) -> SellResult { + let price_before = self.price(); + let progress_before = self.progress(); + + let result = BondingCurveMath::calculate_sell( + token_amount, + self.virtual_sol, + self.virtual_token, + ); + + // 更新状态 + self.virtual_sol -= result.sol_out_before_fee; + self.virtual_token += token_amount; + self.real_token += token_amount; + self.real_sol = self.real_sol.saturating_sub(result.sol_out_before_fee); + + let price_after = self.price(); + let progress_after = self.progress(); + + self.trade_history.push(TradeRecord { + trade_type: TradeType::Sell, + sol_amount: result.sol_out, + token_amount, + fee_paid: result.fee_amount, + price_before, + price_after, + progress_before, + progress_after, + }); + + result + } + + /// 当前价格 (SOL/token) + pub fn price(&self) -> f64 { + BondingCurveMath::spot_price_sol_per_token(self.virtual_sol, self.virtual_token) + } + + /// 当前进度 + pub fn progress(&self) -> f64 { + BondingCurveMath::progress(self.real_token) + } + + /// 是否已毕业 + pub fn is_graduated(&self) -> bool { + self.real_token == 0 || self.real_sol >= GRADUATION_REAL_SOL + } + + /// 计算从当前状态到毕业需要的总 SOL + pub fn sol_to_graduation(&self) -> u64 { + BondingCurveMath::sol_to_graduation(self.real_sol) + } + + /// 计算在指定进度买入 sol_amount,毕业时卖出的收益倍数 + pub fn roi_at_progress(&self, target_progress: f64, sol_amount: u64) -> f64 { + let mut sim = BondingCurveSimulator::new(); + + // 快速推进到目标进度 + let step = LAMPORTS_PER_SOL / 10; // 0.1 SOL + while sim.progress() < target_progress && !sim.is_graduated() { + sim.buy(step); + } + + // 在目标进度买入 + let buy_result = sim.buy(sol_amount); + let tokens_held = buy_result.tokens_out; + + if tokens_held == 0 { + return 0.0; + } + + // 推进到毕业 + while !sim.is_graduated() { + sim.buy(step); + } + + // 毕业时卖出 + let sell_result = sim.sell(tokens_held); + + sell_result.sol_out as f64 / sol_amount as f64 + } + + /// 生成完整的进度-收益表 + pub fn generate_roi_table(&self, sol_amount: u64) -> Vec<(f64, f64)> { + let mut table = Vec::new(); + let progress_steps = [0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95]; + + for &p in &progress_steps { + let roi = self.roi_at_progress(p, sol_amount); + table.push((p, roi)); + } + + table + } +} + +impl Default for BondingCurveSimulator { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_simulator_basic() { + let mut sim = BondingCurveSimulator::new(); + + assert_eq!(sim.progress(), 0.0); + assert!(!sim.is_graduated()); + + // 买入 1 SOL + let result = sim.buy(LAMPORTS_PER_SOL); + assert!(result.tokens_out > 0); + assert!(sim.progress() > 0.0); + } + + #[test] + fn test_simulator_graduation() { + let mut sim = BondingCurveSimulator::new(); + + // 持续买入直到毕业 + let mut total_spent = 0u64; + let step = LAMPORTS_PER_SOL; // 1 SOL + while !sim.is_graduated() { + sim.buy(step); + total_spent += step; + } + + // 应该花费约 85 SOL + let total_sol = total_spent as f64 / LAMPORTS_PER_SOL as f64; + assert!( + (total_sol - 85.0).abs() < 1.0, + "Graduation cost mismatch: {} SOL", + total_sol + ); + } + + #[test] + fn test_roi_table() { + let sim = BondingCurveSimulator::new(); + let table = sim.generate_roi_table(LAMPORTS_PER_SOL); + + // 进度 0% 的 ROI 应该最高 + assert!(table[0].1 > table[5].1, "ROI should decrease with progress"); + + // 所有 ROI 应该 > 1.0 (理论上) + for (p, roi) in &table { + if *p < 0.95 { + assert!(*roi > 1.0, "ROI at progress {} should be > 1.0, got {}", p, roi); + } + } + } + + #[test] + fn test_buy_sell_roundtrip() { + let mut sim = BondingCurveSimulator::new(); + + // 买入 1 SOL + let buy_result = sim.buy(LAMPORTS_PER_SOL); + let tokens = buy_result.tokens_out; + + // 立即卖出 + let sell_result = sim.sell(tokens); + + // 应该亏损约 5% + let loss_pct = 1.0 - sell_result.sol_out as f64 / LAMPORTS_PER_SOL as f64; + assert!( + loss_pct > 0.04 && loss_pct < 0.06, + "Round trip loss: {:.2}%", + loss_pct * 100.0 + ); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9b46dc5 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,184 @@ +//! 配置管理模块 +//! +//! 从 TOML 文件加载配置,支持环境变量覆盖。 + +use serde::Deserialize; +use std::path::Path; + +use crate::error::{SniperError, SniperResult}; + +/// 顶层配置 +#[derive(Debug, Clone, Deserialize)] +pub struct BotConfig { + pub bot: BotSettings, + pub detection: DetectionConfig, + pub decision: DecisionConfig, + pub execution: ExecutionConfig, + pub position: PositionConfig, + pub risk: RiskConfig, + pub monitoring: MonitoringConfig, +} + +/// Bot 基础设置 +#[derive(Debug, Clone, Deserialize)] +pub struct BotSettings { + /// 运行模式: "sniper" | "graduation" | "both" + pub mode: String, + /// 模拟模式 (不实际发送交易) + pub dry_run: bool, + /// 日志级别 + pub log_level: String, +} + +/// 检测层配置 +#[derive(Debug, Clone, Deserialize)] +pub struct DetectionConfig { + pub yellowstone_grpc_url: String, + pub yellowstone_x_token: Option, + pub backup_ws_url: String, + pub graduation_trigger_sol: f64, + pub event_channel_capacity: usize, +} + +/// 决策层配置 +#[derive(Debug, Clone, Deserialize)] +pub struct DecisionConfig { + pub min_score_threshold: f64, + pub max_price_impact_bps: u64, + pub slippage_bps: u64, + pub blacklisted_creators: Vec, + pub min_creator_success_rate: f64, + pub max_creator_total_tokens: u32, +} + +/// 执行层配置 +#[derive(Debug, Clone, Deserialize)] +pub struct ExecutionConfig { + pub rpc_url: String, + pub jito_block_engine_url: String, + pub jito_tip_lamports: u64, + pub default_priority_fee: u64, + pub compute_unit_limit: u32, + pub confirm_timeout_secs: u64, + pub max_retries: u32, + pub enable_simulation: bool, + pub enable_multi_relay: bool, + /// 钱包文件路径 (逗号分隔) + #[serde(default)] + pub wallet_paths: Vec, +} + +/// 仓位管理配置 +#[derive(Debug, Clone, Deserialize)] +pub struct PositionConfig { + pub max_position_sol: f64, + pub min_position_sol: f64, + pub max_concurrent_positions: usize, + pub take_profit_1_roi: f64, + pub take_profit_1_sell_pct: f64, + pub take_profit_2_roi: f64, + pub take_profit_2_sell_pct: f64, + pub take_profit_3_roi: f64, + pub take_profit_3_sell_pct: f64, + pub stop_loss_pct: f64, + pub time_stop_secs: u64, +} + +/// 风控配置 +#[derive(Debug, Clone, Deserialize)] +pub struct RiskConfig { + pub daily_loss_limit_sol: f64, + pub max_consecutive_failures: u32, + pub circuit_breaker_cooldown_minutes: i64, + pub max_single_loss_sol: f64, + pub max_daily_trades: u32, +} + +/// 监控配置 +#[derive(Debug, Clone, Deserialize)] +pub struct MonitoringConfig { + pub prometheus_port: u16, + pub json_logs: bool, + pub metrics_flush_interval_secs: u64, +} + +impl BotConfig { + /// 从 TOML 文件加载配置 + pub fn load(path: &str) -> SniperResult { + let content = std::fs::read_to_string(Path::new(path)) + .map_err(|e| SniperError::Config(format!("Failed to read config file '{}': {}", path, e)))?; + + let config: BotConfig = toml::from_str(&content) + .map_err(|e| SniperError::Config(format!("Failed to parse config: {}", e)))?; + + // 环境变量覆盖 + let config = Self::apply_env_overrides(config); + + Ok(config) + } + + /// 环境变量覆盖关键配置 + fn apply_env_overrides(mut config: BotConfig) -> Self { + if let Ok(url) = std::env::var("SOLANA_RPC_URL") { + config.execution.rpc_url = url; + } + if let Ok(url) = std::env::var("YELLOWSTONE_GRPC_URL") { + config.detection.yellowstone_grpc_url = url; + } + if let Ok(token) = std::env::var("YELLOWSTONE_X_TOKEN") { + config.detection.yellowstone_x_token = Some(token); + } + if let Ok(url) = std::env::var("JITO_BLOCK_ENGINE_URL") { + config.execution.jito_block_engine_url = url; + } + if let Ok(tip) = std::env::var("JITO_TIP_LAMPORTS") { + if let Ok(tip) = tip.parse() { + config.execution.jito_tip_lamports = tip; + } + } + if let Ok(paths) = std::env::var("WALLET_PATHS") { + config.execution.wallet_paths = paths.split(',').map(|s| s.trim().to_string()).collect(); + } + if let Ok(val) = std::env::var("MAX_POSITION_SOL") { + if let Ok(val) = val.parse() { + config.position.max_position_sol = val; + } + } + if let Ok(val) = std::env::var("DAILY_LOSS_LIMIT_SOL") { + if let Ok(val) = val.parse() { + config.risk.daily_loss_limit_sol = val; + } + } + if let Ok(val) = std::env::var("LOG_LEVEL") { + config.bot.log_level = val; + } + + config + } + + /// 验证配置有效性 + pub fn validate(&self) -> SniperResult<()> { + if self.position.max_position_sol <= 0.0 { + return Err(SniperError::Config("max_position_sol must be > 0".into())); + } + if self.position.max_position_sol < self.position.min_position_sol { + return Err(SniperError::Config( + "max_position_sol must be >= min_position_sol".into(), + )); + } + if self.risk.daily_loss_limit_sol <= 0.0 { + return Err(SniperError::Config("daily_loss_limit_sol must be > 0".into())); + } + if self.decision.slippage_bps > 5000 { + return Err(SniperError::Config( + "slippage_bps > 5000 (50%) is dangerously high".into(), + )); + } + if self.execution.wallet_paths.is_empty() && !self.bot.dry_run { + return Err(SniperError::Config( + "No wallet paths configured. Set WALLET_PATHS or execution.wallet_paths".into(), + )); + } + Ok(()) + } +} \ No newline at end of file diff --git a/src/decision/mod.rs b/src/decision/mod.rs new file mode 100644 index 0000000..c08316a --- /dev/null +++ b/src/decision/mod.rs @@ -0,0 +1,107 @@ +//! 决策引擎模块 +//! +//! 负责: +//! 1. Rug Pull 过滤 +//! 2. 信号评分 +//! 3. 仓位计算 +//! 4. 最终 GO/NO-GO 决策 + +pub mod position_sizer; +pub mod rug_filter; +pub mod scorer; + +pub use position_sizer::PositionSizer; +pub use rug_filter::RugFilter; +pub use scorer::SignalScorer; + +use crate::config::DecisionConfig; +use crate::detection::TokenCreatedEvent; + +/// 决策引擎 +pub struct DecisionEngine { + rug_filter: RugFilter, + scorer: SignalScorer, + position_sizer: PositionSizer, + config: DecisionConfig, +} + +/// 决策结果 +#[derive(Debug, Clone)] +pub struct Decision { + /// 是否买入 + pub should_buy: bool, + /// 综合评分 (0-100) + pub score: f64, + /// 建议仓位 (SOL) + pub position_size_sol: f64, + /// 预期获得代币数 + pub expected_tokens: u64, + /// 决策原因 + pub reason: String, +} + +impl DecisionEngine { + pub fn new(config: DecisionConfig) -> Self { + Self { + rug_filter: RugFilter::new(&config), + scorer: SignalScorer::new(&config), + position_sizer: PositionSizer::new(&config), + config, + } + } + + /// 评估新币是否值得狙击 + pub async fn evaluate_new_token(&self, event: &TokenCreatedEvent) -> Decision { + // Step 1: Rug 过滤 (硬过滤,不通过直接拒绝) + let rug_result = self.rug_filter.evaluate(event).await; + if !rug_result.passed { + return Decision { + should_buy: false, + score: 0.0, + position_size_sol: 0.0, + expected_tokens: 0, + reason: format!("Rug 过滤拒绝: {}", rug_result.reason), + }; + } + + // Step 2: 信号评分 + let score = self.scorer.score(event, &rug_result).await; + + // Step 3: 阈值判断 + if score < self.config.min_score_threshold { + return Decision { + should_buy: false, + score, + position_size_sol: 0.0, + expected_tokens: 0, + reason: format!("评分 {:.1} < 阈值 {:.1}", score, self.config.min_score_threshold), + }; + } + + // Step 4: 仓位计算 + let position_size = self.position_sizer.calculate(score); + + // Step 5: 计算预期代币数 + let sol_lamports = (position_size * 1_000_000_000.0) as u64; + let buy_result = crate::bonding_curve::BondingCurveMath::calculate_buy( + sol_lamports, + crate::bonding_curve::INITIAL_VIRTUAL_SOL, + crate::bonding_curve::INITIAL_VIRTUAL_TOKENS, + crate::bonding_curve::INITIAL_REAL_TOKENS, + ); + + Decision { + should_buy: true, + score, + position_size_sol: position_size, + expected_tokens: buy_result.tokens_out, + reason: format!( + "评分={:.1} | Rug分数={:.0} | 仓位={:.4} SOL | 预期代币={}", + score, + rug_result.score, + position_size, + buy_result.tokens_out / crate::bonding_curve::TOKEN_SCALING, + ), + } + } +} \ No newline at end of file diff --git a/src/decision/position_sizer.rs b/src/decision/position_sizer.rs new file mode 100644 index 0000000..aab4db0 --- /dev/null +++ b/src/decision/position_sizer.rs @@ -0,0 +1,115 @@ +//! 仓位计算器 +//! +//! 根据评分和风控参数计算最优仓位大小 + +use crate::config::DecisionConfig; + +/// 仓位计算器 +pub struct PositionSizer { + max_position_sol: f64, + min_position_sol: f64, +} + +impl PositionSizer { + pub fn new(config: &DecisionConfig) -> Self { + // 从全局配置获取 (这里简化,实际应从 PositionConfig 获取) + Self { + max_position_sol: 0.1, // 默认值,实际从配置读取 + min_position_sol: 0.01, + } + } + + /// 使用完整配置初始化 + pub fn with_limits(max_sol: f64, min_sol: f64) -> Self { + Self { + max_position_sol: max_sol, + min_position_sol: min_sol, + } + } + + /// 根据评分计算仓位 + /// + /// 策略: 线性映射 + /// - 评分 60 (阈值) → min_position + /// - 评分 100 (满分) → max_position + /// + /// 公式: position = min + (max - min) * (score - threshold) / (100 - threshold) + pub fn calculate(&self, score: f64) -> f64 { + let threshold = 60.0; // 最低通过分数 + + if score <= threshold { + return 0.0; + } + + let normalized = (score - threshold) / (100.0 - threshold); + let position = self.min_position_sol + + (self.max_position_sol - self.min_position_sol) * normalized; + + position.clamp(self.min_position_sol, self.max_position_sol) + } + + /// Kelly 公式仓位计算 (高级) + /// + /// f* = (bp - q) / b + /// 其中: + /// - b = 赔率 (预期收益/预期亏损) + /// - p = 胜率 + /// - q = 1 - p + /// + /// 使用 Half-Kelly 降低风险 + pub fn kelly_fraction(&self, win_rate: f64, avg_win: f64, avg_loss: f64) -> f64 { + if avg_loss <= 0.0 || win_rate <= 0.0 || win_rate >= 1.0 { + return 0.0; + } + + let b = avg_win / avg_loss; // 赔率 + let p = win_rate; + let q = 1.0 - p; + + let kelly = (b * p - q) / b; + + // Half-Kelly (更保守) + let half_kelly = kelly * 0.5; + + half_kelly.clamp(0.0, 0.1) // 最大 10% 仓位 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_position_sizing() { + let sizer = PositionSizer::with_limits(0.1, 0.01); + + // 评分 60 (阈值) → 最小仓位 + let pos_60 = sizer.calculate(60.0); + assert!(pos_60 <= 0.011); // 接近 min + + // 评分 100 → 最大仓位 + let pos_100 = sizer.calculate(100.0); + assert!((pos_100 - 0.1).abs() < 0.001); + + // 评分 80 → 中间 + let pos_80 = sizer.calculate(80.0); + assert!(pos_80 > 0.01 && pos_80 < 0.1); + + // 评分 50 (低于阈值) → 0 + let pos_50 = sizer.calculate(50.0); + assert_eq!(pos_50, 0.0); + } + + #[test] + fn test_kelly_fraction() { + let sizer = PositionSizer::with_limits(0.1, 0.01); + + // 胜率 60%, 赔率 3:1 + let f = sizer.kelly_fraction(0.6, 3.0, 1.0); + assert!(f > 0.0 && f <= 0.1); + + // 胜率 50%, 赔率 1:1 → Kelly = 0 + let f2 = sizer.kelly_fraction(0.5, 1.0, 1.0); + assert_eq!(f2, 0.0); + } +} \ No newline at end of file diff --git a/src/decision/rug_filter.rs b/src/decision/rug_filter.rs new file mode 100644 index 0000000..b3b4bd3 --- /dev/null +++ b/src/decision/rug_filter.rs @@ -0,0 +1,182 @@ +//! Rug Pull 过滤器 +//! +//! 多层检测: +//! 1. 创建者黑名单 +//! 2. 元数据完整性 +//! 3. 创建者历史分析 +//! 4. 持仓集中度 +//! 5. 社交信号 + +use crate::config::DecisionConfig; +use crate::detection::TokenCreatedEvent; +use tracing::debug; + +/// Rug 过滤结果 +#[derive(Debug, Clone)] +pub struct RugFilterResult { + /// 是否通过 + pub passed: bool, + /// 安全评分 (0-100, 越高越安全) + pub score: f64, + /// 拒绝/警告原因 + pub reason: String, + /// 各维度得分明细 + pub details: Vec<(String, f64)>, +} + +/// Rug Pull 过滤器 +pub struct RugFilter { + blacklisted_creators: Vec, + max_creator_total_tokens: u32, + min_creator_success_rate: f64, +} + +impl RugFilter { + pub fn new(config: &DecisionConfig) -> Self { + Self { + blacklisted_creators: config.blacklisted_creators.clone(), + max_creator_total_tokens: config.max_creator_total_tokens, + min_creator_success_rate: config.min_creator_success_rate, + } + } + + /// 执行多层 Rug 检测 + pub async fn evaluate(&self, event: &TokenCreatedEvent) -> RugFilterResult { + let mut score = 100.0; + let mut reasons: Vec = Vec::new(); + let mut details: Vec<(String, f64)> = Vec::new(); + + // === 1. 创建者黑名单 (硬过滤) === + if self.blacklisted_creators.contains(&event.creator) { + return RugFilterResult { + passed: false, + score: 0.0, + reason: "创建者在黑名单中".to_string(), + details: vec![("blacklist".to_string(), 0.0)], + }; + } + details.push(("blacklist".to_string(), 100.0)); + + // === 2. 元数据完整性 === + let mut metadata_score = 100.0; + if event.name.is_empty() || event.name == "null" { + metadata_score -= 40.0; + reasons.push("名称为空".to_string()); + } + if event.symbol.is_empty() || event.symbol == "null" { + metadata_score -= 30.0; + reasons.push("符号为空".to_string()); + } + if event.uri.is_empty() || event.uri == "null" { + metadata_score -= 30.0; + reasons.push("无元数据URI".to_string()); + } + // 名称/符号过短可能是垃圾 + if !event.name.is_empty() && event.name.len() < 2 { + metadata_score -= 10.0; + } + score += (metadata_score - 100.0) * 0.3; // 权重 30% + details.push(("metadata".to_string(), metadata_score)); + + // === 3. 创建者历史分析 === + let creator_score = self.evaluate_creator_history(&event.creator).await; + score += (creator_score - 100.0) * 0.4; // 权重 40% + details.push(("creator_history".to_string(), creator_score)); + + if creator_score < 30.0 { + reasons.push("创建者历史可疑".to_string()); + } + + // === 4. 时间分析 === + // 如果创建时间戳异常(未来时间或过旧),可能是重放攻击 + let now = chrono::Utc::now().timestamp(); + let time_score = if event.timestamp > now + 60 { + reasons.push("时间戳异常(未来)".to_string()); + 0.0 + } else if now - event.timestamp > 3600 { + reasons.push("代币创建超过1小时".to_string()); + 50.0 + } else { + 100.0 + }; + score += (time_score - 100.0) * 0.15; // 权重 15% + details.push(("timestamp".to_string(), time_score)); + + // === 5. 名称/符号模式检测 === + let pattern_score = self.detect_suspicious_patterns(&event.name, &event.symbol); + score += (pattern_score - 100.0) * 0.15; // 权重 15% + details.push(("pattern".to_string(), pattern_score)); + + if pattern_score < 50.0 { + reasons.push("名称/符号模式可疑".to_string()); + } + + // 最终得分 (clamp 到 0-100) + score = score.clamp(0.0, 100.0); + + let passed = score >= 40.0; // 低于 40 分直接拒绝 + + RugFilterResult { + passed, + score, + reason: if passed { + "通过".to_string() + } else { + reasons.join("; ") + }, + details, + } + } + + /// 评估创建者历史 + async fn evaluate_creator_history(&self, creator: &str) -> f64 { + // TODO: 实际实现需要查询链上数据 + // 方案 1: 查询 Birdeye API / DexScreener API + // 方案 2: 自建索引数据库 + // 方案 3: 查询 Solana RPC getSignaturesForAddress + // + // 当前: 基于简单启发式 + // - 新地址 (首次发币) → 中性 60 分 + // - 已知批量发币者 → 低分 + // - 有成功毕业记录 → 高分 + + // 临时实现: 给所有创建者中性分数 + // 生产环境必须替换为真实查询 + debug!("评估创建者历史: {} (当前使用默认分数)", creator); + 60.0 + } + + /// 检测可疑名称/符号模式 + fn detect_suspicious_patterns(&self, name: &str, symbol: &str) -> f64 { + let mut score = 100.0; + + let name_lower = name.to_lowercase(); + let symbol_lower = symbol.to_lowercase(); + + // 常见 rug 模式 + let suspicious_patterns = [ + "test", "fake", "scam", "rug", "pump", "dump", + "safe", "guaranteed", "100x", "1000x", "moon", + "elon", "trump", "bitcoin", "eth", + ]; + + for pattern in &suspicious_patterns { + if name_lower.contains(pattern) || symbol_lower.contains(pattern) { + score -= 15.0; + } + } + + // 全大写可能是喊单 + if name == name.to_uppercase() && name.len() > 5 { + score -= 10.0; + } + + // 包含过多特殊字符 + let special_count = name.chars().filter(|c| !c.is_alphanumeric() && *c != ' ').count(); + if special_count > 3 { + score -= 20.0; + } + + score.clamp(0.0, 100.0) + } +} \ No newline at end of file diff --git a/src/decision/scorer.rs b/src/decision/scorer.rs new file mode 100644 index 0000000..a4f809a --- /dev/null +++ b/src/decision/scorer.rs @@ -0,0 +1,115 @@ +//! 信号评分器 +//! +//! 综合多维度信号计算最终评分 (0-100) + +use crate::config::DecisionConfig; +use crate::detection::TokenCreatedEvent; +use super::rug_filter::RugFilterResult; + +/// 信号评分器 +pub struct SignalScorer { + config: DecisionConfig, +} + +impl SignalScorer { + pub fn new(config: &DecisionConfig) -> Self { + Self { + config: config.clone(), + } + } + + /// 计算综合评分 + /// + /// 评分维度: + /// - Rug 安全分 (40%): 来自 RugFilter + /// - 元数据质量 (20%): 名称、符号、URI 完整性 + /// - 创建者信誉 (25%): 历史成功率 + /// - 市场时机 (15%): 创建时间、slot 等 + pub async fn score(&self, event: &TokenCreatedEvent, rug_result: &RugFilterResult) -> f64 { + let mut total_score = 0.0; + + // 1. Rug 安全分 (权重 40%) + let rug_weight = 0.40; + total_score += rug_result.score * rug_weight; + + // 2. 元数据质量 (权重 20%) + let metadata_weight = 0.20; + let metadata_score = self.score_metadata(event); + total_score += metadata_score * metadata_weight; + + // 3. 创建者信誉 (权重 25%) + let creator_weight = 0.25; + let creator_score = self.score_creator(event).await; + total_score += creator_score * creator_weight; + + // 4. 市场时机 (权重 15%) + let timing_weight = 0.15; + let timing_score = self.score_timing(event); + total_score += timing_score * timing_weight; + + total_score.clamp(0.0, 100.0) + } + + /// 元数据质量评分 + fn score_metadata(&self, event: &TokenCreatedEvent) -> f64 { + let mut score = 0.0; + + // 名称质量 + if !event.name.is_empty() && event.name != "null" { + score += 30.0; + // 名称长度合理 (3-30 字符) + if event.name.len() >= 3 && event.name.len() <= 30 { + score += 10.0; + } + } + + // 符号质量 + if !event.symbol.is_empty() && event.symbol != "null" { + score += 30.0; + // 符号长度合理 (2-10 字符) + if event.symbol.len() >= 2 && event.symbol.len() <= 10 { + score += 10.0; + } + } + + // URI 存在 + if !event.uri.is_empty() && event.uri != "null" { + score += 20.0; + // URI 是有效 URL + if event.uri.starts_with("http") || event.uri.starts_with("ipfs") { + score += 10.0; + } + } + + score.clamp(0.0, 100.0) + } + + /// 创建者信誉评分 + async fn score_creator(&self, event: &TokenCreatedEvent) -> f64 { + // TODO: 实际实现需要查询链上数据 + // 当前使用 RugFilter 中的创建者分数 + // 生产环境应查询: + // - 创建者历史发币数 + // - 毕业率 + // - 平均存活时间 + // - 是否有 rug pull 记录 + + // 临时: 中性分数 + 50.0 + } + + /// 市场时机评分 + fn score_timing(&self, event: &TokenCreatedEvent) -> f64 { + let now = chrono::Utc::now().timestamp(); + let age_secs = now - event.timestamp; + + // 越新越好 (刚创建的币机会最大) + match age_secs { + 0..=5 => 100.0, // 5秒内: 最佳 + 6..=30 => 90.0, // 30秒内: 很好 + 31..=120 => 70.0, // 2分钟内: 可以 + 121..=600 => 40.0, // 10分钟内: 一般 + _ => 10.0, // 超过10分钟: 太晚 + } + } +} \ No newline at end of file diff --git a/src/detection/bonding_curve_monitor.rs b/src/detection/bonding_curve_monitor.rs new file mode 100644 index 0000000..7f49c1a --- /dev/null +++ b/src/detection/bonding_curve_monitor.rs @@ -0,0 +1,103 @@ +//! Bonding Curve 状态监控器 +//! +//! 监控特定代币的 Bonding Curve 账户状态变化, +//! 当进度接近毕业阈值时触发毕业狙击。 + +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{info, warn}; + +use crate::bonding_curve::{BondingCurveState, BondingCurveMath, LAMPORTS_PER_SOL}; +use crate::config::DetectionConfig; +use crate::detection::streamer::{BondingCurveProgress, SniperEvent}; +use crate::error::SniperResult; + +/// Bonding Curve 监控器 +pub struct BondingCurveMonitor { + /// 正在监控的代币 (mint → bonding_curve PDA) + watched_tokens: HashMap, + /// 毕业触发阈值 (lamports) + graduation_trigger_lamports: u64, + /// 事件发送通道 + event_tx: mpsc::Sender, +} + +impl BondingCurveMonitor { + pub fn new(config: &DetectionConfig, event_tx: mpsc::Sender) -> Self { + let graduation_trigger_lamports = + (config.graduation_trigger_sol * LAMPORTS_PER_SOL as f64) as u64; + + Self { + watched_tokens: HashMap::new(), + graduation_trigger_lamports, + event_tx, + } + } + + /// 添加监控代币 + pub fn watch_token(&mut self, mint: String, bonding_curve: String) { + info!("👁️ 开始监控: mint={}, curve={}", mint, bonding_curve); + self.watched_tokens.insert(mint, bonding_curve); + } + + /// 移除监控代币 + pub fn unwatch_token(&mut self, mint: &str) { + self.watched_tokens.remove(mint); + } + + /// 处理 Bonding Curve 状态更新 + pub async fn on_state_update( + &self, + mint: &str, + state: &BondingCurveState, + slot: u64, + ) -> SniperResult<()> { + let progress = state.progress(); + let real_sol = state.real_sol_reserves; + + // 发送进度更新事件 + let progress_event = BondingCurveProgress { + mint: mint.to_string(), + bonding_curve: self + .watched_tokens + .get(mint) + .cloned() + .unwrap_or_default(), + sol_deposited: real_sol, + progress_pct: progress, + virtual_sol_reserves: state.virtual_sol_reserves, + virtual_token_reserves: state.virtual_token_reserves, + real_sol_reserves: state.real_sol_reserves, + real_token_reserves: state.real_token_reserves, + slot, + }; + + self.event_tx + .send(SniperEvent::BondingCurveProgress(progress_event)) + .await + .map_err(|_| crate::error::SniperError::EventChannelFull)?; + + // 检查是否接近毕业 + if real_sol >= self.graduation_trigger_lamports && !state.is_graduated() { + info!( + "🎓 毕业 imminent! mint={}, progress={:.1}%, real_sol={:.2} SOL", + mint, + progress * 100.0, + real_sol as f64 / LAMPORTS_PER_SOL as f64 + ); + } + + Ok(()) + } + + /// 获取当前监控的代币数量 + pub fn watched_count(&self) -> usize { + self.watched_tokens.len() + } + + /// 检查代币是否正在被监控 + pub fn is_watching(&self, mint: &str) -> bool { + self.watched_tokens.contains_key(mint) + } +} diff --git a/src/detection/mod.rs b/src/detection/mod.rs new file mode 100644 index 0000000..fe02f70 --- /dev/null +++ b/src/detection/mod.rs @@ -0,0 +1,12 @@ +//! 事件检测模块 +//! +//! 负责监听链上事件,包括: +//! - 新币创建事件 (PumpFun CreateToken) +//! - Bonding Curve 状态变化 +//! - Graduation 事件 (PumpSwap CreatePool) + +pub mod bonding_curve_monitor; +pub mod streamer; + +pub use bonding_curve_monitor::BondingCurveMonitor; +pub use streamer::{SniperEvent, TokenCreatedEvent, BondingCurveProgress, GraduationEvent}; diff --git a/src/detection/streamer.rs b/src/detection/streamer.rs new file mode 100644 index 0000000..e25df66 --- /dev/null +++ b/src/detection/streamer.rs @@ -0,0 +1,283 @@ +//! 事件流检测器 +//! +//! 使用 Yellowstone gRPC 订阅 Pump.fun 程序事件。 +//! 当 solana-streamer-sdk 安装后,替换 mock 实现为真实 gRPC 连接。 + +use tokio::sync::mpsc; +use tracing::{info, warn, error}; + +use crate::config::DetectionConfig; +use crate::error::SniperResult; + +// ================================================================ +// 事件类型定义 +// ================================================================ + +/// 狙击事件枚举 +#[derive(Debug, Clone)] +pub enum SniperEvent { + /// 新币创建 + TokenCreated(TokenCreatedEvent), + /// Bonding Curve 进度更新 + BondingCurveProgress(BondingCurveProgress), + /// 毕业完成 (迁移到 PumpSwap) + GraduationComplete(GraduationEvent), +} + +impl SniperEvent { + pub fn event_type(&self) -> &'static str { + match self { + Self::TokenCreated(_) => "token_created", + Self::BondingCurveProgress(_) => "curve_progress", + Self::GraduationComplete(_) => "graduation", + } + } +} + +/// 新币创建事件 +#[derive(Debug, Clone)] +pub struct TokenCreatedEvent { + /// 代币 Mint 地址 + pub mint: String, + /// Bonding Curve PDA 地址 + pub bonding_curve: String, + /// 创建者地址 + pub creator: String, + /// 代币名称 + pub name: String, + /// 代币符号 + pub symbol: String, + /// 元数据 URI + pub uri: String, + /// 创建时的 slot + pub slot: u64, + /// 时间戳 + pub timestamp: i64, +} + +/// Bonding Curve 进度事件 +#[derive(Debug, Clone)] +pub struct BondingCurveProgress { + /// 代币 Mint 地址 + pub mint: String, + /// Bonding Curve PDA 地址 + pub bonding_curve: String, + /// 已存入的 SOL (lamports) + pub sol_deposited: u64, + /// 进度百分比 (0.0 ~ 1.0) + pub progress_pct: f64, + /// 虚拟 SOL 储备 + pub virtual_sol_reserves: u64, + /// 虚拟代币储备 + pub virtual_token_reserves: u64, + /// 真实 SOL 储备 + pub real_sol_reserves: u64, + /// 真实代币储备 + pub real_token_reserves: u64, + /// 当前 slot + pub slot: u64, +} + +/// 毕业事件 +#[derive(Debug, Clone)] +pub struct GraduationEvent { + /// 代币 Mint 地址 + pub mint: String, + /// PumpSwap 池地址 + pub pool: String, + /// 毕业时的 slot + pub slot: u64, + /// 时间戳 + pub timestamp: i64, +} + +// ================================================================ +// 事件流启动器 +// ================================================================ + +/// 启动事件检测 +/// +/// 当 solana-streamer-sdk 安装后,此函数将: +/// 1. 连接 Yellowstone gRPC 端点 +/// 2. 订阅 Pump.fun 程序 (6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P) +/// 3. 解析 CreateToken / Buy / Sell 事件 +/// 4. 监控 Bonding Curve 账户状态变化 +/// 5. 将事件发送到 event_tx 通道 +pub async fn start_detection( + config: DetectionConfig, + event_tx: mpsc::Sender, +) -> SniperResult<()> { + info!("📡 启动事件检测器..."); + info!(" gRPC 端点: {}", config.yellowstone_grpc_url); + info!(" 毕业触发阈值: {} SOL", config.graduation_trigger_sol); + + // ============================================================ + // TODO: 安装 solana-streamer-sdk 后,替换为真实实现 + // ============================================================ + // + // 真实实现示例: + // + // ```rust + // use solana_streamer_sdk::prelude::*; + // + // let streamer = SolanaStreamer::builder() + // .grpc_endpoint(&config.yellowstone_grpc_url) + // .x_token(config.yellowstone_x_token.as_deref()) + // .subscribe_program(PUMP_FUN_PROGRAM_ID) + // .order_mode(OrderMode::Unordered) + // .build()?; + // + // streamer.start(|event: DexEvent| { + // match event { + // DexEvent::PumpFunCreateToken(create) => { + // let sniper_event = SniperEvent::TokenCreated(TokenCreatedEvent { + // mint: create.mint.to_string(), + // bonding_curve: create.bonding_curve.to_string(), + // creator: create.creator.to_string(), + // name: create.name.clone(), + // symbol: create.symbol.clone(), + // uri: create.uri.clone(), + // slot: create.slot, + // timestamp: create.timestamp, + // }); + // let _ = event_tx.try_send(sniper_event); + // } + // DexEvent::PumpSwapCreatePool(pool) => { + // let sniper_event = SniperEvent::GraduationComplete(GraduationEvent { + // mint: pool.mint.to_string(), + // pool: pool.pool.to_string(), + // slot: pool.slot, + // timestamp: pool.timestamp, + // }); + // let _ = event_tx.try_send(sniper_event); + // } + // _ => {} + // } + // }).await?; + // ``` + + // 临时: 使用 RPC 轮询模式 (延迟较高,仅用于开发测试) + info!("⚠️ 当前使用 RPC 轮询模式 (开发测试用)"); + info!(" 安装 solana-streamer-sdk 后切换为 gRPC 模式"); + + start_rpc_polling(config, event_tx).await +} + +/// RPC 轮询模式 (开发测试用,延迟 150-300ms) +async fn start_rpc_polling( + config: DetectionConfig, + event_tx: mpsc::Sender, +) -> SniperResult<()> { + use solana_client::nonblocking::rpc_client::RpcClient; + use solana_sdk::pubkey::Pubkey; + use std::str::FromStr; + + let rpc_url = std::env::var("SOLANA_RPC_URL") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); + let client = RpcClient::new(rpc_url); + + let pump_fun_program = Pubkey::from_str( + crate::bonding_curve::PUMP_FUN_PROGRAM_ID, + ) + .map_err(|e| crate::error::SniperError::Detection(format!("Invalid program ID: {}", e)))?; + + info!("🔍 开始轮询 Pump.fun 程序账户..."); + + let mut last_slot = 0u64; + + loop { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // 获取最新 slot + let current_slot = match client.get_slot().await { + Ok(slot) => slot, + Err(e) => { + warn!("获取 slot 失败: {}", e); + continue; + } + }; + + if current_slot <= last_slot { + continue; + } + last_slot = current_slot; + + // 获取 Pump.fun 程序的最近交易 + // 注意: 这是简化实现,生产环境应使用 gRPC + match client + .get_signatures_for_address_with_config( + &pump_fun_program, + solana_client::rpc_config::RpcSignaturesForAddressConfig { + before: None, + until: None, + limit: Some(10), + commitment: Some(solana_sdk::commitment_config::CommitmentConfig::confirmed()), + }, + ) + .await + { + Ok(signatures) => { + for sig_info in signatures { + if sig_info.slot <= last_slot - 10 { + continue; // 跳过旧交易 + } + + // 获取交易详情 + if let Ok(Some(tx)) = client + .get_transaction_with_config( + &sig_info.signature, + solana_client::rpc_config::RpcTransactionConfig { + encoding: Some( + solana_transaction_status::UiTransactionEncoding::JsonParsed, + ), + commitment: Some( + solana_sdk::commitment_config::CommitmentConfig::confirmed(), + ), + max_supported_transaction_version: Some(0), + }, + ) + .await + { + // 解析交易,检测 CreateToken 事件 + if let Some(parsed) = parse_create_token_event(&tx, sig_info.slot) { + info!( + "🆕 检测到新币: {} ({}) by {}", + parsed.name, parsed.symbol, parsed.creator + ); + if event_tx + .send(SniperEvent::TokenCreated(parsed)) + .await + .is_err() + { + error!("事件通道已关闭"); + return Ok(()); + } + } + } + } + } + Err(e) => { + warn!("获取交易签名失败: {}", e); + } + } + } +} + +/// 解析 CreateToken 事件 (简化实现) +fn parse_create_token_event( + _tx: &solana_transaction_status::EncodedConfirmedTransactionWithStatusMeta, + slot: u64, +) -> Option { + // TODO: 实现完整的交易解析逻辑 + // 需要解析 inner instructions 和 log messages 来提取: + // - mint 地址 + // - bonding curve PDA + // - creator 地址 + // - name, symbol, uri + // + // 当 solana-streamer-sdk 安装后,此函数将被替换 + + // 临时: 返回 None (不解析) + let _ = slot; + None +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b6c0dc6 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,104 @@ +//! 统一错误类型定义 +//! +//! 所有模块的错误都转换为 SniperError,便于统一处理和日志记录。 + +use thiserror::Error; + +/// 全局错误类型 +#[derive(Error, Debug)] +pub enum SniperError { + // === 配置错误 === + #[error("Config error: {0}")] + Config(String), + + // === 检测层错误 === + #[error("Detection error: {0}")] + Detection(String), + + #[error("gRPC connection failed: {0}")] + GrpcConnection(String), + + #[error("Event channel full")] + EventChannelFull, + + // === 决策层错误 === + #[error("Decision error: {0}")] + Decision(String), + + #[error("Rug filter rejected: {0}")] + RugFilterRejected(String), + + // === 执行层错误 === + #[error("Execution error: {0}")] + Execution(String), + + #[error("Transaction build failed: {0}")] + TxBuildFailed(String), + + #[error("Transaction submit failed: {0}")] + TxSubmitFailed(String), + + #[error("Transaction simulation failed: {0}")] + TxSimulationFailed(String), + + #[error("Transaction confirmation timeout: {signature}")] + TxConfirmTimeout { signature: String }, + + #[error("Jito bundle failed: {0}")] + JitoBundleFailed(String), + + // === 仓位管理错误 === + #[error("Position error: {0}")] + Position(String), + + #[error("Position not found: {mint}")] + PositionNotFound { mint: String }, + + // === 风控错误 === + #[error("Circuit breaker tripped: {0}")] + CircuitBreakerTripped(String), + + #[error("Risk limit exceeded: {0}")] + RiskLimitExceeded(String), + + // === Bonding Curve 错误 === + #[error("Bonding curve error: {0}")] + BondingCurve(String), + + #[error("Invalid account data: expected {expected} bytes, got {actual}")] + InvalidAccountData { expected: usize, actual: usize }, + + #[error("Invalid discriminator: expected {expected:#x}, got {actual:#x}")] + InvalidDiscriminator { expected: u64, actual: u64 }, + + #[error("Invariant violation: k deviation {deviation_pct:.6}%")] + InvariantViolation { deviation_pct: f64 }, + + // === 通用错误 === + #[error("RPC error: {0}")] + Rpc(String), + + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Internal error: {0}")] + Internal(String), +} + +/// 便捷 Result 类型 +pub type SniperResult = Result; + +impl From for SniperError { + fn from(e: serde_json::Error) -> Self { + SniperError::Serialization(e.to_string()) + } +} + +impl From for SniperError { + fn from(e: toml::de::Error) -> Self { + SniperError::Config(e.to_string()) + } +} diff --git a/src/execution/jito.rs b/src/execution/jito.rs new file mode 100644 index 0000000..13b96a5 --- /dev/null +++ b/src/execution/jito.rs @@ -0,0 +1,165 @@ +//! Jito Block Engine 客户端 +//! +//! 通过 HTTP JSON-RPC 与 Jito Block Engine 交互 +//! API 文档: https://jito-labs.gitbook.io/mev/searcher-resources/json-rpc-api-reference + +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use solana_sdk::transaction::Transaction; +use tracing::{debug, warn}; + +use crate::error::{SniperError, SniperResult}; + +/// Jito 客户端 +pub struct JitoClient { + http_client: Client, + block_engine_url: String, + tip_lamports: u64, +} + +/// JSON-RPC 请求 +#[derive(Serialize)] +struct JsonRpcRequest { + jsonrpc: String, + id: u64, + method: String, + params: serde_json::Value, +} + +/// JSON-RPC 响应 +#[derive(Deserialize)] +struct JsonRpcResponse { + result: Option, + error: Option, +} + +#[derive(Deserialize)] +struct JsonRpcError { + code: i64, + message: String, +} + +impl JitoClient { + pub fn new(block_engine_url: &str, tip_lamports: u64) -> Self { + Self { + http_client: Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .expect("Failed to create HTTP client"), + block_engine_url: block_engine_url.trim_end_matches('/').to_string(), + tip_lamports, + } + } + + /// 发送交易到 Jito + pub async fn send_transaction(&self, tx: &Transaction) -> SniperResult { + let tx_bytes = bincode::serialize(tx) + .map_err(|e| SniperError::JitoBundleFailed(format!("Serialize tx: {}", e)))?; + let tx_base64 = base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + &tx_bytes, + ); + + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: 1, + method: "sendTransaction".to_string(), + params: serde_json::json!([tx_base64, { + "encoding": "base64" + }]), + }; + + let response = self.rpc_call(request).await?; + + if let Some(error) = response.error { + return Err(SniperError::JitoBundleFailed(format!( + "Jito error {}: {}", + error.code, error.message + ))); + } + + response + .result + .and_then(|r| r.as_str().map(|s| s.to_string())) + .ok_or_else(|| SniperError::JitoBundleFailed("Empty response".to_string())) + } + + /// 发送 Bundle (多笔交易打包) + pub async fn send_bundle(&self, transactions: &[Transaction]) -> SniperResult { + let encoded: Vec = transactions + .iter() + .map(|tx| { + let bytes = bincode::serialize(tx).unwrap_or_default(); + base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes) + }) + .collect(); + + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: 1, + method: "sendBundle".to_string(), + params: serde_json::json!([encoded]), + }; + + let response = self.rpc_call(request).await?; + + if let Some(error) = response.error { + return Err(SniperError::JitoBundleFailed(format!( + "Bundle error {}: {}", + error.code, error.message + ))); + } + + response + .result + .and_then(|r| r.as_str().map(|s| s.to_string())) + .ok_or_else(|| SniperError::JitoBundleFailed("Empty bundle response".to_string())) + } + + /// 获取 Tip 账户 + pub async fn get_tip_accounts(&self) -> SniperResult> { + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: 1, + method: "getTipAccounts".to_string(), + params: serde_json::json!([]), + }; + + let response = self.rpc_call(request).await?; + + response + .result + .and_then(|r| serde_json::from_value(r).ok()) + .ok_or_else(|| SniperError::JitoBundleFailed("Failed to get tip accounts".to_string())) + } + + /// 执行 JSON-RPC 调用 + async fn rpc_call(&self, request: JsonRpcRequest) -> SniperResult { + let url = format!("{}/api/v1", self.block_engine_url); + + debug!("Jito RPC: {} → {}", request.method, url); + + let response = self + .http_client + .post(&url) + .json(&request) + .send() + .await + .map_err(|e| SniperError::JitoBundleFailed(format!("HTTP error: {}", e)))?; + + if !response.status().is_success() { + return Err(SniperError::JitoBundleFailed(format!( + "HTTP {}: {}", + response.status(), + response.text().await.unwrap_or_default() + ))); + } + + let rpc_response: JsonRpcResponse = response + .json() + .await + .map_err(|e| SniperError::JitoBundleFailed(format!("Parse response: {}", e)))?; + + Ok(rpc_response) + } +} \ No newline at end of file diff --git a/src/execution/mod.rs b/src/execution/mod.rs new file mode 100644 index 0000000..835552a --- /dev/null +++ b/src/execution/mod.rs @@ -0,0 +1,125 @@ +//! 交易执行模块 +//! +//! 负责: +//! 1. 交易构建 (PumpFun Buy/Sell) +//! 2. Priority Fee 动态计算 +//! 3. 交易模拟预检 +//! 4. 多中继提交 (RPC + Jito) +//! 5. 交易确认 + +pub mod jito; +pub mod submitter; +pub mod tx_builder; + +pub use jito::JitoClient; +pub use submitter::SubmitResult; +pub use tx_builder::TxBuilder; + +use crate::config::ExecutionConfig; +use crate::error::{SniperError, SniperResult}; +use tracing::{info, warn}; + +/// 交易执行引擎 +pub struct ExecutionEngine { + config: ExecutionConfig, + tx_builder: TxBuilder, + jito_client: JitoClient, + dry_run: bool, +} + +impl ExecutionEngine { + pub async fn new(config: ExecutionConfig, dry_run: bool) -> SniperResult { + let tx_builder = TxBuilder::new(&config).await?; + let jito_client = JitoClient::new( + &config.jito_block_engine_url, + config.jito_tip_lamports, + ); + + if dry_run { + info!("⚠️ DRY RUN 模式: 交易不会实际发送"); + } + + Ok(Self { + config, + tx_builder, + jito_client, + dry_run, + }) + } + + /// 买入 PumpFun 代币 (Bonding Curve 阶段) + pub async fn buy_pumpfun_token( + &self, + mint: &str, + bonding_curve: &str, + sol_amount: f64, + slippage_bps: u64, + ) -> SniperResult { + let lamports = (sol_amount * 1_000_000_000.0) as u64; + + // 1. 构建交易 + let tx = self.tx_builder.build_pumpfun_buy( + mint, + bonding_curve, + lamports, + slippage_bps, + ).await?; + + // 2. Dry run 模式 + if self.dry_run { + let fake_sig = format!("DRY_RUN_{}", &hex::encode(rand::random::<[u8; 16]>())); + info!("🏜️ [DRY RUN] 买入 {} SOL → mint={}", sol_amount, mint); + return Ok(fake_sig); + } + + // 3. 模拟预检 + if self.config.enable_simulation { + self.tx_builder.simulate_transaction(&tx).await?; + } + + // 4. 提交 + let result = submitter::submit_transaction( + &tx, + &self.tx_builder, + &self.jito_client, + &self.config, + ).await?; + + Ok(result.signature) + } + + /// 卖出 PumpFun 代币 + pub async fn sell_pumpfun_token( + &self, + mint: &str, + bonding_curve: &str, + token_amount: u64, + slippage_bps: u64, + ) -> SniperResult { + let tx = self.tx_builder.build_pumpfun_sell( + mint, + bonding_curve, + token_amount, + slippage_bps, + ).await?; + + if self.dry_run { + let fake_sig = format!("DRY_RUN_SELL_{}", &hex::encode(rand::random::<[u8; 16]>())); + info!("🏜️ [DRY RUN] 卖出 {} tokens → mint={}", token_amount, mint); + return Ok(fake_sig); + } + + if self.config.enable_simulation { + self.tx_builder.simulate_transaction(&tx).await?; + } + + let result = submitter::submit_transaction( + &tx, + &self.tx_builder, + &self.jito_client, + &self.config, + ).await?; + + Ok(result.signature) + } +} \ No newline at end of file diff --git a/src/execution/submitter.rs b/src/execution/submitter.rs new file mode 100644 index 0000000..8910416 --- /dev/null +++ b/src/execution/submitter.rs @@ -0,0 +1,107 @@ +//! 多中继交易提交器 +//! +//! 并行提交到多个中继: +//! 1. 直接 RPC (sendTransaction) +//! 2. Jito Block Engine (sendBundle) +//! 任一路径成功即视为成功 + +use solana_sdk::transaction::Transaction; +use tracing::{info, warn}; + +use super::jito::JitoClient; +use super::tx_builder::TxBuilder; +use crate::config::ExecutionConfig; +use crate::error::{SniperError, SniperResult}; + +/// 提交结果 +#[derive(Debug, Clone)] +pub struct SubmitResult { + /// 交易签名 + pub signature: String, + /// 提交路径 + pub relay: String, + /// 提交耗时 (ms) + pub latency_ms: u64, +} + +/// 多中继提交交易 +pub async fn submit_transaction( + tx: &Transaction, + tx_builder: &TxBuilder, + jito_client: &JitoClient, + config: &ExecutionConfig, +) -> SniperResult { + let start = std::time::Instant::now(); + + if config.enable_multi_relay { + // 并行提交到 RPC + Jito + let (rpc_result, jito_result) = tokio::join!( + submit_via_rpc(tx, tx_builder), + submit_via_jito(tx, jito_client), + ); + + let latency_ms = start.elapsed().as_millis() as u64; + + // 任一路径成功 + match rpc_result { + Ok(sig) => { + return Ok(SubmitResult { + signature: sig, + relay: "rpc".to_string(), + latency_ms, + }); + } + Err(rpc_err) => { + match jito_result { + Ok(sig) => { + return Ok(SubmitResult { + signature: sig, + relay: "jito".to_string(), + latency_ms, + }); + } + Err(jito_err) => { + return Err(SniperError::TxSubmitFailed(format!( + "All relays failed. RPC: {} | Jito: {}", + rpc_err, jito_err + ))); + } + } + } + } + } else { + // 仅 RPC + let sig = submit_via_rpc(tx, tx_builder).await?; + let latency_ms = start.elapsed().as_millis() as u64; + Ok(SubmitResult { + signature: sig, + relay: "rpc".to_string(), + latency_ms, + }) + } +} + +/// 通过 RPC 提交 +async fn submit_via_rpc( + tx: &Transaction, + tx_builder: &TxBuilder, +) -> SniperResult { + let sig = tx_builder + .rpc_client() + .send_transaction(tx) + .await + .map_err(|e| SniperError::TxSubmitFailed(format!("RPC send: {}", e)))?; + + info!("📤 RPC 提交: {}", sig); + Ok(sig.to_string()) +} + +/// 通过 Jito 提交 +async fn submit_via_jito( + tx: &Transaction, + jito_client: &JitoClient, +) -> SniperResult { + let sig = jito_client.send_transaction(tx).await?; + info!("📤 Jito 提交: {}", sig); + Ok(sig) +} \ No newline at end of file diff --git a/src/execution/tx_builder.rs b/src/execution/tx_builder.rs new file mode 100644 index 0000000..444041f --- /dev/null +++ b/src/execution/tx_builder.rs @@ -0,0 +1,380 @@ +//! 交易构建器 +//! +//! 构建 PumpFun Buy/Sell 交易指令 +//! 当 sol-trade-sdk 安装后,替换为 SDK 调用 + +use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + system_program, + transaction::Transaction, +}; +use std::str::FromStr; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use tracing::{info, warn}; + +use crate::bonding_curve::{BondingCurveMath, LAMPORTS_PER_SOL, PUMP_FUN_PROGRAM_ID}; +use crate::config::ExecutionConfig; +use crate::error::{SniperError, SniperResult}; + +/// 交易构建器 +pub struct TxBuilder { + rpc_client: RpcClient, + keypairs: Vec>, + current_idx: AtomicUsize, + program_id: Pubkey, + compute_unit_limit: u32, + default_priority_fee: u64, +} + +impl TxBuilder { + pub async fn new(config: &ExecutionConfig) -> SniperResult { + let rpc_client = RpcClient::new(config.rpc_url.clone()); + + // 加载钱包 + let keypairs: Vec> = if config.wallet_paths.is_empty() { + // 开发模式: 生成临时钱包 + warn!("⚠️ 未配置钱包,生成临时开发钱包"); + vec![Arc::new(Keypair::new())] + } else { + config + .wallet_paths + .iter() + .map(|path| { + let keypair = Self::load_keypair(path) + .unwrap_or_else(|e| { + warn!("加载钱包失败 {}: {}, 使用临时钱包", path, e); + Keypair::new() + }); + Arc::new(keypair) + }) + .collect() + }; + + info!("🔑 已加载 {} 个钱包", keypairs.len()); + for (i, kp) in keypairs.iter().enumerate() { + info!(" 钱包 {}: {}", i, kp.pubkey()); + } + + let program_id = Pubkey::from_str(PUMP_FUN_PROGRAM_ID) + .map_err(|e| SniperError::Execution(format!("Invalid program ID: {}", e)))?; + + Ok(Self { + rpc_client, + keypairs, + current_idx: AtomicUsize::new(0), + program_id, + compute_unit_limit: config.compute_unit_limit, + default_priority_fee: config.default_priority_fee, + }) + } + + /// 获取当前钱包 (轮换) + pub fn current_wallet(&self) -> Arc { + let idx = self.current_idx.fetch_add(1, Ordering::Relaxed); + Arc::clone(&self.keypairs[idx % self.keypairs.len()]) + } + + /// 获取 RPC 客户端引用 + pub fn rpc_client(&self) -> &RpcClient { + &self.rpc_client + } + + /// 构建 PumpFun 买入交易 + pub async fn build_pumpfun_buy( + &self, + mint: &str, + bonding_curve: &str, + sol_amount: u64, + slippage_bps: u64, + ) -> SniperResult { + let wallet = self.current_wallet(); + let mint_pubkey = Pubkey::from_str(mint) + .map_err(|e| SniperError::TxBuildFailed(format!("Invalid mint: {}", e)))?; + let bonding_curve_pubkey = Pubkey::from_str(bonding_curve) + .map_err(|e| SniperError::TxBuildFailed(format!("Invalid bonding_curve: {}", e)))?; + + // 计算最小输出 (滑点保护) + let buy_result = BondingCurveMath::calculate_buy( + sol_amount, + crate::bonding_curve::INITIAL_VIRTUAL_SOL, + crate::bonding_curve::INITIAL_VIRTUAL_TOKENS, + crate::bonding_curve::INITIAL_REAL_TOKENS, + ); + let min_tokens_out = BondingCurveMath::min_tokens_out(buy_result.tokens_out, slippage_bps); + + // 构建指令 + let instructions = self.build_buy_instructions( + &wallet.pubkey(), + &mint_pubkey, + &bonding_curve_pubkey, + sol_amount, + min_tokens_out, + )?; + + // 获取 blockhash + let recent_blockhash = self.rpc_client + .get_latest_blockhash() + .await + .map_err(|e| SniperError::Rpc(format!("Get blockhash failed: {}", e)))?; + + // 组装交易 + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&wallet.pubkey()), + &[&*wallet], + recent_blockhash, + ); + + Ok(tx) + } + + /// 构建 PumpFun 卖出交易 + pub async fn build_pumpfun_sell( + &self, + mint: &str, + bonding_curve: &str, + token_amount: u64, + slippage_bps: u64, + ) -> SniperResult { + let wallet = self.current_wallet(); + let mint_pubkey = Pubkey::from_str(mint) + .map_err(|e| SniperError::TxBuildFailed(format!("Invalid mint: {}", e)))?; + let bonding_curve_pubkey = Pubkey::from_str(bonding_curve) + .map_err(|e| SniperError::TxBuildFailed(format!("Invalid bonding_curve: {}", e)))?; + + let instructions = self.build_sell_instructions( + &wallet.pubkey(), + &mint_pubkey, + &bonding_curve_pubkey, + token_amount, + slippage_bps, + )?; + + let recent_blockhash = self.rpc_client + .get_latest_blockhash() + .await + .map_err(|e| SniperError::Rpc(format!("Get blockhash failed: {}", e)))?; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&wallet.pubkey()), + &[&*wallet], + recent_blockhash, + ); + + Ok(tx) + } + + /// 构建买入指令列表 + fn build_buy_instructions( + &self, + user: &Pubkey, + mint: &Pubkey, + bonding_curve: &Pubkey, + sol_amount: u64, + min_tokens_out: u64, + ) -> SniperResult> { + let mut instructions = Vec::with_capacity(4); + + // 1. Compute Budget + instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( + self.compute_unit_limit, + )); + instructions.push(ComputeBudgetInstruction::set_compute_unit_price( + self.default_priority_fee, + )); + + // 2. 创建 ATA (如果不存在) + let ata = spl_associated_token_account_address(user, mint); + instructions.push(Instruction { + program_id: spl_associated_token_account_program_id(), + accounts: vec![ + AccountMeta::new(*user, true), + AccountMeta::new(ata, false), + AccountMeta::new_readonly(*user, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token_program_id(), false), + ], + data: vec![0], // Create instruction discriminator + }); + + // 3. PumpFun Buy 指令 + // 注意: 这是简化的指令构建 + // 生产环境应使用 sol-trade-sdk 的 pumpfun::build_buy() + let buy_ix = self.build_pumpfun_buy_ix( + user, mint, bonding_curve, sol_amount, min_tokens_out, + )?; + instructions.push(buy_ix); + + Ok(instructions) + } + + /// 构建卖出指令列表 + fn build_sell_instructions( + &self, + user: &Pubkey, + mint: &Pubkey, + bonding_curve: &Pubkey, + token_amount: u64, + _slippage_bps: u64, + ) -> SniperResult> { + let mut instructions = Vec::with_capacity(3); + + instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( + self.compute_unit_limit, + )); + instructions.push(ComputeBudgetInstruction::set_compute_unit_price( + self.default_priority_fee, + )); + + let sell_ix = self.build_pumpfun_sell_ix( + user, mint, bonding_curve, token_amount, + )?; + instructions.push(sell_ix); + + Ok(instructions) + } + + /// 构建 PumpFun Buy 指令 (底层) + /// + /// TODO: 安装 sol-trade-sdk 后替换为: + /// ```rust + /// use sol_trade_sdk::protocols::pumpfun::{self, PumpFunBuyParams}; + /// let params = PumpFunBuyParams { ... }; + /// pumpfun::build_buy(¶ms) + /// ``` + fn build_pumpfun_buy_ix( + &self, + user: &Pubkey, + mint: &Pubkey, + bonding_curve: &Pubkey, + sol_amount: u64, + min_tokens_out: u64, + ) -> SniperResult { + // PumpFun Buy 指令 discriminator + // anchor: sha256("global:buy")[0..8] + let discriminator: [u8; 8] = [0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea]; + + // 序列化参数: amount (u64) + min_tokens_out (u64) + let mut data = Vec::with_capacity(24); + data.extend_from_slice(&discriminator); + data.extend_from_slice(&sol_amount.to_le_bytes()); + data.extend_from_slice(&min_tokens_out.to_le_bytes()); + + // 派生 PDA + let (authority, _) = Pubkey::find_program_address( + &[b"global"], + &self.program_id, + ); + + let accounts = vec![ + AccountMeta::new(*bonding_curve, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new(*user, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(authority, false), + ]; + + Ok(Instruction { + program_id: self.program_id, + accounts, + data, + }) + } + + /// 构建 PumpFun Sell 指令 (底层) + fn build_pumpfun_sell_ix( + &self, + user: &Pubkey, + mint: &Pubkey, + bonding_curve: &Pubkey, + token_amount: u64, + ) -> SniperResult { + // anchor: sha256("global:sell")[0..8] + let discriminator: [u8; 8] = [0x33, 0xe6, 0x85, 0xa4, 0x01, 0x7f, 0x83, 0xad]; + + let mut data = Vec::with_capacity(16); + data.extend_from_slice(&discriminator); + data.extend_from_slice(&token_amount.to_le_bytes()); + + let (authority, _) = Pubkey::find_program_address( + &[b"global"], + &self.program_id, + ); + + let accounts = vec![ + AccountMeta::new(*bonding_curve, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new(*user, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(authority, false), + ]; + + Ok(Instruction { + program_id: self.program_id, + accounts, + data, + }) + } + + /// 模拟交易 (预检) + pub async fn simulate_transaction(&self, tx: &Transaction) -> SniperResult<()> { + let result = self.rpc_client + .simulate_transaction(tx) + .await + .map_err(|e| SniperError::TxSimulationFailed(format!("RPC error: {}", e)))?; + + if let Some(err) = result.value.err { + return Err(SniperError::TxSimulationFailed(format!( + "Simulation failed: {:?} | logs: {:?}", + err, result.value.logs + ))); + } + + Ok(()) + } + + /// 从文件加载 Keypair + fn load_keypair(path: &str) -> SniperResult { + let content = std::fs::read_to_string(path) + .map_err(|e| SniperError::Execution(format!("Read wallet file '{}': {}", path, e)))?; + + // 支持 JSON 数组格式 [1,2,3,...] (solana-keygen 格式) + let bytes: Vec = serde_json::from_str(&content) + .map_err(|e| SniperError::Execution(format!("Parse wallet '{}': {}", path, e)))?; + + Keypair::from_bytes(&bytes) + .map_err(|e| SniperError::Execution(format!("Invalid keypair '{}': {}", path, e))) + } +} + +// ================================================================ +// SPL Token 辅助函数 (避免额外依赖) +// ================================================================ + +fn spl_token_program_id() -> Pubkey { + Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap() +} + +fn spl_associated_token_account_program_id() -> Pubkey { + Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap() +} + +fn spl_associated_token_account_address(wallet: &Pubkey, mint: &Pubkey) -> Pubkey { + let (ata, _) = Pubkey::find_program_address( + &[ + wallet.as_ref(), + spl_token_program_id().as_ref(), + mint.as_ref(), + ], + &spl_associated_token_account_program_id(), + ); + ata +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..eccbc6c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,264 @@ +//! Pump.fun Sniper Bot - 主入口 +//! +//! 架构: +//! Layer 1: 事件检测 (detection) → Yellowstone gRPC / RPC 轮询 +//! Layer 2: 决策引擎 (decision) → Rug 过滤 + 评分 + 仓位计算 +//! Layer 3: 交易执行 (execution) → 交易构建 + 多中继提交 +//! Layer 4: 仓位管理 (position) → 止盈止损 + 退出策略 +//! Layer 5: 风控 (risk) → 熔断器 + 日亏损限制 +//! Layer 6: 监控 (monitoring) → Prometheus + 日志 + +mod bonding_curve; +mod config; +mod decision; +mod detection; +mod error; +mod execution; +mod monitoring; +mod position; +mod risk; + +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{info, warn, error}; + +use config::BotConfig; +use detection::SniperEvent; +use decision::DecisionEngine; +use execution::ExecutionEngine; +use position::PositionManager; +use risk::CircuitBreaker; +use monitoring::Metrics; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // ================================================================ + // 1. 加载配置 + // ================================================================ + let config_path = std::env::args() + .nth(1) + .unwrap_or_else(|| "config/mainnet.toml".to_string()); + + let config = BotConfig::load(&config_path)?; + config.validate()?; + + // ================================================================ + // 2. 初始化日志 + // ================================================================ + init_logging(&config.bot.log_level, config.monitoring.json_logs); + + info!("╔══════════════════════════════════════════════════╗"); + info!("║ 🎯 Pump.fun Sniper Bot v0.1.0 ║"); + info!("╚══════════════════════════════════════════════════╝"); + info!(" 模式: {}", config.bot.mode); + info!(" Dry Run: {}", config.bot.dry_run); + info!(" 最大仓位: {} SOL", config.position.max_position_sol); + info!(" 最大并发: {}", config.position.max_concurrent_positions); + info!(" 日亏损限制: {} SOL", config.risk.daily_loss_limit_sol); + info!(" RPC: {}", config.execution.rpc_url); + info!(" gRPC: {}", config.detection.yellowstone_grpc_url); + + // ================================================================ + // 3. 初始化各模块 + // ================================================================ + let circuit_breaker = Arc::new(CircuitBreaker::new( + config.risk.max_consecutive_failures, + config.risk.daily_loss_limit_sol, + config.risk.circuit_breaker_cooldown_minutes, + )); + + let metrics = Arc::new(Metrics::new()); + let decision_engine = Arc::new(DecisionEngine::new(config.decision.clone())); + let execution_engine = Arc::new( + ExecutionEngine::new(config.execution.clone(), config.bot.dry_run).await?, + ); + let position_manager = Arc::new(PositionManager::new(config.position.clone())); + + // ================================================================ + // 4. 启动 Prometheus 指标服务 + // ================================================================ + let metrics_clone = metrics.clone(); + let prometheus_port = config.monitoring.prometheus_port; + tokio::spawn(async move { + if let Err(e) = monitoring::serve_metrics(prometheus_port, metrics_clone).await { + error!("Prometheus 指标服务启动失败: {}", e); + } + }); + + // ================================================================ + // 5. 启动事件检测 + // ================================================================ + let (event_tx, mut event_rx) = mpsc::channel::(config.detection.event_channel_capacity); + + let detection_config = config.detection.clone(); + let detection_handle = tokio::spawn(async move { + if let Err(e) = detection::start_detection(detection_config, event_tx).await { + error!("事件检测器异常退出: {}", e); + } + }); + + // ================================================================ + // 6. 主事件循环 + // ================================================================ + info!("📡 进入主事件循环..."); + + let mut total_trades = 0u32; + let mut successful_trades = 0u32; + + while let Some(event) = event_rx.recv().await { + // 6.1 熔断检查 + if circuit_breaker.is_tripped() { + warn!("⚡ 熔断器触发中,跳过事件: {}", event.event_type()); + continue; + } + + // 6.2 日交易次数限制 + if total_trades >= config.risk.max_daily_trades { + warn!("📊 日交易次数已达上限: {}", config.risk.max_daily_trades); + continue; + } + + match event { + // === 新币创建狙击 === + SniperEvent::TokenCreated(create_event) => { + metrics.increment_events_detected("token_created"); + + info!( + "🆕 新币: {} ({}) | creator={} | slot={}", + create_event.name, create_event.symbol, create_event.creator, create_event.slot + ); + + // 决策 + let decision = decision_engine.evaluate_new_token(&create_event).await; + + if decision.should_buy { + let position_size = decision.position_size_sol; + info!( + "✅ 决策: BUY {:.4} SOL | 评分: {:.1}/100 | 原因: {}", + position_size, decision.score, decision.reason + ); + + // 检查并发仓位限制 + if position_manager.active_count() >= config.position.max_concurrent_positions { + warn!("⚠️ 并发仓位已满,跳过"); + continue; + } + + // 执行买入 + total_trades += 1; + let result = execution_engine + .buy_pumpfun_token( + &create_event.mint, + &create_event.bonding_curve, + position_size, + config.decision.slippage_bps, + ) + .await; + + match result { + Ok(tx_sig) => { + successful_trades += 1; + info!("🎯 买入成功: {}", tx_sig); + position_manager.register_position( + &create_event.mint, + &create_event.bonding_curve, + position_size, + decision.expected_tokens, + tx_sig.clone(), + ); + circuit_breaker.record_success(); + metrics.increment_trades_success(); + } + Err(e) => { + error!("❌ 买入失败: {}", e); + circuit_breaker.record_failure(); + metrics.increment_trades_failed(); + } + } + } else { + info!( + "⏭️ 跳过: 评分 {:.1} < 阈值 {:.1} | {}", + decision.score, config.decision.min_score_threshold, decision.reason + ); + metrics.increment_events_skipped(); + } + } + + // === Bonding Curve 进度更新 === + SniperEvent::BondingCurveProgress(progress) => { + // 毕业狙击模式 + if config.bot.mode == "graduation" || config.bot.mode == "both" { + let trigger_sol = + (config.detection.graduation_trigger_sol * 1_000_000_000.0) as u64; + + if progress.sol_deposited >= trigger_sol { + info!( + "🎓 Graduation imminent: {} | {:.1}% | {:.2} SOL", + progress.mint, + progress.progress_pct * 100.0, + progress.sol_deposited as f64 / 1_000_000_000.0 + ); + + // TODO: 毕业狙击逻辑 + } + } + + // 更新持仓状态 + position_manager + .update_price(&progress.mint, progress.virtual_sol_reserves, progress.virtual_token_reserves) + .await; + } + + // === 毕业完成 === + SniperEvent::GraduationComplete(grad_event) => { + info!( + "🎓 毕业完成: mint={} | pool={} | slot={}", + grad_event.mint, grad_event.pool, grad_event.slot + ); + + // 通知仓位管理器 + position_manager.on_graduation(&grad_event.mint, &grad_event.pool).await; + } + } + + // 定期输出统计 + if total_trades > 0 && total_trades % 10 == 0 { + let win_rate = successful_trades as f64 / total_trades as f64 * 100.0; + info!( + "📊 统计: 总交易={} | 成功={} | 胜率={:.1}% | 活跃仓位={}", + total_trades, + successful_trades, + win_rate, + position_manager.active_count() + ); + } + } + + // 等待检测任务结束 + detection_handle.await?; + + Ok(()) +} + +/// 初始化日志系统 +fn init_logging(level: &str, json_format: bool) { + use tracing_subscriber::{fmt, EnvFilter}; + + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(level)); + + if json_format { + fmt() + .with_env_filter(filter) + .json() + .with_target(true) + .with_thread_ids(true) + .init(); + } else { + fmt() + .with_env_filter(filter) + .with_target(true) + .with_thread_ids(false) + .init(); + } +} \ No newline at end of file diff --git a/src/monitoring/metrics.rs b/src/monitoring/metrics.rs new file mode 100644 index 0000000..253d559 --- /dev/null +++ b/src/monitoring/metrics.rs @@ -0,0 +1,120 @@ +//! Prometheus 指标收集 + +use std::sync::atomic::{AtomicU64, Ordering}; + +/// 指标收集器 +pub struct Metrics { + /// 检测到的事件总数 + events_detected: AtomicU64, + /// 跳过的事件数 + events_skipped: AtomicU64, + /// 成功交易数 + trades_success: AtomicU64, + /// 失败交易数 + trades_failed: AtomicU64, + /// 总买入 SOL (lamports) + total_buy_lamports: AtomicU64, + /// 总卖出 SOL (lamports) + total_sell_lamports: AtomicU64, + /// 启动时间戳 + start_time: u64, +} + +impl Metrics { + pub fn new() -> Self { + Self { + events_detected: AtomicU64::new(0), + events_skipped: AtomicU64::new(0), + trades_success: AtomicU64::new(0), + trades_failed: AtomicU64::new(0), + total_buy_lamports: AtomicU64::new(0), + total_sell_lamports: AtomicU64::new(0), + start_time: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + } + } + + pub fn increment_events_detected(&self, _event_type: &str) { + self.events_detected.fetch_add(1, Ordering::Relaxed); + } + + pub fn increment_events_skipped(&self) { + self.events_skipped.fetch_add(1, Ordering::Relaxed); + } + + pub fn increment_trades_success(&self) { + self.trades_success.fetch_add(1, Ordering::Relaxed); + } + + pub fn increment_trades_failed(&self) { + self.trades_failed.fetch_add(1, Ordering::Relaxed); + } + + pub fn add_buy_volume(&self, lamports: u64) { + self.total_buy_lamports.fetch_add(lamports, Ordering::Relaxed); + } + + pub fn add_sell_volume(&self, lamports: u64) { + self.total_sell_lamports.fetch_add(lamports, Ordering::Relaxed); + } + + /// 渲染 Prometheus 格式指标 + pub fn render_prometheus(&self) -> String { + let uptime = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + - self.start_time; + + let total_trades = + self.trades_success.load(Ordering::Relaxed) + self.trades_failed.load(Ordering::Relaxed); + let win_rate = if total_trades > 0 { + self.trades_success.load(Ordering::Relaxed) as f64 / total_trades as f64 * 100.0 + } else { + 0.0 + }; + + format!( + "# HELP sniper_uptime_seconds Bot uptime in seconds\n\ + # TYPE sniper_uptime_seconds gauge\n\ + sniper_uptime_seconds {}\n\ + # HELP sniper_events_detected_total Total events detected\n\ + # TYPE sniper_events_detected_total counter\n\ + sniper_events_detected_total {}\n\ + # HELP sniper_events_skipped_total Total events skipped\n\ + # TYPE sniper_events_skipped_total counter\n\ + sniper_events_skipped_total {}\n\ + # HELP sniper_trades_success_total Total successful trades\n\ + # TYPE sniper_trades_success_total counter\n\ + sniper_trades_success_total {}\n\ + # HELP sniper_trades_failed_total Total failed trades\n\ + # TYPE sniper_trades_failed_total counter\n\ + sniper_trades_failed_total {}\n\ + # HELP sniper_win_rate_percent Trade win rate\n\ + # TYPE sniper_win_rate_percent gauge\n\ + sniper_win_rate_percent {:.2}\n\ + # HELP sniper_buy_volume_lamports Total buy volume in lamports\n\ + # TYPE sniper_buy_volume_lamports counter\n\ + sniper_buy_volume_lamports {}\n\ + # HELP sniper_sell_volume_lamports Total sell volume in lamports\n\ + # TYPE sniper_sell_volume_lamports counter\n\ + sniper_sell_volume_lamports {}\n", + uptime, + self.events_detected.load(Ordering::Relaxed), + self.events_skipped.load(Ordering::Relaxed), + self.trades_success.load(Ordering::Relaxed), + self.trades_failed.load(Ordering::Relaxed), + win_rate, + self.total_buy_lamports.load(Ordering::Relaxed), + self.total_sell_lamports.load(Ordering::Relaxed), + ) + } +} + +impl Default for Metrics { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/src/monitoring/mod.rs b/src/monitoring/mod.rs new file mode 100644 index 0000000..88e8269 --- /dev/null +++ b/src/monitoring/mod.rs @@ -0,0 +1,39 @@ +//! 监控模块 + +pub mod metrics; + +pub use metrics::Metrics; + +use std::sync::Arc; +use tracing::info; + +/// 启动 Prometheus 指标 HTTP 服务 +pub async fn serve_metrics(port: u16, metrics: Arc) -> anyhow::Result<()> { + use std::convert::Infallible; + use std::net::SocketAddr; + + // 使用简单的 TCP 监听 (避免引入 hyper 依赖) + // 生产环境建议使用 prometheus::TextEncoder + hyper + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + info!("📊 Prometheus 指标服务: http://0.0.0.0:{}/metrics", port); + + let listener = tokio::net::TcpListener::bind(addr).await?; + + loop { + let (mut stream, _) = listener.accept().await?; + let metrics = metrics.clone(); + + tokio::spawn(async move { + use tokio::io::AsyncWriteExt; + + let body = metrics.render_prometheus(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: {}\r\n\r\n{}", + body.len(), + body + ); + + let _ = stream.write_all(response.as_bytes()).await; + }); + } +} \ No newline at end of file diff --git a/src/position/exit_strategy.rs b/src/position/exit_strategy.rs new file mode 100644 index 0000000..fc65d76 --- /dev/null +++ b/src/position/exit_strategy.rs @@ -0,0 +1,208 @@ +//! 退出策略 +//! +//! 评估仓位是否应该退出: +//! - 止盈 (分批) +//! - 止损 +//! - 时间止损 + +use crate::config::PositionConfig; +use super::manager::Position; + +/// 退出信号 +#[derive(Debug, Clone)] +pub struct ExitSignal { + /// 退出原因 + pub reason: String, + /// 卖出比例 (0.0 ~ 1.0) + pub sell_pct: f64, + /// 紧急程度 (true = 立即全部卖出) + pub urgent: bool, +} + +/// 退出策略 +pub struct ExitStrategy { + config: PositionConfig, +} + +impl ExitStrategy { + pub fn new(config: &PositionConfig) -> Self { + Self { + config: config.clone(), + } + } + + /// 评估仓位是否应该退出 + pub fn evaluate(&self, position: &Position) -> Option { + let roi = position.roi(); + let holding_secs = position.holding_secs(); + + // === 1. 止损 (最高优先级) === + if roi <= (1.0 - self.config.stop_loss_pct) { + return Some(ExitSignal { + reason: format!( + "止损: ROI={:.2}x <= {:.2}x", + roi, + 1.0 - self.config.stop_loss_pct + ), + sell_pct: 1.0, + urgent: true, + }); + } + + // === 2. 时间止损 === + if holding_secs >= self.config.time_stop_secs as i64 { + return Some(ExitSignal { + reason: format!( + "时间止损: 持仓{}s >= {}s", + holding_secs, self.config.time_stop_secs + ), + sell_pct: 1.0, + urgent: false, + }); + } + + // === 3. 止盈 3 (最高级别) === + if roi >= self.config.take_profit_3_roi && position.take_profit_level < 3 { + return Some(ExitSignal { + reason: format!( + "止盈3: ROI={:.2}x >= {:.2}x", + roi, self.config.take_profit_3_roi + ), + sell_pct: self.config.take_profit_3_sell_pct, + urgent: false, + }); + } + + // === 4. 止盈 2 === + if roi >= self.config.take_profit_2_roi && position.take_profit_level < 2 { + return Some(ExitSignal { + reason: format!( + "止盈2: ROI={:.2}x >= {:.2}x", + roi, self.config.take_profit_2_roi + ), + sell_pct: self.config.take_profit_2_sell_pct, + urgent: false, + }); + } + + // === 5. 止盈 1 === + if roi >= self.config.take_profit_1_roi && position.take_profit_level < 1 { + return Some(ExitSignal { + reason: format!( + "止盈1: ROI={:.2}x >= {:.2}x", + roi, self.config.take_profit_1_roi + ), + sell_pct: self.config.take_profit_1_sell_pct, + urgent: false, + }); + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::position::manager::Position; + use chrono::Utc; + + fn make_position(roi: f64, holding_secs: i64) -> Position { + let entry_sol = 0.1; + let entry_tokens = 1_000_000; + let current_price = entry_sol * roi / entry_tokens as f64; + + Position { + mint: "test".to_string(), + bonding_curve: "test".to_string(), + entry_sol, + entry_tokens, + current_tokens: entry_tokens, + entry_tx: "test".to_string(), + entry_time: Utc::now() - chrono::Duration::seconds(holding_secs), + entry_price: entry_sol / entry_tokens as f64, + current_price, + realized_pnl: 0.0, + graduated: false, + pumpswap_pool: None, + take_profit_level: 0, + } + } + + #[test] + fn test_stop_loss() { + let config = PositionConfig { + max_position_sol: 0.1, + min_position_sol: 0.01, + max_concurrent_positions: 5, + take_profit_1_roi: 2.0, + take_profit_1_sell_pct: 0.5, + take_profit_2_roi: 5.0, + take_profit_2_sell_pct: 0.3, + take_profit_3_roi: 10.0, + take_profit_3_sell_pct: 1.0, + stop_loss_pct: 0.3, + time_stop_secs: 600, + }; + + let strategy = ExitStrategy::new(&config); + + // ROI = 0.5x (亏损 50%) → 触发止损 + let pos = make_position(0.5, 10); + let signal = strategy.evaluate(&pos); + assert!(signal.is_some()); + assert!(signal.unwrap().urgent); + } + + #[test] + fn test_take_profit_1() { + let config = PositionConfig { + max_position_sol: 0.1, + min_position_sol: 0.01, + max_concurrent_positions: 5, + take_profit_1_roi: 2.0, + take_profit_1_sell_pct: 0.5, + take_profit_2_roi: 5.0, + take_profit_2_sell_pct: 0.3, + take_profit_3_roi: 10.0, + take_profit_3_sell_pct: 1.0, + stop_loss_pct: 0.3, + time_stop_secs: 600, + }; + + let strategy = ExitStrategy::new(&config); + + // ROI = 2.5x → 触发止盈1 + let pos = make_position(2.5, 10); + let signal = strategy.evaluate(&pos); + assert!(signal.is_some()); + let sig = signal.unwrap(); + assert_eq!(sig.sell_pct, 0.5); + assert!(!sig.urgent); + } + + #[test] + fn test_time_stop() { + let config = PositionConfig { + max_position_sol: 0.1, + min_position_sol: 0.01, + max_concurrent_positions: 5, + take_profit_1_roi: 2.0, + take_profit_1_sell_pct: 0.5, + take_profit_2_roi: 5.0, + take_profit_2_sell_pct: 0.3, + take_profit_3_roi: 10.0, + take_profit_3_sell_pct: 1.0, + stop_loss_pct: 0.3, + time_stop_secs: 600, + }; + + let strategy = ExitStrategy::new(&config); + + // 持仓 700s, ROI = 1.2x → 时间止损 + let pos = make_position(1.2, 700); + let signal = strategy.evaluate(&pos); + assert!(signal.is_some()); + assert!(signal.unwrap().reason.contains("时间止损")); + } +} \ No newline at end of file diff --git a/src/position/manager.rs b/src/position/manager.rs new file mode 100644 index 0000000..f2daa00 --- /dev/null +++ b/src/position/manager.rs @@ -0,0 +1,187 @@ +//! 仓位管理器 +//! +//! 跟踪所有活跃仓位,执行止盈止损逻辑 + +use std::collections::HashMap; +use std::sync::Arc; +use chrono::{DateTime, Utc}; +use parking_lot::RwLock; +use tracing::{info, warn}; + +use crate::bonding_curve::{BondingCurveMath, LAMPORTS_PER_SOL, TOKEN_SCALING}; +use crate::config::PositionConfig; +use super::exit_strategy::ExitStrategy; + +/// 单个仓位 +#[derive(Debug, Clone)] +pub struct Position { + /// 代币 Mint + pub mint: String, + /// Bonding Curve PDA + pub bonding_curve: String, + /// 买入 SOL 数量 + pub entry_sol: f64, + /// 买入代币数量 (raw units) + pub entry_tokens: u64, + /// 当前持有代币数量 + pub current_tokens: u64, + /// 买入交易签名 + pub entry_tx: String, + /// 买入时间 + pub entry_time: DateTime, + /// 买入时价格 + pub entry_price: f64, + /// 当前价格 + pub current_price: f64, + /// 已实现收益 (SOL) + pub realized_pnl: f64, + /// 是否已毕业 + pub graduated: bool, + /// PumpSwap 池地址 (毕业后) + pub pumpswap_pool: Option, + /// 已触发的止盈级别 + pub take_profit_level: u8, +} + +impl Position { + /// 当前未实现收益 (SOL) + pub fn unrealized_pnl(&self) -> f64 { + let current_value = self.current_tokens as f64 * self.current_price; + let entry_value = self.entry_sol; + current_value - entry_value + } + + /// 当前收益率 + pub fn roi(&self) -> f64 { + if self.entry_sol <= 0.0 { + return 0.0; + } + let current_value = self.current_tokens as f64 * self.current_price; + current_value / self.entry_sol + } + + /// 持仓时间 (秒) + pub fn holding_secs(&self) -> i64 { + Utc::now().timestamp() - self.entry_time.timestamp() + } +} + +/// 仓位管理器 +pub struct PositionManager { + config: PositionConfig, + positions: Arc>>, + exit_strategy: ExitStrategy, +} + +impl PositionManager { + pub fn new(config: PositionConfig) -> Self { + let exit_strategy = ExitStrategy::new(&config); + Self { + config, + positions: Arc::new(RwLock::new(HashMap::new())), + exit_strategy, + } + } + + /// 注册新仓位 + pub fn register_position( + &self, + mint: &str, + bonding_curve: &str, + entry_sol: f64, + entry_tokens: u64, + entry_tx: String, + ) { + let entry_price = BondingCurveMath::spot_price_sol_per_token( + crate::bonding_curve::INITIAL_VIRTUAL_SOL, + crate::bonding_curve::INITIAL_VIRTUAL_TOKENS, + ); + + let position = Position { + mint: mint.to_string(), + bonding_curve: bonding_curve.to_string(), + entry_sol, + entry_tokens, + current_tokens: entry_tokens, + entry_tx, + entry_time: Utc::now(), + entry_price, + current_price: entry_price, + realized_pnl: 0.0, + graduated: false, + pumpswap_pool: None, + take_profit_level: 0, + }; + + info!( + "📝 注册仓位: mint={} | {:.4} SOL | {} tokens", + mint, + entry_sol, + entry_tokens / TOKEN_SCALING, + ); + + self.positions.write().insert(mint.to_string(), position); + } + + /// 更新代币价格 + pub async fn update_price( + &self, + mint: &str, + virtual_sol: u64, + virtual_token: u64, + ) { + let price = BondingCurveMath::spot_price_sol_per_token(virtual_sol, virtual_token); + + let mut positions = self.positions.write(); + if let Some(pos) = positions.get_mut(mint) { + pos.current_price = price; + + // 检查退出条件 + let exit_signal = self.exit_strategy.evaluate(pos); + if let Some(signal) = exit_signal { + info!( + "🚪 退出信号: mint={} | {} | ROI={:.2}x | 持仓{}s", + mint, + signal.reason, + pos.roi(), + pos.holding_secs(), + ); + // TODO: 触发卖出交易 + } + } + } + + /// 毕业事件处理 + pub async fn on_graduation(&self, mint: &str, pool: &str) { + let mut positions = self.positions.write(); + if let Some(pos) = positions.get_mut(mint) { + pos.graduated = true; + pos.pumpswap_pool = Some(pool.to_string()); + info!("🎓 仓位毕业: mint={} → pool={}", mint, pool); + } + } + + /// 获取活跃仓位数量 + pub fn active_count(&self) -> usize { + self.positions.read().len() + } + + /// 获取所有仓位 + pub fn all_positions(&self) -> Vec { + self.positions.read().values().cloned().collect() + } + + /// 移除已关闭的仓位 + pub fn close_position(&self, mint: &str) -> Option { + self.positions.write().remove(mint) + } + + /// 总未实现 PnL + pub fn total_unrealized_pnl(&self) -> f64 { + self.positions + .read() + .values() + .map(|p| p.unrealized_pnl()) + .sum() + } +} \ No newline at end of file diff --git a/src/position/mod.rs b/src/position/mod.rs new file mode 100644 index 0000000..ee644d7 --- /dev/null +++ b/src/position/mod.rs @@ -0,0 +1,7 @@ +//! 仓位管理模块 + +pub mod exit_strategy; +pub mod manager; + +pub use exit_strategy::ExitStrategy; +pub use manager::{PositionManager, Position}; \ No newline at end of file diff --git a/src/risk/circuit_breaker.rs b/src/risk/circuit_breaker.rs new file mode 100644 index 0000000..35bc9a9 --- /dev/null +++ b/src/risk/circuit_breaker.rs @@ -0,0 +1,197 @@ +//! 熔断器 +//! +//! 保护机制: +//! 1. 连续失败 N 次 → 熔断 +//! 2. 日亏损达到限制 → 熔断 +//! 3. 熔断后冷却 N 分钟 +//! 4. 冷却结束后自动恢复 + +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use chrono::{DateTime, Duration, Utc}; +use parking_lot::Mutex; +use tracing::{info, warn}; + +/// 熔断器 +pub struct CircuitBreaker { + /// 连续失败计数 + consecutive_failures: AtomicU32, + /// 最大连续失败次数 + max_consecutive_failures: u32, + /// 日累计亏损 (SOL) + daily_loss_sol: Mutex, + /// 日亏损限制 (SOL) + daily_loss_limit_sol: f64, + /// 是否已熔断 + is_tripped: AtomicBool, + /// 熔断时间 + trip_time: Mutex>>, + /// 冷却时间 (分钟) + cooldown_minutes: i64, + /// 日交易计数 + daily_trades: AtomicU32, + /// 最后重置日期 + last_reset_date: Mutex, +} + +impl CircuitBreaker { + pub fn new(max_failures: u32, daily_loss_limit: f64, cooldown_minutes: i64) -> Self { + info!( + "🛡️ 熔断器初始化: max_failures={} | daily_loss_limit={:.2} SOL | cooldown={}min", + max_failures, daily_loss_limit, cooldown_minutes + ); + + Self { + consecutive_failures: AtomicU32::new(0), + max_consecutive_failures: max_failures, + daily_loss_sol: Mutex::new(0.0), + daily_loss_limit_sol: daily_loss_limit, + is_tripped: AtomicBool::new(false), + trip_time: Mutex::new(None), + cooldown_minutes, + daily_trades: AtomicU32::new(0), + last_reset_date: Mutex::new(Utc::now().format("%Y-%m-%d").to_string()), + } + } + + /// 检查是否已熔断 + pub fn is_tripped(&self) -> bool { + if !self.is_tripped.load(Ordering::Relaxed) { + return false; + } + + // 检查冷却期是否结束 + let trip_time = self.trip_time.lock(); + if let Some(t) = *trip_time { + if Utc::now() - t > Duration::minutes(self.cooldown_minutes) { + info!("🔄 熔断器冷却期结束,自动恢复"); + drop(trip_time); + self.reset(); + return false; + } + } + + true + } + + /// 记录成功 + pub fn record_success(&self) { + self.consecutive_failures.store(0, Ordering::Relaxed); + self.daily_trades.fetch_add(1, Ordering::Relaxed); + self.check_daily_reset(); + } + + /// 记录失败 + pub fn record_failure(&self) { + let failures = self.consecutive_failures.fetch_add(1, Ordering::Relaxed) + 1; + self.daily_trades.fetch_add(1, Ordering::Relaxed); + self.check_daily_reset(); + + if failures >= self.max_consecutive_failures { + warn!( + "⚡ 连续失败 {} 次 (限制: {}),触发熔断!", + failures, self.max_consecutive_failures + ); + self.trip("连续失败次数超限"); + } + } + + /// 记录亏损 + pub fn record_loss(&self, loss_sol: f64) { + let mut daily_loss = self.daily_loss_sol.lock(); + *daily_loss += loss_sol; + + if *daily_loss >= self.daily_loss_limit_sol { + warn!( + "⚡ 日亏损 {:.4} SOL 达到限制 {:.2} SOL,触发熔断!", + *daily_loss, self.daily_loss_limit_sol + ); + drop(daily_loss); + self.trip("日亏损限制"); + } + } + + /// 获取当前连续失败次数 + pub fn current_failures(&self) -> u32 { + self.consecutive_failures.load(Ordering::Relaxed) + } + + /// 获取日累计亏损 + pub fn daily_loss(&self) -> f64 { + *self.daily_loss_sol.lock() + } + + /// 获取日交易次数 + pub fn daily_trade_count(&self) -> u32 { + self.daily_trades.load(Ordering::Relaxed) + } + + /// 触发熔断 + fn trip(&self, reason: &str) { + self.is_tripped.store(true, Ordering::Relaxed); + *self.trip_time.lock() = Some(Utc::now()); + warn!("🔴 熔断器触发: {} | 冷却 {} 分钟", reason, self.cooldown_minutes); + } + + /// 重置熔断器 + fn reset(&self) { + self.is_tripped.store(false, Ordering::Relaxed); + self.consecutive_failures.store(0, Ordering::Relaxed); + *self.trip_time.lock() = None; + info!("🟢 熔断器已重置"); + } + + /// 检查是否需要日重置 + fn check_daily_reset(&self) { + let today = Utc::now().format("%Y-%m-%d").to_string(); + let mut last_date = self.last_reset_date.lock(); + if *last_date != today { + *last_date = today; + *self.daily_loss_sol.lock() = 0.0; + self.daily_trades.store(0, Ordering::Relaxed); + info!("📅 日重置: {}", today); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_consecutive_failures() { + let cb = CircuitBreaker::new(3, 10.0, 30); + + assert!(!cb.is_tripped()); + + cb.record_failure(); + cb.record_failure(); + assert!(!cb.is_tripped()); + + cb.record_failure(); // 第3次 → 触发 + assert!(cb.is_tripped()); + } + + #[test] + fn test_success_resets_counter() { + let cb = CircuitBreaker::new(3, 10.0, 30); + + cb.record_failure(); + cb.record_failure(); + cb.record_success(); // 重置计数 + + cb.record_failure(); + cb.record_failure(); + assert!(!cb.is_tripped()); // 只有2次连续失败 + } + + #[test] + fn test_daily_loss_limit() { + let cb = CircuitBreaker::new(10, 1.0, 30); + + cb.record_loss(0.5); + assert!(!cb.is_tripped()); + + cb.record_loss(0.6); // 总计 1.1 > 1.0 + assert!(cb.is_tripped()); + } +} \ No newline at end of file diff --git a/src/risk/mod.rs b/src/risk/mod.rs new file mode 100644 index 0000000..dfc199b --- /dev/null +++ b/src/risk/mod.rs @@ -0,0 +1,5 @@ +//! 风控模块 + +pub mod circuit_breaker; + +pub use circuit_breaker::CircuitBreaker; \ No newline at end of file