QWEN3.8初次完成
This commit is contained in:
@@ -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
|
||||
+73
@@ -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
|
||||
@@ -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 <wallet_address> 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 的部分即可。架构不需要改动。
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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<Self> {
|
||||
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<f64> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<SolForTokensResult> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<TradeRecord>,
|
||||
}
|
||||
|
||||
/// 交易记录
|
||||
#[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
|
||||
);
|
||||
}
|
||||
}
|
||||
+184
@@ -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<String>,
|
||||
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<String>,
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// 仓位管理配置
|
||||
#[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<Self> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
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<String> = 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)
|
||||
}
|
||||
}
|
||||
@@ -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分钟: 太晚
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, String>,
|
||||
/// 毕业触发阈值 (lamports)
|
||||
graduation_trigger_lamports: u64,
|
||||
/// 事件发送通道
|
||||
event_tx: mpsc::Sender<SniperEvent>,
|
||||
}
|
||||
|
||||
impl BondingCurveMonitor {
|
||||
pub fn new(config: &DetectionConfig, event_tx: mpsc::Sender<SniperEvent>) -> 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)
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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<SniperEvent>,
|
||||
) -> 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<SniperEvent>,
|
||||
) -> 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<TokenCreatedEvent> {
|
||||
// TODO: 实现完整的交易解析逻辑
|
||||
// 需要解析 inner instructions 和 log messages 来提取:
|
||||
// - mint 地址
|
||||
// - bonding curve PDA
|
||||
// - creator 地址
|
||||
// - name, symbol, uri
|
||||
//
|
||||
// 当 solana-streamer-sdk 安装后,此函数将被替换
|
||||
|
||||
// 临时: 返回 None (不解析)
|
||||
let _ = slot;
|
||||
None
|
||||
}
|
||||
+104
@@ -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<T> = Result<T, SniperError>;
|
||||
|
||||
impl From<serde_json::Error> for SniperError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
SniperError::Serialization(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for SniperError {
|
||||
fn from(e: toml::de::Error) -> Self {
|
||||
SniperError::Config(e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -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<serde_json::Value>,
|
||||
error: Option<JsonRpcError>,
|
||||
}
|
||||
|
||||
#[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<String> {
|
||||
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<String> {
|
||||
let encoded: Vec<String> = 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<Vec<String>> {
|
||||
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<JsonRpcResponse> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<Self> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<SubmitResult> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
let sig = jito_client.send_transaction(tx).await?;
|
||||
info!("📤 Jito 提交: {}", sig);
|
||||
Ok(sig)
|
||||
}
|
||||
@@ -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<Arc<Keypair>>,
|
||||
current_idx: AtomicUsize,
|
||||
program_id: Pubkey,
|
||||
compute_unit_limit: u32,
|
||||
default_priority_fee: u64,
|
||||
}
|
||||
|
||||
impl TxBuilder {
|
||||
pub async fn new(config: &ExecutionConfig) -> SniperResult<Self> {
|
||||
let rpc_client = RpcClient::new(config.rpc_url.clone());
|
||||
|
||||
// 加载钱包
|
||||
let keypairs: Vec<Arc<Keypair>> = 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<Keypair> {
|
||||
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<Transaction> {
|
||||
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<Transaction> {
|
||||
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<Vec<Instruction>> {
|
||||
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<Vec<Instruction>> {
|
||||
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<Instruction> {
|
||||
// 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<Instruction> {
|
||||
// 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<Keypair> {
|
||||
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<u8> = 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
|
||||
}
|
||||
+264
@@ -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::<SniperEvent>(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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<Metrics>) -> 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<ExitSignal> {
|
||||
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("时间止损"));
|
||||
}
|
||||
}
|
||||
@@ -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<Utc>,
|
||||
/// 买入时价格
|
||||
pub entry_price: f64,
|
||||
/// 当前价格
|
||||
pub current_price: f64,
|
||||
/// 已实现收益 (SOL)
|
||||
pub realized_pnl: f64,
|
||||
/// 是否已毕业
|
||||
pub graduated: bool,
|
||||
/// PumpSwap 池地址 (毕业后)
|
||||
pub pumpswap_pool: Option<String>,
|
||||
/// 已触发的止盈级别
|
||||
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<RwLock<HashMap<String, Position>>>,
|
||||
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<Position> {
|
||||
self.positions.read().values().cloned().collect()
|
||||
}
|
||||
|
||||
/// 移除已关闭的仓位
|
||||
pub fn close_position(&self, mint: &str) -> Option<Position> {
|
||||
self.positions.write().remove(mint)
|
||||
}
|
||||
|
||||
/// 总未实现 PnL
|
||||
pub fn total_unrealized_pnl(&self) -> f64 {
|
||||
self.positions
|
||||
.read()
|
||||
.values()
|
||||
.map(|p| p.unrealized_pnl())
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! 仓位管理模块
|
||||
|
||||
pub mod exit_strategy;
|
||||
pub mod manager;
|
||||
|
||||
pub use exit_strategy::ExitStrategy;
|
||||
pub use manager::{PositionManager, Position};
|
||||
@@ -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<f64>,
|
||||
/// 日亏损限制 (SOL)
|
||||
daily_loss_limit_sol: f64,
|
||||
/// 是否已熔断
|
||||
is_tripped: AtomicBool,
|
||||
/// 熔断时间
|
||||
trip_time: Mutex<Option<DateTime<Utc>>>,
|
||||
/// 冷却时间 (分钟)
|
||||
cooldown_minutes: i64,
|
||||
/// 日交易计数
|
||||
daily_trades: AtomicU32,
|
||||
/// 最后重置日期
|
||||
last_reset_date: Mutex<String>,
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! 风控模块
|
||||
|
||||
pub mod circuit_breaker;
|
||||
|
||||
pub use circuit_breaker::CircuitBreaker;
|
||||
Reference in New Issue
Block a user