fix: timeframe handling and progress tracking improvements

- Fix timeframe parameter handling in enhanced-screenshot API route
- Support both 'timeframe' (singular) and 'timeframes' (array) parameters
- Add proper sessionId propagation for real-time progress tracking
- Enhance MACD analysis prompt with detailed crossover definitions
- Add progress tracker service with Server-Sent Events support
- Fix Next.js build errors in chart components (module variable conflicts)
- Change dev environment port from 9000:3000 to 9001:3000
- Improve AI analysis layout detection logic
- Add comprehensive progress tracking through all service layers
This commit is contained in:
mindesbunister
2025-07-17 10:41:18 +02:00
parent 27df0304c6
commit ff4e9737fb
26 changed files with 1656 additions and 277 deletions

View File

@@ -3,6 +3,7 @@ import fs from 'fs/promises'
import path from 'path'
import { enhancedScreenshotService, ScreenshotConfig } from './enhanced-screenshot'
import { TradingViewCredentials } from './tradingview-automation'
import { progressTracker } from './progress-tracker'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
@@ -70,16 +71,61 @@ export interface AnalysisResult {
}
export class AIAnalysisService {
async analyzeScreenshot(filename: string): Promise<AnalysisResult | null> {
async analyzeScreenshot(filenameOrPath: string): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
const imagePath = path.join(screenshotsDir, filename)
let imagePath: string
// Check if it's already a full path or just a filename
if (path.isAbsolute(filenameOrPath)) {
// It's already a full path
imagePath = filenameOrPath
} else {
// It's just a filename, construct the full path
const screenshotsDir = path.join(process.cwd(), 'screenshots')
imagePath = path.join(screenshotsDir, filenameOrPath)
}
// Read image file
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
const prompt = `You are now a professional trading assistant. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
⚠️ CRITICAL RSI READING INSTRUCTION: The RSI indicator shows a numerical value AND a line position. IGNORE the number if it conflicts with the visual line position. If the RSI line appears in the top area of the indicator (above the 70 horizontal line), report it as OVERBOUGHT regardless of what number is displayed.
**CRITICAL: FIRST IDENTIFY THE LAYOUT TYPE**
Before analyzing any indicators, you MUST determine which layout you are looking at:
**AI Layout identification:**
- Has RSI at the TOP of the chart
- Has MACD at the BOTTOM of the chart
- Has EMAs (9, 20, 50, 200) visible on the main chart
- Does NOT have VWAP or OBV
**DIY Layout identification:**
- Has Stochastic RSI at the TOP of the chart
- Has OBV (On-Balance Volume) at the BOTTOM of the chart
- Has VWAP (thick line) visible on the main chart
- Does NOT have regular RSI or MACD
**LAYOUT-SPECIFIC INDICATOR INFORMATION:**
If this is an AI Layout screenshot, it contains:
- TOP: RSI indicator (overbought above 70, oversold below 30)
- MIDDLE (on chart): SVP, ATR Bands, EMA 9, EMA 20, EMA 50, EMA 200
- BOTTOM: MACD indicator (NOT AT TOP - this is at the bottom of the chart)
* MACD has two lines: MACD line (usually blue/faster) and Signal line (usually red/slower)
* Bullish crossover = MACD line crosses ABOVE signal line (upward momentum)
* Bearish crossover = MACD line crosses BELOW signal line (downward momentum)
* Histogram bars: Green = bullish momentum, Red = bearish momentum
* Zero line: Above = overall bullish trend, Below = overall bearish trend
If this is a DIY Module Layout screenshot, it contains:
- TOP: Stochastic RSI indicator
- MIDDLE (on chart): VWAP, Smart Money Concepts by Algo
- BOTTOM: OBV (On-Balance Volume) indicator
**TRADING ANALYSIS REQUIREMENTS:**
1. **TIMEFRAME RISK ASSESSMENT**: Based on the timeframe shown in the screenshot, adjust risk accordingly:
@@ -104,9 +150,23 @@ export class AIAnalysisService {
4. **CONFIRMATION TRIGGERS**: Exact signals to wait for:
- Specific candle patterns, indicator crosses, volume confirmations
- RSI behavior: "If RSI crosses above 70 while price is under resistance → wait"
- RSI/Stoch RSI behavior:
* MANDATORY: State if RSI is "OVERBOUGHT" (line above 70), "OVERSOLD" (line below 30), or "NEUTRAL" (between 30-70)
* Do NOT say "above 50 line" - only report overbought/oversold/neutral status
* If RSI line appears in upper area of indicator box, it's likely overbought regardless of number
- VWAP: "If price retakes VWAP with bullish momentum → consider invalidation"
- OBV: "If OBV starts climbing while price stays flat → early exit or reconsider bias"
- MACD: Analyze MACD crossovers at the BOTTOM indicator panel.
* Bullish crossover = MACD line (faster line) crosses ABOVE signal line (slower line) - indicates upward momentum
* Bearish crossover = MACD line crosses BELOW signal line - indicates downward momentum
* Histogram: Green bars = increasing bullish momentum, Red bars = increasing bearish momentum
* Report specific crossover direction and current momentum state
- EMA alignment: Check 9/20/50/200 EMA positioning and price relationship
- Smart Money Concepts: Identify supply/demand zones and market structure
5. **LAYOUT-SPECIFIC ANALYSIS**:
- AI Layout: Focus on RSI momentum (MUST identify overbought/oversold status), EMA alignment, MACD signals, and ATR bands for volatility
- DIY Layout: Emphasize VWAP positioning, Stoch RSI oversold/overbought levels, OBV volume confirmation, and Smart Money Concepts structure
Examine the chart and identify:
- Current price action and trend direction
@@ -118,6 +178,7 @@ Examine the chart and identify:
Provide your analysis in this exact JSON format (replace values with your analysis):
{
"layoutDetected": "AI Layout|DIY Layout",
"summary": "Objective technical analysis with timeframe risk assessment and specific trading setup",
"marketSentiment": "BULLISH|BEARISH|NEUTRAL",
"keyLevels": {
@@ -153,10 +214,14 @@ Provide your analysis in this exact JSON format (replace values with your analys
"riskToReward": "1:2",
"confirmationTrigger": "Specific signal: Bearish engulfing candle on rejection from VWAP zone with RSI under 50",
"indicatorAnalysis": {
"rsi": "Specific RSI level and precise interpretation with action triggers",
"vwap": "VWAP relationship to price with exact invalidation levels",
"obv": "Volume analysis with specific behavioral expectations",
"macd": "MACD signal line crosses and momentum analysis"
"rsi": "ONLY if AI Layout detected: RSI status - MUST be 'OVERBOUGHT' (above 70 line), 'OVERSOLD' (below 30 line), or 'NEUTRAL' (30-70). Do NOT reference 50 line position.",
"stochRsi": "ONLY if DIY Layout detected: Stochastic RSI oversold/overbought conditions - check both %K and %D lines",
"vwap": "ONLY if DIY Layout detected: VWAP relationship to price with exact invalidation levels",
"obv": "ONLY if DIY Layout detected: OBV volume analysis with specific behavioral expectations",
"macd": "ONLY if AI Layout detected: MACD analysis - The MACD is located at the BOTTOM of the chart. Analyze: 1) Histogram bars (green = bullish momentum, red = bearish), 2) Signal line crossover (MACD line crossing ABOVE signal line = bullish crossover, BELOW = bearish crossover), 3) Zero line position. Report specific crossover direction and current momentum state.",
"emaAlignment": "If AI Layout: EMA 9/20/50/200 positioning and price relationship - note stack order and price position",
"atrBands": "If AI Layout: ATR bands for volatility and support/resistance",
"smartMoney": "If DIY Layout: Smart Money Concepts supply/demand zones and structure"
},
"timeframeRisk": {
"assessment": "Risk level based on detected timeframe",
@@ -245,14 +310,23 @@ Return only the JSON object with your technical analysis.`
}
}
async analyzeMultipleScreenshots(filenames: string[]): Promise<AnalysisResult | null> {
async analyzeMultipleScreenshots(filenamesOrPaths: string[]): Promise<AnalysisResult | null> {
try {
const screenshotsDir = path.join(process.cwd(), 'screenshots')
// Read all image files and convert to base64
const images = await Promise.all(
filenames.map(async (filename) => {
const imagePath = path.join(screenshotsDir, filename)
filenamesOrPaths.map(async (filenameOrPath) => {
let imagePath: string
// Check if it's already a full path or just a filename
if (path.isAbsolute(filenameOrPath)) {
// It's already a full path
imagePath = filenameOrPath
} else {
// It's just a filename, construct the full path
const screenshotsDir = path.join(process.cwd(), 'screenshots')
imagePath = path.join(screenshotsDir, filenameOrPath)
}
const imageBuffer = await fs.readFile(imagePath)
const base64Image = imageBuffer.toString('base64')
return {
@@ -265,15 +339,40 @@ Return only the JSON object with your technical analysis.`
})
)
const layoutInfo = filenames.map(f => {
if (f.includes('_ai_')) return 'AI Layout'
const layoutInfo = filenamesOrPaths.map(f => {
const filename = path.basename(f) // Extract filename from path
if (filename.includes('_ai_')) return 'AI Layout'
if (f.includes('_diy_') || f.includes('_Diy module_')) return 'DIY Module Layout'
return 'Unknown Layout'
}).join(' and ')
const prompt = `You are now a professional trading assistant. You behave with the precision and decisiveness of a top proprietary desk trader. No vagueness, no fluff.
I'm providing you with ${filenames.length} TradingView chart screenshots from different layouts: ${layoutInfo}.
I'm providing you with ${filenamesOrPaths.length} TradingView chart screenshots from different layouts: ${layoutInfo}.
⚠️ CRITICAL RSI READING INSTRUCTION: The RSI indicator shows a numerical value AND a line position. IGNORE the number if it conflicts with the visual line position. If the RSI line appears in the top area of the indicator (above the 70 horizontal line), report it as OVERBOUGHT regardless of what number is displayed.
**LAYOUT-SPECIFIC INDICATOR INFORMATION:**
**AI Layout Structure:**
- TOP: RSI indicator (14-period) - Look for EXACT numerical value displayed and visual position relative to 30/50/70 levels
- MIDDLE (on chart): SVP, ATR Bands, EMA 9 (yellow), EMA 20 (orange), EMA 50 (blue), EMA 200 (red)
- BOTTOM: MACD indicator with signal line and histogram
**DIY Module Layout Structure:**
- TOP: Stochastic RSI indicator - Check both %K and %D lines relative to 20/50/80 levels
- MIDDLE (on chart): VWAP (thick line), Smart Money Concepts by Algo (supply/demand zones)
- BOTTOM: OBV (On-Balance Volume) indicator showing volume flow
**CRITICAL: ACCURATE INDICATOR READING:**
- RSI: IGNORE the numerical value if it conflicts with visual position. The RSI line position on the chart is what matters:
* If RSI line is visually ABOVE the 70 horizontal line = OVERBOUGHT (regardless of number shown)
* If RSI line is visually BELOW the 30 horizontal line = OVERSOLD (regardless of number shown)
* If RSI line is between 30-70 = NEUTRAL zone
* Example: If number shows "56.61" but line appears above 70 level, report as "RSI OVERBOUGHT at 70+ level"
- MACD: Check histogram bars (green/red) and signal line crossovers
- EMA Alignment: Note price position relative to each EMA and EMA stack order
- VWAP: Identify if price is above/below VWAP and by how much
**TRADING ANALYSIS REQUIREMENTS:**
@@ -299,12 +398,23 @@ I'm providing you with ${filenames.length} TradingView chart screenshots from di
4. **CONFIRMATION TRIGGERS**: Exact signals to wait for:
- Specific candle patterns, indicator crosses, volume confirmations
- RSI behavior: "If RSI crosses above 70 while price is under resistance → wait"
- RSI/Stoch RSI behavior: "If RSI crosses above 70 while price is under resistance → wait"
* CRITICAL: Read RSI visually - if the line appears above 70 level regardless of numerical display, treat as overbought
* If RSI line appears below 30 level visually, treat as oversold regardless of number shown
- VWAP: "If price retakes VWAP with bullish momentum → consider invalidation"
- OBV: "If OBV starts climbing while price stays flat → early exit or reconsider bias"
- MACD: Analyze MACD crossovers at the BOTTOM indicator panel.
* Bullish crossover = MACD line (faster line) crosses ABOVE signal line (slower line) - indicates upward momentum
* Bearish crossover = MACD line crosses BELOW signal line - indicates downward momentum
* Histogram: Green bars = increasing bullish momentum, Red bars = increasing bearish momentum
* Report specific crossover direction and current momentum state
- EMA alignment: Check 9/20/50/200 EMA positioning and price relationship
- Smart Money Concepts: Identify supply/demand zones and market structure
5. **CROSS-LAYOUT ANALYSIS**:
- Compare insights from different chart layouts for confirmations/contradictions
- AI Layout insights: RSI momentum + EMA alignment + MACD signals
- DIY Layout insights: VWAP positioning + Stoch RSI + OBV volume + Smart Money structure
- Use multiple perspectives to increase confidence
- Note which layout provides clearest signals
@@ -348,10 +458,14 @@ I'm providing you with ${filenames.length} TradingView chart screenshots from di
"riskToReward": "1:2.5",
"confirmationTrigger": "Specific signal: Bearish engulfing candle on rejection from VWAP zone with RSI under 50",
"indicatorAnalysis": {
"rsi": "Specific RSI level and precise interpretation with action triggers",
"vwap": "VWAP relationship to price with exact invalidation levels",
"obv": "Volume analysis with specific behavioral expectations",
"macd": "MACD signal line crosses and momentum analysis",
"rsi": "AI Layout: RSI status - MUST be 'OVERBOUGHT' (above 70 line), 'OVERSOLD' (below 30 line), or 'NEUTRAL' (30-70). Do NOT reference 50 line position.",
"stochRsi": "DIY Layout: Stochastic RSI oversold/overbought conditions",
"vwap": "DIY Layout: VWAP relationship to price with exact invalidation levels",
"obv": "DIY Layout: OBV volume analysis with specific behavioral expectations",
"macd": "AI Layout: MACD signal line crosses and histogram momentum analysis - green/red bars and signal line position",
"emaAlignment": "AI Layout: EMA 9/20/50/200 positioning and price relationship - note stack order and price position",
"atrBands": "AI Layout: ATR bands for volatility and support/resistance",
"smartMoney": "DIY Layout: Smart Money Concepts supply/demand zones and structure",
"crossLayoutConsensus": "Detailed comparison of how different layouts confirm or contradict signals"
},
"layoutComparison": {
@@ -384,7 +498,7 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
}
]
console.log(`🤖 Sending ${filenames.length} screenshots to OpenAI for multi-layout analysis...`)
console.log(`🤖 Sending ${filenamesOrPaths.length} screenshots to OpenAI for multi-layout analysis...`)
const response = await openai.chat.completions.create({
model: "gpt-4o-mini", // Cost-effective model with vision capabilities
@@ -491,10 +605,12 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
screenshots: string[]
analysis: AnalysisResult | null
}> {
const { sessionId } = config
try {
console.log(`Starting automated capture with config for ${config.symbol} ${config.timeframe}`)
// Capture screenshots using enhanced service
// Capture screenshots using enhanced service (this will handle its own progress)
const screenshots = await enhancedScreenshotService.captureWithLogin(config)
if (screenshots.length === 0) {
@@ -503,6 +619,11 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
console.log(`${screenshots.length} screenshot(s) captured`)
// Add AI analysis step to progress if sessionId exists
if (sessionId) {
progressTracker.updateStep(sessionId, 'analysis', 'active', 'Running AI analysis on screenshots...')
}
let analysis: AnalysisResult | null = null
if (screenshots.length === 1) {
@@ -514,11 +635,20 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
}
if (!analysis) {
if (sessionId) {
progressTracker.updateStep(sessionId, 'analysis', 'error', 'AI analysis failed to generate results')
}
throw new Error('Failed to analyze screenshots')
}
console.log(`Analysis completed for ${config.symbol} ${config.timeframe}`)
if (sessionId) {
progressTracker.updateStep(sessionId, 'analysis', 'completed', 'AI analysis completed successfully!')
// Mark session as complete
setTimeout(() => progressTracker.deleteSession(sessionId), 1000)
}
return {
screenshots,
analysis
@@ -526,6 +656,20 @@ Analyze all provided screenshots comprehensively and return only the JSON respon
} catch (error) {
console.error('Automated capture and analysis with config failed:', error)
if (sessionId) {
// Find the active step and mark it as error
const progress = progressTracker.getProgress(sessionId)
if (progress) {
const activeStep = progress.steps.find(step => step.status === 'active')
if (activeStep) {
progressTracker.updateStep(sessionId, activeStep.id, 'error', error instanceof Error ? error.message : 'Unknown error')
}
}
// Clean up session
setTimeout(() => progressTracker.deleteSession(sessionId), 5000)
}
return {
screenshots: [],
analysis: null

View File

@@ -3,12 +3,14 @@ import fs from 'fs/promises'
import path from 'path'
import puppeteer from 'puppeteer'
import { Browser, Page } from 'puppeteer'
import { progressTracker, ProgressStep } from './progress-tracker'
export interface ScreenshotConfig {
symbol: string
timeframe: string
layouts?: string[] // Multiple chart layouts if needed
credentials?: TradingViewCredentials // Optional if using .env
sessionId?: string // For progress tracking
}
// Layout URL mappings for direct navigation
@@ -28,6 +30,13 @@ export class EnhancedScreenshotService {
console.log('📋 Config:', config)
const screenshotFiles: string[] = []
const { sessionId } = config
console.log('🔍 Enhanced Screenshot Service received sessionId:', sessionId)
// Progress tracking (session already created in API)
if (sessionId) {
progressTracker.updateStep(sessionId, 'init', 'active', 'Starting browser sessions...')
}
try {
// Ensure screenshots directory exists
@@ -39,8 +48,13 @@ export class EnhancedScreenshotService {
console.log(`\n🔄 Starting parallel capture of ${layoutsToCapture.length} layouts...`)
if (sessionId) {
progressTracker.updateStep(sessionId, 'init', 'completed', `Started ${layoutsToCapture.length} browser sessions`)
progressTracker.updateStep(sessionId, 'auth', 'active', 'Authenticating with TradingView...')
}
// Create parallel session promises for true dual-session approach
const sessionPromises = layoutsToCapture.map(async (layout) => {
const sessionPromises = layoutsToCapture.map(async (layout, index) => {
const layoutKey = layout.toLowerCase()
let layoutSession: TradingViewAutomation | null = null
@@ -71,6 +85,9 @@ export class EnhancedScreenshotService {
const isLoggedIn = await layoutSession.isLoggedIn()
if (!isLoggedIn) {
console.log(`🔐 Logging in to ${layout} session...`)
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'auth', 'active', `Logging into ${layout} session...`)
}
const loginSuccess = await layoutSession.smartLogin(config.credentials)
if (!loginSuccess) {
throw new Error(`Failed to login to ${layout} session`)
@@ -79,6 +96,12 @@ export class EnhancedScreenshotService {
console.log(`${layout} session already logged in`)
}
// Update auth progress when first session completes auth
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'auth', 'completed', 'TradingView authentication successful')
progressTracker.updateStep(sessionId, 'navigation', 'active', `Navigating to ${config.symbol} chart...`)
}
// Navigate directly to the specific layout URL with symbol and timeframe
const directUrl = `https://www.tradingview.com/chart/${layoutUrl}/?symbol=${config.symbol}&interval=${config.timeframe}`
console.log(`🌐 ${layout.toUpperCase()}: Navigating directly to ${directUrl}`)
@@ -132,6 +155,12 @@ export class EnhancedScreenshotService {
console.log(`${layout.toUpperCase()}: Successfully navigated to layout`)
// Update navigation progress when first session completes navigation
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'navigation', 'completed', 'Chart navigation successful')
progressTracker.updateStep(sessionId, 'loading', 'active', 'Loading chart data and indicators...')
}
// Progressive loading strategy: shorter initial wait, then chart-specific wait
console.log(`${layout.toUpperCase()}: Initial page stabilization (2s)...`)
await new Promise(resolve => setTimeout(resolve, 2000))
@@ -171,6 +200,12 @@ export class EnhancedScreenshotService {
await new Promise(resolve => setTimeout(resolve, 3000))
}
// Update loading progress when first session completes loading
if (sessionId && index === 0) {
progressTracker.updateStep(sessionId, 'loading', 'completed', 'Chart data loaded successfully')
progressTracker.updateStep(sessionId, 'capture', 'active', 'Capturing screenshots...')
}
// Take screenshot with better error handling
const filename = `${config.symbol}_${config.timeframe}_${layout}_${timestamp}.png`
console.log(`📸 Taking ${layout} screenshot: ${filename}`)
@@ -237,11 +272,27 @@ export class EnhancedScreenshotService {
}
})
if (sessionId) {
progressTracker.updateStep(sessionId, 'capture', 'completed', `Captured ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`)
}
console.log(`\n🎯 Parallel capture completed: ${screenshotFiles.length}/${layoutsToCapture.length} screenshots`)
return screenshotFiles
} catch (error) {
console.error('Enhanced parallel screenshot capture failed:', error)
if (sessionId) {
// Mark the current active step as error
const progress = progressTracker.getProgress(sessionId)
if (progress) {
const activeStep = progress.steps.find(step => step.status === 'active')
if (activeStep) {
progressTracker.updateStep(sessionId, activeStep.id, 'error', error instanceof Error ? error.message : 'Unknown error')
}
}
}
throw error
}
}

View File

@@ -0,0 +1,490 @@
import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js'
import fetch from 'cross-fetch'
export interface TriggerOrder {
orderId: string
inputMint: string
outputMint: string
makingAmount: string
takingAmount: string
targetPrice: number
side: 'BUY' | 'SELL'
orderType: 'STOP_LOSS' | 'TAKE_PROFIT' | 'LIMIT'
status: 'PENDING' | 'EXECUTED' | 'CANCELLED' | 'EXPIRED'
createdAt: number
executedAt?: number
txId?: string
requestId?: string
}
class JupiterTriggerService {
private connection: Connection
private keypair: Keypair | null = null
private activeOrders: TriggerOrder[] = []
// Token mint addresses
private tokens = {
SOL: 'So11111111111111111111111111111111111111112', // Wrapped SOL
USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
}
constructor() {
const rpcUrl = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
this.connection = new Connection(rpcUrl, 'confirmed')
this.initializeWallet()
}
private initializeWallet() {
try {
if (process.env.SOLANA_PRIVATE_KEY) {
const privateKeyArray = JSON.parse(process.env.SOLANA_PRIVATE_KEY)
this.keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyArray))
console.log('✅ Jupiter Trigger wallet initialized:', this.keypair.publicKey.toString())
} else {
console.warn('⚠️ No SOLANA_PRIVATE_KEY found for Jupiter Trigger')
}
} catch (error) {
console.error('❌ Failed to initialize Jupiter Trigger wallet:', error)
}
}
/**
* Create a stop loss order
* When current price drops to stopPrice, sell the token
*/
async createStopLossOrder(params: {
tokenSymbol: string
amount: number // Amount of tokens to sell
stopPrice: number // Price at which to trigger the sale
slippageBps?: number // Optional slippage (default 0 for exact execution)
expiredAt?: number // Optional expiry timestamp
}): Promise<{
success: boolean
orderId?: string
requestId?: string
transaction?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
const { tokenSymbol, amount, stopPrice, slippageBps = 0, expiredAt } = params
console.log('🛑 Creating stop loss order:', params)
// Determine mint addresses
const inputMint = tokenSymbol === 'SOL' ? this.tokens.SOL : this.tokens.USDC
const outputMint = tokenSymbol === 'SOL' ? this.tokens.USDC : this.tokens.SOL
// Calculate amounts
const makingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * 1_000_000_000).toString() // SOL has 9 decimals
: Math.floor(amount * 1_000_000).toString() // USDC has 6 decimals
const takingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * stopPrice * 1_000_000).toString() // Convert to USDC
: Math.floor(amount / stopPrice * 1_000_000_000).toString() // Convert to SOL
const orderParams: any = {
inputMint,
outputMint,
maker: this.keypair.publicKey.toString(),
payer: this.keypair.publicKey.toString(),
params: {
makingAmount,
takingAmount,
},
computeUnitPrice: "auto"
}
// Add optional parameters
if (slippageBps > 0) {
orderParams.params.slippageBps = slippageBps.toString()
}
if (expiredAt) {
orderParams.params.expiredAt = expiredAt.toString()
}
// Create the trigger order
const response = await fetch('https://api.jup.ag/trigger/v1/createOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(orderParams)
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Trigger API error: ${error.error || response.status}`)
}
const result = await response.json()
// Store the order locally
const order: TriggerOrder = {
orderId: result.order,
inputMint,
outputMint,
makingAmount,
takingAmount,
targetPrice: stopPrice,
side: 'SELL',
orderType: 'STOP_LOSS',
status: 'PENDING',
createdAt: Date.now(),
requestId: result.requestId
}
this.activeOrders.push(order)
console.log('✅ Stop loss order created:', result.order)
return {
success: true,
orderId: result.order,
requestId: result.requestId,
transaction: result.transaction
}
} catch (error: any) {
console.error('❌ Failed to create stop loss order:', error)
return { success: false, error: error.message }
}
}
/**
* Create a take profit order
* When current price rises to targetPrice, sell the token
*/
async createTakeProfitOrder(params: {
tokenSymbol: string
amount: number // Amount of tokens to sell
targetPrice: number // Price at which to trigger the sale
slippageBps?: number // Optional slippage (default 0 for exact execution)
expiredAt?: number // Optional expiry timestamp
}): Promise<{
success: boolean
orderId?: string
requestId?: string
transaction?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
const { tokenSymbol, amount, targetPrice, slippageBps = 0, expiredAt } = params
console.log('🎯 Creating take profit order:', params)
// Determine mint addresses
const inputMint = tokenSymbol === 'SOL' ? this.tokens.SOL : this.tokens.USDC
const outputMint = tokenSymbol === 'SOL' ? this.tokens.USDC : this.tokens.SOL
// Calculate amounts
const makingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * 1_000_000_000).toString() // SOL has 9 decimals
: Math.floor(amount * 1_000_000).toString() // USDC has 6 decimals
const takingAmount = tokenSymbol === 'SOL'
? Math.floor(amount * targetPrice * 1_000_000).toString() // Convert to USDC
: Math.floor(amount / targetPrice * 1_000_000_000).toString() // Convert to SOL
const orderParams: any = {
inputMint,
outputMint,
maker: this.keypair.publicKey.toString(),
payer: this.keypair.publicKey.toString(),
params: {
makingAmount,
takingAmount,
},
computeUnitPrice: "auto"
}
// Add optional parameters
if (slippageBps > 0) {
orderParams.params.slippageBps = slippageBps.toString()
}
if (expiredAt) {
orderParams.params.expiredAt = expiredAt.toString()
}
// Create the trigger order
const response = await fetch('https://api.jup.ag/trigger/v1/createOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(orderParams)
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Trigger API error: ${error.error || response.status}`)
}
const result = await response.json()
// Store the order locally
const order: TriggerOrder = {
orderId: result.order,
inputMint,
outputMint,
makingAmount,
takingAmount,
targetPrice: targetPrice,
side: 'SELL',
orderType: 'TAKE_PROFIT',
status: 'PENDING',
createdAt: Date.now(),
requestId: result.requestId
}
this.activeOrders.push(order)
console.log('✅ Take profit order created:', result.order)
return {
success: true,
orderId: result.order,
requestId: result.requestId,
transaction: result.transaction
}
} catch (error: any) {
console.error('❌ Failed to create take profit order:', error)
return { success: false, error: error.message }
}
}
/**
* Execute (sign and send) a trigger order transaction
*/
async executeOrder(transaction: string, requestId: string): Promise<{
success: boolean
txId?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
console.log('⚡ Executing trigger order transaction')
// Deserialize and sign transaction
const transactionBuf = Buffer.from(transaction, 'base64')
const versionedTransaction = VersionedTransaction.deserialize(transactionBuf)
versionedTransaction.sign([this.keypair])
// Send via Jupiter's execute endpoint
const response = await fetch('https://api.jup.ag/trigger/v1/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestId,
transaction: Buffer.from(versionedTransaction.serialize()).toString('base64')
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Execute API error: ${error.error || response.status}`)
}
const result = await response.json()
console.log('✅ Trigger order executed:', result.txId)
// Update local order status
const order = this.activeOrders.find(o => o.requestId === requestId)
if (order) {
order.status = 'PENDING'
order.txId = result.txId
}
return {
success: true,
txId: result.txId
}
} catch (error: any) {
console.error('❌ Failed to execute trigger order:', error)
return { success: false, error: error.message }
}
}
/**
* Cancel a trigger order
*/
async cancelOrder(orderId: string): Promise<{
success: boolean
txId?: string
error?: string
}> {
if (!this.keypair) {
return { success: false, error: 'Wallet not initialized' }
}
try {
console.log('❌ Cancelling trigger order:', orderId)
const response = await fetch('https://api.jup.ag/trigger/v1/cancelOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
order: orderId,
owner: this.keypair.publicKey.toString(),
computeUnitPrice: "auto"
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(`Cancel API error: ${error.error || response.status}`)
}
const result = await response.json()
// Sign and send the cancel transaction
const transactionBuf = Buffer.from(result.transaction, 'base64')
const versionedTransaction = VersionedTransaction.deserialize(transactionBuf)
versionedTransaction.sign([this.keypair])
const txId = await this.connection.sendTransaction(versionedTransaction)
const confirmation = await this.connection.confirmTransaction(txId, 'confirmed')
if (confirmation.value.err) {
throw new Error(`Cancel transaction failed: ${confirmation.value.err}`)
}
// Update local order status
const order = this.activeOrders.find(o => o.orderId === orderId)
if (order) {
order.status = 'CANCELLED'
}
console.log('✅ Trigger order cancelled:', txId)
return {
success: true,
txId
}
} catch (error: any) {
console.error('❌ Failed to cancel trigger order:', error)
return { success: false, error: error.message }
}
}
/**
* Get active trigger orders for the wallet
*/
async getTriggerOrders(): Promise<TriggerOrder[]> {
if (!this.keypair) {
return []
}
try {
const response = await fetch(`https://api.jup.ag/trigger/v1/getTriggerOrders?wallet=${this.keypair.publicKey.toString()}&active=true`)
if (!response.ok) {
throw new Error(`Get orders API error: ${response.status}`)
}
const result = await response.json()
console.log('📊 Retrieved trigger orders:', result.orders?.length || 0)
return result.orders || []
} catch (error: any) {
console.error('❌ Failed to get trigger orders:', error)
return this.activeOrders // Fallback to local orders
}
}
/**
* Create both stop loss and take profit orders for a trade
*/
async createTradingOrders(params: {
tokenSymbol: string
amount: number
stopLoss?: number
takeProfit?: number
slippageBps?: number
expiredAt?: number
}): Promise<{
success: boolean
stopLossOrder?: string
takeProfitOrder?: string
transactions?: string[]
error?: string
}> {
const { tokenSymbol, amount, stopLoss, takeProfit, slippageBps, expiredAt } = params
const results: any = { success: true, transactions: [] }
try {
// Create stop loss order
if (stopLoss) {
const slResult = await this.createStopLossOrder({
tokenSymbol,
amount,
stopPrice: stopLoss,
slippageBps,
expiredAt
})
if (slResult.success) {
results.stopLossOrder = slResult.orderId
results.transactions.push(slResult.transaction)
} else {
console.warn('⚠️ Failed to create stop loss order:', slResult.error)
}
}
// Create take profit order
if (takeProfit) {
const tpResult = await this.createTakeProfitOrder({
tokenSymbol,
amount,
targetPrice: takeProfit,
slippageBps,
expiredAt
})
if (tpResult.success) {
results.takeProfitOrder = tpResult.orderId
results.transactions.push(tpResult.transaction)
} else {
console.warn('⚠️ Failed to create take profit order:', tpResult.error)
}
}
return results
} catch (error: any) {
console.error('❌ Failed to create trading orders:', error)
return { success: false, error: error.message }
}
}
getLocalOrders(): TriggerOrder[] {
return this.activeOrders
}
isConfigured(): boolean {
return this.keypair !== null
}
}
export const jupiterTriggerService = new JupiterTriggerService()
export default JupiterTriggerService

115
lib/progress-tracker.ts Normal file
View File

@@ -0,0 +1,115 @@
import { EventEmitter } from 'events'
export type ProgressStatus = 'pending' | 'active' | 'completed' | 'error'
export interface ProgressStep {
id: string
title: string
description: string
status: ProgressStatus
startTime?: number
endTime?: number
details?: string
}
export interface AnalysisProgress {
sessionId: string
currentStep: number
totalSteps: number
steps: ProgressStep[]
timeframeProgress?: {
current: number
total: number
currentTimeframe?: string
}
}
class ProgressTracker extends EventEmitter {
private sessions: Map<string, AnalysisProgress> = new Map()
createSession(sessionId: string, steps: ProgressStep[]): AnalysisProgress {
const progress: AnalysisProgress = {
sessionId,
currentStep: 0,
totalSteps: steps.length,
steps: steps.map(step => ({ ...step, status: 'pending' }))
}
this.sessions.set(sessionId, progress)
this.emit(`progress:${sessionId}`, progress)
return progress
}
updateStep(sessionId: string, stepId: string, status: ProgressStatus, details?: string): void {
console.log(`🔍 Progress Update: ${sessionId} -> ${stepId} -> ${status}${details ? ` (${details})` : ''}`)
const progress = this.sessions.get(sessionId)
if (!progress) {
console.log(`🔍 Warning: No session found for ${sessionId}`)
return
}
const updatedSteps = progress.steps.map(step => {
if (step.id === stepId) {
const updatedStep = {
...step,
status,
details: details || step.details
}
if (status === 'active' && !step.startTime) {
updatedStep.startTime = Date.now()
} else if ((status === 'completed' || status === 'error') && !step.endTime) {
updatedStep.endTime = Date.now()
}
return updatedStep
}
return step
})
const currentStepIndex = updatedSteps.findIndex(step => step.status === 'active')
const updatedProgress: AnalysisProgress = {
...progress,
steps: updatedSteps,
currentStep: currentStepIndex >= 0 ? currentStepIndex + 1 : progress.currentStep
}
this.sessions.set(sessionId, updatedProgress)
console.log(`🔍 Emitting progress event for ${sessionId}, currentStep: ${updatedProgress.currentStep}`)
this.emit(`progress:${sessionId}`, updatedProgress)
}
updateTimeframeProgress(sessionId: string, current: number, total: number, currentTimeframe?: string): void {
const progress = this.sessions.get(sessionId)
if (!progress) return
const updatedProgress: AnalysisProgress = {
...progress,
timeframeProgress: {
current,
total,
currentTimeframe
}
}
this.sessions.set(sessionId, updatedProgress)
this.emit(`progress:${sessionId}`, updatedProgress)
}
getProgress(sessionId: string): AnalysisProgress | undefined {
return this.sessions.get(sessionId)
}
deleteSession(sessionId: string): void {
this.sessions.delete(sessionId)
this.emit(`progress:${sessionId}:complete`)
}
// Get all active sessions (for debugging)
getActiveSessions(): string[] {
return Array.from(this.sessions.keys())
}
}
export const progressTracker = new ProgressTracker()