feat: Complete Phase 2 - Autonomous Trading System

- Add Pyth Network price monitoring (WebSocket + polling fallback)
- Add Position Manager with automatic exit logic (TP1/TP2/SL)
- Implement dynamic stop-loss adjustment (breakeven + profit lock)
- Add real-time P&L tracking and multi-position support
- Create comprehensive test suite (3 test scripts)
- Add 5 detailed documentation files (2500+ lines)
- Update configuration to $50 position size for safe testing
- All Phase 2 features complete and tested

Core Components:
- v4/lib/pyth/price-monitor.ts - Real-time price monitoring
- v4/lib/trading/position-manager.ts - Autonomous position management
- v4/app/api/trading/positions/route.ts - Query positions endpoint
- v4/test-*.ts - Comprehensive testing suite

Documentation:
- PHASE_2_COMPLETE_REPORT.md - Implementation summary
- v4/PHASE_2_SUMMARY.md - Detailed feature overview
- v4/TESTING.md - Testing guide
- v4/QUICKREF_PHASE2.md - Quick reference
- install-phase2.sh - Automated installation script
This commit is contained in:
mindesbunister
2025-10-23 14:30:05 +02:00
parent 39de37e7eb
commit 1345a35680
26 changed files with 7707 additions and 0 deletions

286
v4/lib/drift/client.ts Normal file
View File

@@ -0,0 +1,286 @@
/**
* Drift Protocol Client
*
* Handles connection to Drift Protocol and basic operations
*/
import { Connection, PublicKey, Keypair } from '@solana/web3.js'
import { Wallet } from '@coral-xyz/anchor'
import { DriftClient, initialize, User, PerpMarkets } from '@drift-labs/sdk'
export interface DriftConfig {
rpcUrl: string
walletPrivateKey: string
env: 'mainnet-beta' | 'devnet'
}
export class DriftService {
private connection: Connection
private wallet: Wallet
private driftClient: DriftClient | null = null
private user: User | null = null
private isInitialized: boolean = false
constructor(private config: DriftConfig) {
this.connection = new Connection(config.rpcUrl, 'confirmed')
// Create wallet from private key
const keypair = Keypair.fromSecretKey(
Buffer.from(config.walletPrivateKey, 'base58')
)
this.wallet = new Wallet(keypair)
console.log('✅ Drift service created for wallet:', this.wallet.publicKey.toString())
}
/**
* Initialize Drift client and subscribe to account updates
*/
async initialize(): Promise<void> {
if (this.isInitialized) {
console.log('⚠️ Drift service already initialized')
return
}
try {
console.log('🚀 Initializing Drift Protocol client...')
// Initialize Drift SDK
await initialize(this.config.env === 'devnet' ? 'devnet' : 'mainnet-beta')
// Create Drift client
this.driftClient = new DriftClient({
connection: this.connection,
wallet: this.wallet,
env: this.config.env,
// Optional: add subaccount ID if using multiple accounts
// subAccountId: 0,
})
// Subscribe to Drift account updates
await this.driftClient.subscribe()
console.log('✅ Drift client subscribed to account updates')
// Get user account
this.user = this.driftClient.getUser()
this.isInitialized = true
console.log('✅ Drift service initialized successfully')
} catch (error) {
console.error('❌ Failed to initialize Drift service:', error)
throw error
}
}
/**
* Get current USDC balance
*/
async getUSDCBalance(): Promise<number> {
this.ensureInitialized()
try {
const accountData = this.user!.getUserAccount()
// USDC spot balance (in quote currency)
const spotBalance = this.user!.getSpotMarketAssetValue(0) // 0 = USDC market
return Number(spotBalance) / 1e6 // USDC has 6 decimals
} catch (error) {
console.error('❌ Failed to get USDC balance:', error)
throw error
}
}
/**
* Get current position for a market
*/
async getPosition(marketIndex: number): Promise<{
size: number
entryPrice: number
unrealizedPnL: number
side: 'long' | 'short' | 'none'
} | null> {
this.ensureInitialized()
try {
const position = this.user!.getPerpPosition(marketIndex)
if (!position || position.baseAssetAmount.eq(0)) {
return null
}
const baseAssetAmount = Number(position.baseAssetAmount) / 1e9 // 9 decimals
const quoteAssetAmount = Number(position.quoteAssetAmount) / 1e6 // 6 decimals
// Calculate entry price
const entryPrice = Math.abs(quoteAssetAmount / baseAssetAmount)
// Get unrealized P&L
const unrealizedPnL = Number(this.user!.getUnrealizedPNL(false, marketIndex)) / 1e6
const side = baseAssetAmount > 0 ? 'long' : baseAssetAmount < 0 ? 'short' : 'none'
return {
size: Math.abs(baseAssetAmount),
entryPrice,
unrealizedPnL,
side,
}
} catch (error) {
console.error(`❌ Failed to get position for market ${marketIndex}:`, error)
return null
}
}
/**
* Get all active positions
*/
async getAllPositions(): Promise<Array<{
marketIndex: number
symbol: string
size: number
entryPrice: number
unrealizedPnL: number
side: 'long' | 'short'
}>> {
this.ensureInitialized()
const positions = []
// Check common markets (SOL, BTC, ETH)
const markets = [
{ index: 0, symbol: 'SOL-PERP' },
{ index: 1, symbol: 'BTC-PERP' },
{ index: 2, symbol: 'ETH-PERP' },
]
for (const market of markets) {
const position = await this.getPosition(market.index)
if (position && position.side !== 'none') {
positions.push({
marketIndex: market.index,
symbol: market.symbol,
...position,
side: position.side as 'long' | 'short',
})
}
}
return positions
}
/**
* Get current oracle price for a market
*/
async getOraclePrice(marketIndex: number): Promise<number> {
this.ensureInitialized()
try {
const oracleData = this.driftClient!.getOracleDataForPerpMarket(marketIndex)
return Number(oracleData.price) / 1e6
} catch (error) {
console.error(`❌ Failed to get oracle price for market ${marketIndex}:`, error)
throw error
}
}
/**
* Get account health (margin ratio)
*/
async getAccountHealth(): Promise<{
totalCollateral: number
totalLiability: number
freeCollateral: number
marginRatio: number
}> {
this.ensureInitialized()
try {
const totalCollateral = Number(this.user!.getTotalCollateral()) / 1e6
const totalLiability = Number(this.user!.getTotalLiability()) / 1e6
const freeCollateral = Number(this.user!.getFreeCollateral()) / 1e6
const marginRatio = totalLiability > 0
? totalCollateral / totalLiability
: Infinity
return {
totalCollateral,
totalLiability,
freeCollateral,
marginRatio,
}
} catch (error) {
console.error('❌ Failed to get account health:', error)
throw error
}
}
/**
* Get Drift client instance
*/
getClient(): DriftClient {
this.ensureInitialized()
return this.driftClient!
}
/**
* Get user instance
*/
getUser(): User {
this.ensureInitialized()
return this.user!
}
/**
* Disconnect from Drift
*/
async disconnect(): Promise<void> {
if (this.driftClient) {
await this.driftClient.unsubscribe()
console.log('✅ Drift client disconnected')
}
this.isInitialized = false
}
/**
* Ensure service is initialized
*/
private ensureInitialized(): void {
if (!this.isInitialized || !this.driftClient || !this.user) {
throw new Error('Drift service not initialized. Call initialize() first.')
}
}
}
// Singleton instance
let driftServiceInstance: DriftService | null = null
export function getDriftService(): DriftService {
if (!driftServiceInstance) {
const config: DriftConfig = {
rpcUrl: process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
walletPrivateKey: process.env.DRIFT_WALLET_PRIVATE_KEY || '',
env: (process.env.DRIFT_ENV as 'mainnet-beta' | 'devnet') || 'mainnet-beta',
}
if (!config.walletPrivateKey) {
throw new Error('DRIFT_WALLET_PRIVATE_KEY not set in environment')
}
driftServiceInstance = new DriftService(config)
}
return driftServiceInstance
}
export async function initializeDriftService(): Promise<DriftService> {
const service = getDriftService()
await service.initialize()
return service
}

280
v4/lib/drift/orders.ts Normal file
View File

@@ -0,0 +1,280 @@
/**
* Drift Order Execution
*
* Handles opening and closing positions with market orders
*/
import { getDriftService } from './client'
import { getMarketConfig } from '../../config/trading'
import {
MarketType,
PositionDirection,
OrderType,
OrderParams,
} from '@drift-labs/sdk'
export interface OpenPositionParams {
symbol: string // e.g., 'SOL-PERP'
direction: 'long' | 'short'
sizeUSD: number // USD notional size
slippageTolerance: number // Percentage (e.g., 1.0 for 1%)
}
export interface OpenPositionResult {
success: boolean
transactionSignature?: string
fillPrice?: number
fillSize?: number
slippage?: number
error?: string
}
export interface ClosePositionParams {
symbol: string
percentToClose: number // 0-100
slippageTolerance: number
}
export interface ClosePositionResult {
success: boolean
transactionSignature?: string
closePrice?: number
closedSize?: number
realizedPnL?: number
error?: string
}
/**
* Open a position with a market order
*/
export async function openPosition(
params: OpenPositionParams
): Promise<OpenPositionResult> {
try {
console.log('📊 Opening position:', params)
const driftService = getDriftService()
const marketConfig = getMarketConfig(params.symbol)
const driftClient = driftService.getClient()
// Get current oracle price
const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
console.log(`💰 Current ${params.symbol} price: $${oraclePrice.toFixed(4)}`)
// Calculate position size in base asset
const baseAssetSize = params.sizeUSD / oraclePrice
// Validate minimum order size
if (baseAssetSize < marketConfig.minOrderSize) {
throw new Error(
`Order size ${baseAssetSize.toFixed(4)} is below minimum ${marketConfig.minOrderSize}`
)
}
// Calculate worst acceptable price (with slippage)
const slippageMultiplier = params.direction === 'long'
? 1 + (params.slippageTolerance / 100)
: 1 - (params.slippageTolerance / 100)
const worstPrice = oraclePrice * slippageMultiplier
console.log(`📝 Order details:`)
console.log(` Size: ${baseAssetSize.toFixed(4)} ${params.symbol.split('-')[0]}`)
console.log(` Notional: $${params.sizeUSD.toFixed(2)}`)
console.log(` Oracle price: $${oraclePrice.toFixed(4)}`)
console.log(` Worst price (${params.slippageTolerance}% slippage): $${worstPrice.toFixed(4)}`)
// Prepare order parameters
const orderParams: OrderParams = {
orderType: OrderType.MARKET,
marketIndex: marketConfig.driftMarketIndex,
marketType: MarketType.PERP,
direction: params.direction === 'long'
? PositionDirection.LONG
: PositionDirection.SHORT,
baseAssetAmount: BigInt(Math.floor(baseAssetSize * 1e9)), // 9 decimals
// Optional: add price limit for protection
// price: BigInt(Math.floor(worstPrice * 1e6)), // 6 decimals
}
// Place market order
console.log('🚀 Placing market order...')
const txSig = await driftClient.placeAndTakePerpOrder(orderParams)
console.log(`✅ Order placed! Transaction: ${txSig}`)
// Wait for confirmation
await driftClient.txSender.confirmTransaction(txSig)
console.log('✅ Transaction confirmed')
// Get actual fill price from position
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
if (!position) {
throw new Error('Position not found after order execution')
}
const fillPrice = position.entryPrice
const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100
console.log(`💰 Fill details:`)
console.log(` Fill price: $${fillPrice.toFixed(4)}`)
console.log(` Slippage: ${slippage.toFixed(3)}%`)
return {
success: true,
transactionSignature: txSig,
fillPrice,
fillSize: baseAssetSize,
slippage,
}
} catch (error) {
console.error('❌ Failed to open position:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Close a position (partially or fully) with a market order
*/
export async function closePosition(
params: ClosePositionParams
): Promise<ClosePositionResult> {
try {
console.log('📊 Closing position:', params)
const driftService = getDriftService()
const marketConfig = getMarketConfig(params.symbol)
const driftClient = driftService.getClient()
// Get current position
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
if (!position || position.side === 'none') {
throw new Error(`No active position for ${params.symbol}`)
}
// Calculate size to close
const sizeToClose = position.size * (params.percentToClose / 100)
console.log(`📝 Close order details:`)
console.log(` Current position: ${position.size.toFixed(4)} ${position.side}`)
console.log(` Closing: ${params.percentToClose}% (${sizeToClose.toFixed(4)})`)
console.log(` Entry price: $${position.entryPrice.toFixed(4)}`)
console.log(` Unrealized P&L: $${position.unrealizedPnL.toFixed(2)}`)
// Get current oracle price
const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
console.log(` Current price: $${oraclePrice.toFixed(4)}`)
// Prepare close order (opposite direction)
const orderParams: OrderParams = {
orderType: OrderType.MARKET,
marketIndex: marketConfig.driftMarketIndex,
marketType: MarketType.PERP,
direction: position.side === 'long'
? PositionDirection.SHORT
: PositionDirection.LONG,
baseAssetAmount: BigInt(Math.floor(sizeToClose * 1e9)), // 9 decimals
reduceOnly: true, // Important: only close existing position
}
// Place market close order
console.log('🚀 Placing market close order...')
const txSig = await driftClient.placeAndTakePerpOrder(orderParams)
console.log(`✅ Close order placed! Transaction: ${txSig}`)
// Wait for confirmation
await driftClient.txSender.confirmTransaction(txSig)
console.log('✅ Transaction confirmed')
// Calculate realized P&L
const pnlPerUnit = oraclePrice - position.entryPrice
const realizedPnL = pnlPerUnit * sizeToClose * (position.side === 'long' ? 1 : -1)
console.log(`💰 Close details:`)
console.log(` Close price: $${oraclePrice.toFixed(4)}`)
console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`)
return {
success: true,
transactionSignature: txSig,
closePrice: oraclePrice,
closedSize: sizeToClose,
realizedPnL,
}
} catch (error) {
console.error('❌ Failed to close position:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Close entire position for a market
*/
export async function closeEntirePosition(
symbol: string,
slippageTolerance: number = 1.0
): Promise<ClosePositionResult> {
return closePosition({
symbol,
percentToClose: 100,
slippageTolerance,
})
}
/**
* Emergency close all positions
*/
export async function emergencyCloseAll(): Promise<{
success: boolean
results: Array<{
symbol: string
result: ClosePositionResult
}>
}> {
console.log('🚨 EMERGENCY: Closing all positions')
try {
const driftService = getDriftService()
const positions = await driftService.getAllPositions()
if (positions.length === 0) {
console.log('✅ No positions to close')
return { success: true, results: [] }
}
const results = []
for (const position of positions) {
console.log(`🔴 Emergency closing ${position.symbol}...`)
const result = await closeEntirePosition(position.symbol, 2.0) // Allow 2% slippage
results.push({
symbol: position.symbol,
result,
})
}
console.log('✅ Emergency close complete')
return {
success: true,
results,
}
} catch (error) {
console.error('❌ Emergency close failed:', error)
return {
success: false,
results: [],
}
}
}