feat: Add Phase 4 advanced metrics and components
- 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
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user