- Add advanced metrics dashboard with trade analytics - Add new trading components (EntryTypeAnalysis, MultiDayPositionTracker, NewsEventTracker, etc.) - Add strategy mode selector and trend confirmation - Add risk automation panel and slippage correlation analysis - Add daily trading plan enhancements with modal components - Add custom hooks (useApi, useLocalStorage, useAdvancedTradeMetrics) - Add broker service integration and trading API - Add test setup and vitest configuration - Include parquet data files for live market data - Add comprehensive documentation in docs/ folder
216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
// INSTRUCTIONS: Replace handleBuy, handleSell, and handleReset in App.tsx with these versions
|
|
// Also add loadPortfolioFromBackend and call it in useEffect
|
|
|
|
import { executeTradeAPI, getPortfolioAPI, resetSimulationAPI, convertBackendPortfolio } from './services/tradingAPI'
|
|
|
|
// Add this state at the top of App component
|
|
const [isLoadingTrade, setIsLoadingTrade] = useState(false)
|
|
|
|
// Add this function to load portfolio on mount
|
|
const loadPortfolioFromBackend = useCallback(async () => {
|
|
try {
|
|
const backendPortfolio = await getPortfolioAPI()
|
|
const converted = convertBackendPortfolio(backendPortfolio, currentPrice)
|
|
setPortfolio(converted)
|
|
console.log('✅ Portfolio loaded from backend:', converted)
|
|
} catch (error) {
|
|
console.error('Failed to load portfolio from backend:', error)
|
|
// Fall back to default portfolio
|
|
setPortfolio(recalcPortfolio(createInitialPortfolio(), currentPrice))
|
|
}
|
|
}, [currentPrice])
|
|
|
|
// Add this useEffect to load on mount
|
|
useEffect(() => {
|
|
if (syncToBackend) {
|
|
loadPortfolioFromBackend()
|
|
}
|
|
}, []) // Only run once on mount
|
|
|
|
// REPLACE handleBuy with this version
|
|
const handleBuy = useCallback(async (quantity: number) => {
|
|
if (quantity <= 0 || Number.isNaN(quantity)) return
|
|
|
|
if (!syncToBackend) {
|
|
// Original in-memory logic (keep for backward compatibility)
|
|
setPortfolio((prev) => {
|
|
const cost = quantity * currentPrice
|
|
if (cost > prev.cash) {
|
|
alert('Insufficient cash for this order')
|
|
return prev
|
|
}
|
|
const existing = prev.position
|
|
const totalQuantity = existing ? existing.quantity + quantity : quantity
|
|
const avgPrice = existing ? ((existing.avgPrice * existing.quantity + currentPrice * quantity) / totalQuantity) : currentPrice
|
|
const trade: Trade = {
|
|
id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
timestamp: Date.now(),
|
|
action: 'BUY',
|
|
quantity,
|
|
price: currentPrice,
|
|
total: Number(cost.toFixed(2)),
|
|
}
|
|
const updated: Portfolio = {
|
|
...prev,
|
|
cash: Number((prev.cash - cost).toFixed(2)),
|
|
position: {
|
|
symbol: TRADING_SYMBOL,
|
|
quantity: Number(totalQuantity.toFixed(4)),
|
|
avgPrice: Number(avgPrice.toFixed(2)),
|
|
currentPrice,
|
|
unrealizedPnl: 0,
|
|
unrealizedPnlPercent: 0,
|
|
},
|
|
trades: [trade, ...prev.trades].slice(0, 200),
|
|
}
|
|
return recalcPortfolio(updated, currentPrice)
|
|
})
|
|
return
|
|
}
|
|
|
|
// NEW: Backend-persisted logic
|
|
setIsLoadingTrade(true)
|
|
try {
|
|
const response = await executeTradeAPI({
|
|
action: 'BUY',
|
|
quantity,
|
|
price: currentPrice,
|
|
symbol: TRADING_SYMBOL
|
|
})
|
|
|
|
// Reload portfolio from backend to ensure sync
|
|
const backendPortfolio = await getPortfolioAPI()
|
|
const converted = convertBackendPortfolio(backendPortfolio, currentPrice)
|
|
setPortfolio(converted)
|
|
|
|
console.log('✅ BUY trade executed and synced:', response.trade)
|
|
} catch (error: any) {
|
|
console.error('❌ Trade execution failed:', error)
|
|
if (error.response?.data?.detail) {
|
|
alert(`Trade failed: ${error.response.data.detail}`)
|
|
} else {
|
|
alert('Trade execution failed. Please try again.')
|
|
}
|
|
} finally {
|
|
setIsLoadingTrade(false)
|
|
}
|
|
}, [currentPrice, syncToBackend])
|
|
|
|
// REPLACE handleSell with this version
|
|
const handleSell = useCallback(async (quantity: number, reason = 'Manual exit') => {
|
|
if (!syncToBackend) {
|
|
// Original in-memory logic (keep for backward compatibility)
|
|
setPortfolio((prev) => {
|
|
const position = prev.position
|
|
if (!position) {
|
|
alert('No open position to close')
|
|
return prev
|
|
}
|
|
const size = Math.min(quantity, position.quantity)
|
|
if (size <= 0) return prev
|
|
const proceeds = size * currentPrice
|
|
const pnl = (currentPrice - position.avgPrice) * size
|
|
const trade: Trade = {
|
|
id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
timestamp: Date.now(),
|
|
action: 'SELL',
|
|
quantity: size,
|
|
price: currentPrice,
|
|
total: Number(proceeds.toFixed(2)),
|
|
pnl: Number(pnl.toFixed(2)),
|
|
}
|
|
const remainingQty = Number((position.quantity - size).toFixed(4))
|
|
const nextPosition = remainingQty > 0.0001
|
|
? { ...position, quantity: remainingQty, currentPrice }
|
|
: null
|
|
const updated: Portfolio = {
|
|
...prev,
|
|
cash: Number((prev.cash + proceeds).toFixed(2)),
|
|
position: nextPosition,
|
|
trades: [trade, ...prev.trades].slice(0, 200),
|
|
}
|
|
if (reason.startsWith('Auto')) {
|
|
console.info(reason)
|
|
}
|
|
return recalcPortfolio(updated, currentPrice)
|
|
})
|
|
if (!reason.startsWith('Manual')) {
|
|
setAiAnalysis(null)
|
|
}
|
|
return
|
|
}
|
|
|
|
// NEW: Backend-persisted logic
|
|
setIsLoadingTrade(true)
|
|
try {
|
|
// Check if we have a position
|
|
const currentPortfolio = await getPortfolioAPI()
|
|
if (!currentPortfolio.position) {
|
|
alert('No open position to close')
|
|
return
|
|
}
|
|
|
|
const size = Math.min(quantity, currentPortfolio.position.quantity)
|
|
|
|
const response = await executeTradeAPI({
|
|
action: 'SELL',
|
|
quantity: size,
|
|
price: currentPrice,
|
|
symbol: TRADING_SYMBOL,
|
|
notes: reason
|
|
})
|
|
|
|
// Reload portfolio from backend to ensure sync
|
|
const backendPortfolio = await getPortfolioAPI()
|
|
const converted = convertBackendPortfolio(backendPortfolio, currentPrice)
|
|
setPortfolio(converted)
|
|
|
|
console.log('✅ SELL trade executed and synced:', response.trade)
|
|
|
|
if (reason.startsWith('Auto')) {
|
|
console.info(reason)
|
|
}
|
|
} catch (error: any) {
|
|
console.error('❌ Trade execution failed:', error)
|
|
if (error.response?.data?.detail) {
|
|
alert(`Trade failed: ${error.response.data.detail}`)
|
|
} else {
|
|
alert('Trade execution failed. Please try again.')
|
|
}
|
|
} finally {
|
|
setIsLoadingTrade(false)
|
|
}
|
|
|
|
if (!reason.startsWith('Manual')) {
|
|
setAiAnalysis(null)
|
|
}
|
|
}, [currentPrice, syncToBackend])
|
|
|
|
// REPLACE handleReset with this version
|
|
const handleReset = useCallback(async () => {
|
|
if (!syncToBackend) {
|
|
// Original in-memory logic
|
|
setPortfolio(recalcPortfolio(createInitialPortfolio(), currentPrice))
|
|
setAiAnalysis(null)
|
|
return
|
|
}
|
|
|
|
// NEW: Backend-persisted logic
|
|
setIsLoadingTrade(true)
|
|
try {
|
|
const response = await resetSimulationAPI()
|
|
const converted = convertBackendPortfolio(response.portfolio, currentPrice)
|
|
setPortfolio(converted)
|
|
setAiAnalysis(null)
|
|
console.log('✅ Simulation reset and synced')
|
|
} catch (error) {
|
|
console.error('❌ Reset failed:', error)
|
|
alert('Failed to reset simulation. Please try again.')
|
|
} finally {
|
|
setIsLoadingTrade(false)
|
|
}
|
|
}, [currentPrice, syncToBackend])
|
|
|
|
// OPTIONAL: Add loading indicator in your Trade panel
|
|
// Show a spinner or disable buttons when isLoadingTrade is true
|