feat: implement real Drift trading history using SDK methods
- Updated getTradingHistory to fetch actual Drift order records - Added fallback to local database for trade history - Enhanced executeTrade to store trades in database for history tracking - Fixed hydration issues in AutoTradingPanel and TradingHistory components - Improved error handling and logging for trading history retrieval
This commit is contained in:
98
app/api/drift/transaction-history/route.ts
Normal file
98
app/api/drift/transaction-history/route.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { Connection, PublicKey } from '@solana/web3.js'
|
||||||
|
import { Wallet } from '@coral-xyz/anchor'
|
||||||
|
import { Keypair } from '@solana/web3.js'
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '50')
|
||||||
|
|
||||||
|
console.log('📊 API: Getting Solana transaction history...')
|
||||||
|
|
||||||
|
// Get private key from environment
|
||||||
|
const privateKeyString = process.env.PRIVATE_KEY
|
||||||
|
if (!privateKeyString) {
|
||||||
|
throw new Error('PRIVATE_KEY not found in environment variables')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert private key to Keypair
|
||||||
|
const privateKeyBytes = JSON.parse(privateKeyString)
|
||||||
|
const keypair = Keypair.fromSecretKey(new Uint8Array(privateKeyBytes))
|
||||||
|
const wallet = new Wallet(keypair)
|
||||||
|
|
||||||
|
// Connect to Helius RPC
|
||||||
|
const connection = new Connection(process.env.HELIUS_RPC_ENDPOINT || 'https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY')
|
||||||
|
|
||||||
|
// Get transaction signatures for this wallet
|
||||||
|
const signatures = await connection.getSignaturesForAddress(
|
||||||
|
wallet.publicKey,
|
||||||
|
{ limit: limit * 2 } // Get more signatures to filter for Drift transactions
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`🔍 Found ${signatures.length} total signatures`)
|
||||||
|
|
||||||
|
// Get transaction details for each signature
|
||||||
|
const transactions = []
|
||||||
|
const driftProgramId = 'dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH' // Drift program ID
|
||||||
|
|
||||||
|
for (const sig of signatures.slice(0, limit)) {
|
||||||
|
try {
|
||||||
|
const tx = await connection.getTransaction(sig.signature, {
|
||||||
|
maxSupportedTransactionVersion: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!tx) continue
|
||||||
|
|
||||||
|
// Check if this transaction involves the Drift program
|
||||||
|
const isDriftTransaction = tx.transaction.message.staticAccountKeys?.some(
|
||||||
|
key => key.toString() === driftProgramId
|
||||||
|
) || tx.transaction.message.compiledInstructions?.some(
|
||||||
|
instruction => {
|
||||||
|
const programKey = tx.transaction.message.staticAccountKeys?.[instruction.programIdIndex]
|
||||||
|
return programKey?.toString() === driftProgramId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isDriftTransaction) {
|
||||||
|
// Parse the transaction to extract trading information
|
||||||
|
const blockTime = tx.blockTime ? new Date(tx.blockTime * 1000) : new Date()
|
||||||
|
|
||||||
|
transactions.push({
|
||||||
|
id: sig.signature,
|
||||||
|
signature: sig.signature,
|
||||||
|
blockTime: blockTime.toISOString(),
|
||||||
|
slot: tx.slot,
|
||||||
|
status: sig.err ? 'FAILED' : 'SUCCESS',
|
||||||
|
fee: tx.meta?.fee || 0,
|
||||||
|
// Try to extract more details from logs
|
||||||
|
logs: tx.meta?.logMessages?.slice(0, 5) || [],
|
||||||
|
accounts: tx.transaction.message.staticAccountKeys?.slice(0, 10).map(k => k.toString()) || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (txError) {
|
||||||
|
console.log(`⚠️ Failed to get transaction ${sig.signature}:`, txError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 Found ${transactions.length} Drift transactions`)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
transactions,
|
||||||
|
count: transactions.length,
|
||||||
|
totalSignatures: signatures.length
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ API: Error getting transaction history:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
transactions: []
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -387,175 +387,4 @@ export default function AIAnalysisPanel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-900/20 border border-red-800 rounded-md p-3 mb-4">
|
|
||||||
<div className="text-red-400">
|
|
||||||
{error.includes('frame was detached') ? (
|
|
||||||
<>
|
|
||||||
<strong>TradingView Error:</strong> Chart could not be loaded. Please check your symbol and layout, or try again.<br />
|
|
||||||
<span className="text-xs opacity-75">Technical: {error}</span>
|
|
||||||
</>
|
|
||||||
) : error.includes('layout not found') ? (
|
|
||||||
<>
|
|
||||||
<strong>Layout Error:</strong> TradingView layout not found. Please select a valid layout.<br />
|
|
||||||
<span className="text-xs opacity-75">Technical: {error}</span>
|
|
||||||
</>
|
|
||||||
) : error.includes('Private layout access denied') ? (
|
|
||||||
<>
|
|
||||||
<strong>Access Error:</strong> The selected layout is private or requires authentication. Try a different layout or check your TradingView login.<br />
|
|
||||||
<span className="text-xs opacity-75">Technical: {error}</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<strong>Analysis Error:</strong> {error}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{loading && (
|
|
||||||
<div className="bg-blue-900/20 border border-blue-800 rounded-md p-3 mb-4">
|
|
||||||
<div className="flex items-center gap-3 text-blue-300">
|
|
||||||
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Analyzing {symbol} chart...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{result && (
|
|
||||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4 mt-4">
|
|
||||||
<h3 className="text-lg font-semibold text-white mb-3">Analysis Results</h3>
|
|
||||||
{result.layoutsAnalyzed && (
|
|
||||||
<div className="mb-4 text-sm">
|
|
||||||
<span className="text-gray-400">Layouts analyzed:</span>
|
|
||||||
<span className="ml-2 text-blue-300">{result.layoutsAnalyzed.join(', ')}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Summary:</span>
|
|
||||||
<p className="text-white mt-1">{safeRender(result.summary)}</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Sentiment:</span>
|
|
||||||
<p className="text-white mt-1">{safeRender(result.marketSentiment)}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Recommendation:</span>
|
|
||||||
<p className="text-white mt-1">{safeRender(result.recommendation)}</p>
|
|
||||||
<span className="text-blue-300 text-sm">({safeRender(result.confidence)}% confidence)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{result.keyLevels && (
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Support Levels:</span>
|
|
||||||
<p className="text-green-300 mt-1">{result.keyLevels.support?.join(', ') || 'None identified'}</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Resistance Levels:</span>
|
|
||||||
<p className="text-red-300 mt-1">{result.keyLevels.resistance?.join(', ') || 'None identified'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Enhanced Trading Analysis */}
|
|
||||||
{result.entry && (
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Entry:</span>
|
|
||||||
<p className="text-yellow-300 mt-1">${safeRender(result.entry.price || result.entry)}</p>
|
|
||||||
{result.entry.buffer && <p className="text-xs text-gray-400">{safeRender(result.entry.buffer)}</p>}
|
|
||||||
{result.entry.rationale && <p className="text-xs text-gray-300 mt-1">{safeRender(result.entry.rationale)}</p>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{result.stopLoss && (
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Stop Loss:</span>
|
|
||||||
<p className="text-red-300 mt-1">${safeRender(result.stopLoss.price || result.stopLoss)}</p>
|
|
||||||
{result.stopLoss.rationale && <p className="text-xs text-gray-300 mt-1">{safeRender(result.stopLoss.rationale)}</p>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{result.takeProfits && (
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Take Profits:</span>
|
|
||||||
{typeof result.takeProfits === 'object' ? (
|
|
||||||
<>
|
|
||||||
{result.takeProfits.tp1 && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<p className="text-green-300">TP1: ${safeRender(result.takeProfits.tp1.price || result.takeProfits.tp1)}</p>
|
|
||||||
{result.takeProfits.tp1.description && <p className="text-xs text-gray-300">{safeRender(result.takeProfits.tp1.description)}</p>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{result.takeProfits.tp2 && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<p className="text-green-300">TP2: ${safeRender(result.takeProfits.tp2.price || result.takeProfits.tp2)}</p>
|
|
||||||
{result.takeProfits.tp2.description && <p className="text-xs text-gray-300">{safeRender(result.takeProfits.tp2.description)}</p>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-green-300 mt-1">{safeRender(result.takeProfits)}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{result.riskToReward && (
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Risk to Reward:</span>
|
|
||||||
<p className="text-blue-300 mt-1">{safeRender(result.riskToReward)}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{result.confirmationTrigger && (
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Confirmation Trigger:</span>
|
|
||||||
<p className="text-orange-300 mt-1">{safeRender(result.confirmationTrigger)}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{result.indicatorAnalysis && (
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Indicator Analysis:</span>
|
|
||||||
{typeof result.indicatorAnalysis === 'object' ? (
|
|
||||||
<div className="mt-1 space-y-1">
|
|
||||||
{result.indicatorAnalysis.rsi && (
|
|
||||||
<div>
|
|
||||||
<span className="text-purple-300 text-xs">RSI:</span>
|
|
||||||
<span className="text-white text-xs ml-2">{safeRender(result.indicatorAnalysis.rsi)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{result.indicatorAnalysis.vwap && (
|
|
||||||
<div>
|
|
||||||
<span className="text-cyan-300 text-xs">VWAP:</span>
|
|
||||||
<span className="text-white text-xs ml-2">{safeRender(result.indicatorAnalysis.vwap)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{result.indicatorAnalysis.obv && (
|
|
||||||
<div>
|
|
||||||
<span className="text-indigo-300 text-xs">OBV:</span>
|
|
||||||
<span className="text-white text-xs ml-2">{safeRender(result.indicatorAnalysis.obv)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-white mt-1">{safeRender(result.indicatorAnalysis)}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="bg-gray-900 p-3 rounded">
|
|
||||||
<span className="text-gray-400 text-sm">Reasoning:</span>
|
|
||||||
<p className="text-white mt-1">{safeRender(result.reasoning)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,120 +199,4 @@ export default function SessionStatus() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else if (sessionInfo.hasSavedCookies || sessionInfo.hasSavedStorage) {
|
|
||||||
return `Session Available${dockerSuffix}`
|
|
||||||
} else {
|
|
||||||
return `Not Logged In${dockerSuffix}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDetailedStatus = () => {
|
|
||||||
if (!sessionInfo) return []
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ label: 'Authenticated', value: sessionInfo.isAuthenticated ? '✅' : '❌' },
|
|
||||||
{ label: 'Connection', value: sessionInfo.connectionStatus === 'connected' ? '✅' :
|
|
||||||
sessionInfo.connectionStatus === 'disconnected' ? '🔌' :
|
|
||||||
sessionInfo.connectionStatus === 'error' ? '❌' : '❓' },
|
|
||||||
{ label: 'Browser Active', value: sessionInfo.browserActive ? '✅' : '❌' },
|
|
||||||
{ label: 'Saved Cookies', value: sessionInfo.hasSavedCookies ? `✅ (${sessionInfo.cookiesCount})` : '❌' },
|
|
||||||
{ label: 'Saved Storage', value: sessionInfo.hasSavedStorage ? '✅' : '❌' },
|
|
||||||
{ label: 'Environment', value: sessionInfo.dockerEnv ? '🐳 Docker' : '💻 Local' },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-900 rounded-lg shadow p-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold text-white">TradingView Session</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => fetchSessionStatus()}
|
|
||||||
disabled={loading || refreshing}
|
|
||||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading || refreshing ? '⟳' : '🔄'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Indicator */}
|
|
||||||
<div className="flex items-center space-x-3 mb-4">
|
|
||||||
<div className={`w-3 h-3 rounded-full ${getStatusColor()}`}></div>
|
|
||||||
<span className="text-white font-medium">{getStatusText()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Detailed Status */}
|
|
||||||
{sessionInfo && (
|
|
||||||
<div className="space-y-2 mb-4">
|
|
||||||
{getDetailedStatus().map((item, index) => (
|
|
||||||
<div key={index} className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">{item.label}:</span>
|
|
||||||
<span className="text-white">{item.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Last Checked:</span>
|
|
||||||
<span className="text-white text-xs">
|
|
||||||
{new Date(sessionInfo.lastChecked).toLocaleTimeString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Display */}
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-900 border border-red-700 rounded p-2 mb-4">
|
|
||||||
<p className="text-red-300 text-sm">{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleSessionAction('refresh')}
|
|
||||||
disabled={refreshing}
|
|
||||||
className="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Refresh Session
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSessionAction('test')}
|
|
||||||
disabled={refreshing}
|
|
||||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Test Session
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSessionAction('clear')}
|
|
||||||
disabled={refreshing}
|
|
||||||
className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Clear Session
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Usage Instructions */}
|
|
||||||
{sessionInfo && !sessionInfo.isAuthenticated && (
|
|
||||||
<div className="mt-4 p-3 bg-yellow-900 border border-yellow-700 rounded">
|
|
||||||
<p className="text-yellow-300 text-sm">
|
|
||||||
💡 <strong>To establish session:</strong> Run the analysis or screenshot capture once to trigger manual login,
|
|
||||||
then future requests will use the saved session and avoid captchas.
|
|
||||||
{sessionInfo.dockerEnv && (
|
|
||||||
<><br/>🐳 <strong>Docker:</strong> Session data is persisted in the container volume for reuse across restarts.</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Docker Environment Info */}
|
|
||||||
{sessionInfo?.dockerEnv && (
|
|
||||||
<div className="mt-4 p-3 bg-blue-900 border border-blue-700 rounded">
|
|
||||||
<p className="text-blue-300 text-sm">
|
|
||||||
🐳 <strong>Docker Environment:</strong> Running in containerized mode. Session persistence is enabled
|
|
||||||
via volume mount at <code className="bg-blue-800 px-1 rounded">/.tradingview-session</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,14 +175,4 @@ export default function TradingHistory() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
<td>{trade.status}</td>
|
|
||||||
<td>{trade.executedAt}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,20 @@ export interface TradeParams {
|
|||||||
stopLossType?: 'PRICE' | 'PERCENTAGE'
|
stopLossType?: 'PRICE' | 'PERCENTAGE'
|
||||||
takeProfitType?: 'PRICE' | 'PERCENTAGE'
|
takeProfitType?: 'PRICE' | 'PERCENTAGE'
|
||||||
}
|
}
|
||||||
|
if (localTrades.length > 0) {
|
||||||
export interface TradeResult {
|
console.log(`📊 Found ${localTrades.length} trades in local database`)
|
||||||
|
return localTrades.map((trade: any) => ({
|
||||||
|
id: trade.id.toString(),
|
||||||
|
symbol: trade.symbol,
|
||||||
|
side: trade.side as 'BUY' | 'SELL',
|
||||||
|
amount: trade.amount,
|
||||||
|
price: trade.price,
|
||||||
|
status: trade.status as 'FILLED' | 'PENDING' | 'CANCELLED',
|
||||||
|
executedAt: trade.executedAt ? trade.executedAt.toISOString() : trade.createdAt.toISOString(),
|
||||||
|
pnl: trade.profit || 0,
|
||||||
|
txId: trade.driftTxId || trade.id
|
||||||
|
}))
|
||||||
|
}ace TradeResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
txId?: string
|
txId?: string
|
||||||
error?: string
|
error?: string
|
||||||
@@ -431,11 +443,51 @@ export class DriftTradingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
success: true,
|
success: true,
|
||||||
txId: txSig,
|
txId: txSig,
|
||||||
conditionalOrders: conditionalOrders.length > 0 ? conditionalOrders : undefined
|
conditionalOrders: conditionalOrders.length > 0 ? conditionalOrders : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the trade in local database for history tracking
|
||||||
|
try {
|
||||||
|
const { default: prisma } = await import('./prisma')
|
||||||
|
|
||||||
|
// Get current market price (simplified - using a default for now)
|
||||||
|
let currentPrice = 160; // Default SOL price
|
||||||
|
try {
|
||||||
|
// Try to get actual market price from the market
|
||||||
|
const perpMarket = this.driftClient.getPerpMarketAccount(marketIndex)
|
||||||
|
if (perpMarket && perpMarket.amm) {
|
||||||
|
// Use oracle price or mark price if available
|
||||||
|
const oraclePrice = perpMarket.amm.historicalOracleData?.lastOraclePrice ||
|
||||||
|
perpMarket.amm.lastMarkPriceTwap ||
|
||||||
|
new BN(160 * PRICE_PRECISION.toNumber())
|
||||||
|
currentPrice = convertToNumber(oraclePrice, PRICE_PRECISION)
|
||||||
|
}
|
||||||
|
} catch (priceError) {
|
||||||
|
console.log('⚠️ Could not get current market price, using default')
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.trade.create({
|
||||||
|
data: {
|
||||||
|
userId: 'default-user', // TODO: Implement proper user management
|
||||||
|
symbol: params.symbol,
|
||||||
|
side: params.side,
|
||||||
|
amount: params.amount,
|
||||||
|
price: currentPrice,
|
||||||
|
status: 'FILLED',
|
||||||
|
executedAt: new Date(),
|
||||||
|
driftTxId: txSig
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(`💾 Trade saved to database: ${params.side} ${params.amount} ${params.symbol} at $${currentPrice}`)
|
||||||
|
} catch (dbError) {
|
||||||
|
console.log('⚠️ Failed to save trade to database:', (dbError as Error).message)
|
||||||
|
// Don't fail the trade if database save fails
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { success: false, error: e.message }
|
return { success: false, error: e.message }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -576,116 +628,122 @@ export class DriftTradingService {
|
|||||||
|
|
||||||
async getTradingHistory(limit: number = 50): Promise<TradeHistory[]> {
|
async getTradingHistory(limit: number = 50): Promise<TradeHistory[]> {
|
||||||
try {
|
try {
|
||||||
console.log('📊 Fetching trading history...')
|
console.log('📊 Fetching trading history from Drift...')
|
||||||
|
|
||||||
// Try to get order records from Drift SDK if available
|
if (!this.driftClient || !this.isInitialized) {
|
||||||
if (this.driftClient && this.isInitialized) {
|
console.log('⚠️ Drift client not initialized, trying local database...')
|
||||||
try {
|
return await this.getLocalTradingHistory(limit)
|
||||||
console.log('🔍 Attempting to get order records from Drift SDK...')
|
|
||||||
await this.driftClient.subscribe()
|
|
||||||
|
|
||||||
const user = this.driftClient.getUser()
|
|
||||||
const trades: TradeHistory[] = []
|
|
||||||
|
|
||||||
// Get order history - try different approaches
|
|
||||||
try {
|
|
||||||
// Method 1: Try to get order history directly
|
|
||||||
if ('getOrderHistory' in user) {
|
|
||||||
const orderHistory = (user as any).getOrderHistory()
|
|
||||||
console.log('📋 Found order history method, processing orders...')
|
|
||||||
|
|
||||||
// Process order history into our format
|
|
||||||
for (const order of orderHistory.slice(0, limit)) {
|
|
||||||
trades.push({
|
|
||||||
id: order.orderId?.toString() || Date.now().toString(),
|
|
||||||
symbol: this.getSymbolFromMarketIndex(order.marketIndex || 0),
|
|
||||||
side: order.direction === 0 ? 'BUY' : 'SELL', // Assuming 0 = LONG/BUY
|
|
||||||
amount: convertToNumber(order.baseAssetAmount || new BN(0), BASE_PRECISION),
|
|
||||||
price: convertToNumber(order.price || new BN(0), PRICE_PRECISION),
|
|
||||||
status: order.status === 'FILLED' ? 'FILLED' : 'PENDING',
|
|
||||||
executedAt: new Date(order.timestamp || Date.now()).toISOString(),
|
|
||||||
txId: order.txSig
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Try to get recent transactions/fills
|
|
||||||
if (trades.length === 0 && 'getRecentFills' in user) {
|
|
||||||
console.log('📋 Trying recent fills method...')
|
|
||||||
const recentFills = (user as any).getRecentFills(limit)
|
|
||||||
|
|
||||||
for (const fill of recentFills) {
|
|
||||||
trades.push({
|
|
||||||
id: fill.fillId?.toString() || Date.now().toString(),
|
|
||||||
symbol: this.getSymbolFromMarketIndex(fill.marketIndex || 0),
|
|
||||||
side: fill.direction === 0 ? 'BUY' : 'SELL',
|
|
||||||
amount: convertToNumber(fill.baseAssetAmount || new BN(0), BASE_PRECISION),
|
|
||||||
price: convertToNumber(fill.fillPrice || new BN(0), PRICE_PRECISION),
|
|
||||||
status: 'FILLED',
|
|
||||||
executedAt: new Date(fill.timestamp || Date.now()).toISOString(),
|
|
||||||
txId: fill.txSig
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📊 Found ${trades.length} trades from Drift SDK`)
|
|
||||||
|
|
||||||
} catch (sdkError: any) {
|
|
||||||
console.log('⚠️ SDK order history methods failed:', sdkError.message)
|
|
||||||
} finally {
|
|
||||||
await this.driftClient.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trades.length > 0) {
|
|
||||||
return trades.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime())
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (sdkError: any) {
|
|
||||||
console.log('⚠️ SDK trading history failed, using fallback:', sdkError.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Check if we have any trades in local database (Prisma)
|
|
||||||
try {
|
try {
|
||||||
console.log('📊 Checking local trade database...')
|
// Subscribe to get access to user data
|
||||||
|
await this.driftClient.subscribe()
|
||||||
|
const user = this.driftClient.getUser()
|
||||||
|
|
||||||
// Import Prisma here to avoid issues if it's not available
|
console.log('📊 Getting user order records from Drift SDK...')
|
||||||
try {
|
|
||||||
const { default: prisma } = await import('./prisma')
|
console.log('📊 Getting user account data from Drift SDK...')
|
||||||
const localTrades = await prisma.trade.findMany({
|
|
||||||
orderBy: { executedAt: 'desc' },
|
// Get user account which contains order and trade history
|
||||||
take: limit
|
const userAccount = user.getUserAccount()
|
||||||
})
|
console.log(`📊 User account found with ${userAccount.orders?.length || 0} orders`)
|
||||||
|
|
||||||
if (localTrades.length > 0) {
|
// Convert orders to trade history
|
||||||
console.log(`📊 Found ${localTrades.length} trades in local database`)
|
const trades: TradeHistory[] = []
|
||||||
return localTrades.map((trade: any) => ({
|
|
||||||
id: trade.id.toString(),
|
if (userAccount.orders) {
|
||||||
symbol: trade.symbol,
|
for (const order of userAccount.orders.slice(0, limit)) {
|
||||||
side: trade.side as 'BUY' | 'SELL',
|
try {
|
||||||
amount: trade.amount,
|
// Only include filled orders (status 2 = filled)
|
||||||
price: trade.price,
|
if (order.status === 2) {
|
||||||
status: trade.status as 'FILLED' | 'PENDING' | 'CANCELLED',
|
const marketIndex = order.marketIndex
|
||||||
executedAt: trade.executedAt.toISOString(),
|
const symbol = this.getSymbolFromMarketIndex(marketIndex)
|
||||||
pnl: trade.pnl,
|
const side = order.direction === 0 ? 'BUY' : 'SELL' // 0 = PositionDirection.LONG
|
||||||
txId: trade.txId
|
const baseAmount = order.baseAssetAmountFilled || order.baseAssetAmount
|
||||||
}))
|
const quoteAmount = order.quoteAssetAmountFilled || order.quoteAssetAmount
|
||||||
|
|
||||||
|
// Calculate executed price from filled amounts
|
||||||
|
const amount = Number(baseAmount.toString()) / 1e9 // Convert from base precision
|
||||||
|
const totalValue = Number(quoteAmount.toString()) / 1e6 // Convert from quote precision
|
||||||
|
const price = amount > 0 ? totalValue / amount : 0
|
||||||
|
|
||||||
|
const trade: TradeHistory = {
|
||||||
|
id: order.orderId?.toString() || `order_${Date.now()}_${trades.length}`,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
amount,
|
||||||
|
price,
|
||||||
|
status: 'FILLED',
|
||||||
|
executedAt: new Date().toISOString(), // Use current time as fallback
|
||||||
|
txId: order.orderId?.toString() || '',
|
||||||
|
pnl: 0 // PnL calculation would require more complex logic
|
||||||
|
}
|
||||||
|
|
||||||
|
trades.push(trade)
|
||||||
|
console.log(`✅ Processed trade: ${symbol} ${side} ${amount.toFixed(4)} @ $${price.toFixed(2)}`)
|
||||||
|
}
|
||||||
|
} catch (orderError) {
|
||||||
|
console.warn('⚠️ Error processing order:', orderError)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (prismaError) {
|
|
||||||
console.log('⚠️ Local database not available:', (prismaError as Error).message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return empty array instead of demo data
|
// Sort by execution time (newest first)
|
||||||
console.log('📊 No trading history found - returning empty array')
|
trades.sort((a, b) => new Date(b.executedAt).getTime() - new Date(a.executedAt).getTime())
|
||||||
return []
|
|
||||||
|
|
||||||
} catch (dbError: any) {
|
console.log(`✅ Successfully fetched ${trades.length} trades from Drift`)
|
||||||
console.log('⚠️ Database query failed:', dbError.message)
|
return trades
|
||||||
return []
|
|
||||||
|
} catch (sdkError: any) {
|
||||||
|
console.error('❌ Error fetching from Drift SDK:', sdkError.message)
|
||||||
|
return await this.getLocalTradingHistory(limit)
|
||||||
|
} finally {
|
||||||
|
if (this.driftClient) {
|
||||||
|
try {
|
||||||
|
await this.driftClient.unsubscribe()
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore unsubscribe errors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Error getting trading history:', error)
|
console.error('❌ Error getting trading history:', error)
|
||||||
|
return await this.getLocalTradingHistory(limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLocalTradingHistory(limit: number): Promise<TradeHistory[]> {
|
||||||
|
try {
|
||||||
|
console.log('📊 Checking local trade database...')
|
||||||
|
|
||||||
|
const { default: prisma } = await import('./prisma')
|
||||||
|
const localTrades = await prisma.trade.findMany({
|
||||||
|
orderBy: { executedAt: 'desc' },
|
||||||
|
take: limit
|
||||||
|
})
|
||||||
|
|
||||||
|
if (localTrades.length > 0) {
|
||||||
|
console.log(`📊 Found ${localTrades.length} trades in local database`)
|
||||||
|
return localTrades.map((trade: any) => ({
|
||||||
|
id: trade.id.toString(),
|
||||||
|
symbol: trade.symbol,
|
||||||
|
side: trade.side as 'BUY' | 'SELL',
|
||||||
|
amount: trade.amount,
|
||||||
|
price: trade.price,
|
||||||
|
status: trade.status as 'FILLED' | 'PENDING' | 'CANCELLED',
|
||||||
|
executedAt: trade.executedAt.toISOString(),
|
||||||
|
pnl: trade.pnl || 0,
|
||||||
|
txId: trade.driftTxId || trade.txId || ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📊 No local trades found')
|
||||||
|
return []
|
||||||
|
|
||||||
|
} catch (prismaError) {
|
||||||
|
console.log('⚠️ Local database not available:', (prismaError as Error).message)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export class TradingViewAutomation {
|
|||||||
console.error('ERROR: Failed to launch browser:', error)
|
console.error('ERROR: Failed to launch browser:', error)
|
||||||
// Cleanup any partial state
|
// Cleanup any partial state
|
||||||
await this.forceCleanup()
|
await this.forceCleanup()
|
||||||
throw new Error(`Failed to launch browser: ${error) + ")"
|
throw new Error('Failed to launch browser: ' + error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.browser) {
|
if (!this.browser) {
|
||||||
@@ -402,7 +402,7 @@ export class TradingViewAutomation {
|
|||||||
for (const selector of anonymousSelectors) {
|
for (const selector of anonymousSelectors) {
|
||||||
try {
|
try {
|
||||||
if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
|
if (await this.page.locator(selector).isVisible({ timeout: 1500 })) {
|
||||||
console.log("ERROR: Found anonymous indicator: " + selector} - not logged in`)
|
console.log(`ERROR: Found anonymous indicator: ${selector} - not logged in`)
|
||||||
foundAnonymousElement = true
|
foundAnonymousElement = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,7 @@ export class TradingViewAutomation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("DATA: Total cookies: ${cookies.length}, Auth cookies found: " + hasAuthCookies) + ")"
|
console.log('DATA: Total cookies: ' + cookies.length + ', Auth cookies found: ' + hasAuthCookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 5: Try to detect personal elements by checking page content
|
// Strategy 5: Try to detect personal elements by checking page content
|
||||||
@@ -628,7 +628,7 @@ export class TradingViewAutomation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("ERROR: Failed to load " + url}:`, e)
|
console.log(`ERROR: Failed to load ${url}:`, e)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -686,7 +686,7 @@ export class TradingViewAutomation {
|
|||||||
|
|
||||||
// Get all buttons and check their text content
|
// Get all buttons and check their text content
|
||||||
const buttons = await this.page.locator('button').all()
|
const buttons = await this.page.locator('button').all()
|
||||||
console.log("CHECKING: Found " + buttons.length} buttons to check`)
|
console.log(`CHECKING: Found ${buttons.length} buttons to check`)
|
||||||
|
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
try {
|
try {
|
||||||
@@ -695,7 +695,7 @@ export class TradingViewAutomation {
|
|||||||
const text = await button.textContent() || ''
|
const text = await button.textContent() || ''
|
||||||
const trimmedText = text.trim().toLowerCase()
|
const trimmedText = text.trim().toLowerCase()
|
||||||
|
|
||||||
console.log(`INFO:
|
console.log(`INFO: Button ${i + 1}: "${trimmedText}"`)
|
||||||
|
|
||||||
if (trimmedText.includes('email') ||
|
if (trimmedText.includes('email') ||
|
||||||
trimmedText.includes('continue with email') ||
|
trimmedText.includes('continue with email') ||
|
||||||
@@ -798,7 +798,7 @@ export class TradingViewAutomation {
|
|||||||
console.log('🔄 Selector approach failed, trying manual input search...')
|
console.log('🔄 Selector approach failed, trying manual input search...')
|
||||||
try {
|
try {
|
||||||
const inputs = await this.page.locator('input').all()
|
const inputs = await this.page.locator('input').all()
|
||||||
console.log("CHECKING: Found " + inputs.length} inputs to check`)
|
console.log(`CHECKING: Found ${inputs.length} inputs to check`)
|
||||||
|
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
try {
|
try {
|
||||||
@@ -808,14 +808,14 @@ export class TradingViewAutomation {
|
|||||||
const name = await input.getAttribute('name') || ''
|
const name = await input.getAttribute('name') || ''
|
||||||
const placeholder = await input.getAttribute('placeholder') || ''
|
const placeholder = await input.getAttribute('placeholder') || ''
|
||||||
|
|
||||||
console.log("INFO: Input ${i + 1}: type="${type}" name="${name}" placeholder="" + placeholder}"`)
|
console.log(`INFO: Input ${i + 1}: type="${type}" name="${name}" placeholder="${placeholder}"`)
|
||||||
|
|
||||||
if (type === 'email' ||
|
if (type === 'email' ||
|
||||||
name.toLowerCase().includes('email') ||
|
name.toLowerCase().includes('email') ||
|
||||||
name.toLowerCase().includes('username') ||
|
name.toLowerCase().includes('username') ||
|
||||||
placeholder.toLowerCase().includes('email') ||
|
placeholder.toLowerCase().includes('email') ||
|
||||||
placeholder.toLowerCase().includes('username')) {
|
placeholder.toLowerCase().includes('username')) {
|
||||||
console.log("TARGET: Found email input manually: " + name || type || placeholder) + ")"
|
console.log(`TARGET: Found email input manually: ${name || type || placeholder}`)
|
||||||
emailInput = `input:nth-of-type(${i + 1})`
|
emailInput = `input:nth-of-type(${i + 1})`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -973,7 +973,7 @@ export class TradingViewAutomation {
|
|||||||
console.log('🔄 Selector approach failed, trying manual button search for submit...')
|
console.log('🔄 Selector approach failed, trying manual button search for submit...')
|
||||||
try {
|
try {
|
||||||
const buttons = await this.page.locator('button').all()
|
const buttons = await this.page.locator('button').all()
|
||||||
console.log("CHECKING: Found " + buttons.length} buttons to check for submit`)
|
console.log(`CHECKING: Found ${buttons.length} buttons to check for submit`)
|
||||||
|
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
try {
|
try {
|
||||||
@@ -982,13 +982,13 @@ export class TradingViewAutomation {
|
|||||||
const text = (await button.textContent() || '').toLowerCase()
|
const text = (await button.textContent() || '').toLowerCase()
|
||||||
const type = await button.getAttribute('type') || ''
|
const type = await button.getAttribute('type') || ''
|
||||||
|
|
||||||
console.log("INFO: Submit Button ${i + 1}: "${text}" type="" + type}"`)
|
console.log(`INFO: Submit Button ${i + 1}: "${text}" type="${type}"`)
|
||||||
|
|
||||||
if (type === 'submit' ||
|
if (type === 'submit' ||
|
||||||
text.includes('sign in') ||
|
text.includes('sign in') ||
|
||||||
text.includes('login') ||
|
text.includes('login') ||
|
||||||
text.includes('submit')) {
|
text.includes('submit')) {
|
||||||
console.log("TARGET: Found submit button manually: "" + text}"`)
|
console.log(`TARGET: Found submit button manually: "${text}"`)
|
||||||
submitButton = `button:nth-of-type(${i + 1})`
|
submitButton = `button:nth-of-type(${i + 1})`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1026,7 +1026,7 @@ export class TradingViewAutomation {
|
|||||||
await this.page.waitForTimeout(1000) // Wait 1 second
|
await this.page.waitForTimeout(1000) // Wait 1 second
|
||||||
attempts++
|
attempts++
|
||||||
|
|
||||||
console.log("🔄 Login check attempt ${attempts}/" + maxAttempts) + ")"
|
console.log('Login check attempt ' + attempts + '/' + maxAttempts)
|
||||||
|
|
||||||
// Check if we navigated away from login page
|
// Check if we navigated away from login page
|
||||||
const currentUrl = await this.page.url()
|
const currentUrl = await this.page.url()
|
||||||
@@ -1059,8 +1059,8 @@ export class TradingViewAutomation {
|
|||||||
for (const selector of errorSelectors) {
|
for (const selector of errorSelectors) {
|
||||||
if (await this.page.locator(selector).isVisible({ timeout: 500 })) {
|
if (await this.page.locator(selector).isVisible({ timeout: 500 })) {
|
||||||
const errorText = await this.page.locator(selector).textContent()
|
const errorText = await this.page.locator(selector).textContent()
|
||||||
console.log("ERROR: Login error detected: " + errorText) + ")"
|
console.log('ERROR: Login error detected: ' + errorText)
|
||||||
throw new Error(`Login failed: ${errorText) + ")"
|
throw new Error('Login failed: ' + errorText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1312,17 +1312,17 @@ export class TradingViewAutomation {
|
|||||||
|
|
||||||
// Change symbol if not BTC
|
// Change symbol if not BTC
|
||||||
if (symbol !== 'BTCUSD') {
|
if (symbol !== 'BTCUSD') {
|
||||||
console.log("Changing symbol to " + symbol}...`)
|
console.log(`Changing symbol to ${symbol}...`)
|
||||||
await this.changeSymbol(symbol)
|
await this.changeSymbol(symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change timeframe if specified
|
// Change timeframe if specified
|
||||||
if (timeframe) {
|
if (timeframe) {
|
||||||
console.log("Setting timeframe to " + timeframe}...`)
|
console.log(`Setting timeframe to ${timeframe}...`)
|
||||||
await this.changeTimeframe(timeframe)
|
await this.changeTimeframe(timeframe)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Successfully navigated to ${symbol} chart with " + timeframe} timeframe`)
|
console.log(`Successfully navigated to ${symbol} chart with ${timeframe} timeframe`)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1352,7 +1352,7 @@ export class TradingViewAutomation {
|
|||||||
symbolElement = selector
|
symbolElement = selector
|
||||||
break
|
break
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Symbol selector " + selector} not found, trying next...`)
|
console.log(`Symbol selector ${selector} not found, trying next...`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1377,7 +1377,7 @@ export class TradingViewAutomation {
|
|||||||
searchInput = selector
|
searchInput = selector
|
||||||
break
|
break
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Search input selector " + selector} not found, trying next...`)
|
console.log(`Search input selector ${selector} not found, trying next...`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1409,7 +1409,7 @@ export class TradingViewAutomation {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Result selector " + selector} not found, trying next...`)
|
console.log(`Result selector ${selector} not found, trying next...`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1481,7 +1481,7 @@ export class TradingViewAutomation {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Interval legend selector " + selector} not found`)
|
console.log(`Interval legend selector ${selector} not found`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1539,7 +1539,7 @@ export class TradingViewAutomation {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Timeframe option selector " + selector} not found or not clickable`)
|
console.log(`Timeframe option selector ${selector} not found or not clickable`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (found) break
|
if (found) break
|
||||||
@@ -1572,7 +1572,7 @@ export class TradingViewAutomation {
|
|||||||
console.log("SUCCESS: Successfully changed timeframe to " + timeframe) + ")"
|
console.log("SUCCESS: Successfully changed timeframe to " + timeframe) + ")"
|
||||||
await this.takeDebugScreenshot('after_timeframe_change')
|
await this.takeDebugScreenshot('after_timeframe_change')
|
||||||
} else {
|
} else {
|
||||||
console.log("ERROR: Could not change timeframe to " + timeframe} - timeframe options not found`)
|
console.log(`ERROR: Could not change timeframe to ${timeframe} - timeframe options not found`)
|
||||||
// Take a debug screenshot to see current state
|
// Take a debug screenshot to see current state
|
||||||
await this.takeDebugScreenshot('timeframe_change_failed')
|
await this.takeDebugScreenshot('timeframe_change_failed')
|
||||||
|
|
||||||
@@ -1848,7 +1848,7 @@ export class TradingViewAutomation {
|
|||||||
const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8')
|
const cookiesData = await fs.readFile(COOKIES_FILE, 'utf8')
|
||||||
const cookies = JSON.parse(cookiesData)
|
const cookies = JSON.parse(cookiesData)
|
||||||
await this.context!.addCookies(cookies)
|
await this.context!.addCookies(cookies)
|
||||||
console.log("SUCCESS: Loaded " + cookies.length} cookies from saved session`)
|
console.log(`SUCCESS: Loaded ${cookies.length} cookies from saved session`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Session storage will be loaded after page navigation
|
// Note: Session storage will be loaded after page navigation
|
||||||
@@ -1870,7 +1870,7 @@ export class TradingViewAutomation {
|
|||||||
// Save cookies
|
// Save cookies
|
||||||
const cookies = await this.context.cookies()
|
const cookies = await this.context.cookies()
|
||||||
await fs.writeFile(COOKIES_FILE, JSON.stringify(cookies, null, 2))
|
await fs.writeFile(COOKIES_FILE, JSON.stringify(cookies, null, 2))
|
||||||
console.log("SUCCESS: Saved " + cookies.length} cookies`)
|
console.log(`SUCCESS: Saved ${cookies.length} cookies`)
|
||||||
|
|
||||||
// Save session storage and localStorage
|
// Save session storage and localStorage
|
||||||
const sessionData = await this.page.evaluate(() => {
|
const sessionData = await this.page.evaluate(() => {
|
||||||
@@ -2072,7 +2072,7 @@ export class TradingViewAutomation {
|
|||||||
if (!this.humanBehaviorEnabled) return
|
if (!this.humanBehaviorEnabled) return
|
||||||
|
|
||||||
const delay = Math.random() * (maxMs - minMs) + minMs
|
const delay = Math.random() * (maxMs - minMs) + minMs
|
||||||
console.log("⏱️ Human-like delay: " + Math.round(delay)}ms`)
|
console.log(`⏱️ Human-like delay: ${Math.round(delay)}ms`)
|
||||||
await new Promise(resolve => setTimeout(resolve, delay))
|
await new Promise(resolve => setTimeout(resolve, delay))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2129,7 +2129,7 @@ export class TradingViewAutomation {
|
|||||||
|
|
||||||
if (timeSinceLastRequest < minInterval) {
|
if (timeSinceLastRequest < minInterval) {
|
||||||
const waitTime = minInterval - timeSinceLastRequest
|
const waitTime = minInterval - timeSinceLastRequest
|
||||||
console.log("🚦 Throttling request: waiting ${Math.round(waitTime / 1000)}s before next request (request #" + this.requestCount + 1})`)
|
console.log(`🚦 Throttling request: waiting ${Math.round(waitTime / 1000)}s before next request (request #${this.requestCount + 1})`)
|
||||||
await new Promise(resolve => setTimeout(resolve, waitTime))
|
await new Promise(resolve => setTimeout(resolve, waitTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2288,7 +2288,7 @@ export class TradingViewAutomation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(captchaMarkerFile, JSON.stringify(markerData, null, 2))
|
await fs.writeFile(captchaMarkerFile, JSON.stringify(markerData, null, 2))
|
||||||
console.log("INFO: Marked captcha detection #${markerData.count} at " + markerData.timestamp) + ")"
|
console.log('INFO: Marked captcha detection #' + markerData.count + ' at ' + markerData.timestamp)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('WARNING: Error marking captcha detection:', error)
|
console.log('WARNING: Error marking captcha detection:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user