QWEN3.8初次完成

This commit is contained in:
2026-07-21 00:14:46 +08:00
commit 08b9063487
30 changed files with 4793 additions and 0 deletions
+32
View File
@@ -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
View File
@@ -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
+349
View File
@@ -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(&params)?;
```
### 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 Runcargo run -- config/mainnet.toml(默认 dry_run=true
- 后期安装 sol-trade-sdk 和 solana-streamer-sdk 后,只需替换 tx_builder.rs 和 streamer.rs 中标记了 TODO 的部分即可。架构不需要改动。
+60
View File
@@ -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);
+98
View File
@@ -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
+241
View File
@@ -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);
}
}
+638
View File
@@ -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
);
}
}
+17
View File
@@ -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;
+276
View File
@@ -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
View File
@@ -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(())
}
}
+107
View File
@@ -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,
),
}
}
}
+115
View File
@@ -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);
}
}
+182
View File
@@ -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)
}
}
+115
View File
@@ -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分钟: 太晚
}
}
}
+103
View File
@@ -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)
}
}
+12
View File
@@ -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};
+283
View File
@@ -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
View File
@@ -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())
}
}
+165
View File
@@ -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)
}
}
+125
View File
@@ -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)
}
}
+107
View File
@@ -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)
}
+380
View File
@@ -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(&params)
/// ```
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
View File
@@ -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();
}
}
+120
View File
@@ -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()
}
}
+39
View File
@@ -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;
});
}
}
+208
View File
@@ -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("时间止损"));
}
}
+187
View File
@@ -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()
}
}
+7
View File
@@ -0,0 +1,7 @@
//! 仓位管理模块
pub mod exit_strategy;
pub mod manager;
pub use exit_strategy::ExitStrategy;
pub use manager::{PositionManager, Position};
+197
View File
@@ -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());
}
}
+5
View File
@@ -0,0 +1,5 @@
//! 风控模块
pub mod circuit_breaker;
pub use circuit_breaker::CircuitBreaker;