Compare commits
11 Commits
fe48536e9b
...
2b3355cddb
| Author | SHA1 | Date |
|---|---|---|
|
|
2b3355cddb | |
|
|
f91534e976 | |
|
|
c981faf3a6 | |
|
|
2ba6425eed | |
|
|
a0c54feee5 | |
|
|
b3373d6d85 | |
|
|
2dba57191b | |
|
|
cd83c18555 | |
|
|
76c24aaec1 | |
|
|
4d00f978e2 | |
|
|
f68b3e5b75 |
|
|
@ -3,6 +3,9 @@ import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
|
||||||
export const setupSecurity = (app) => {
|
export const setupSecurity = (app) => {
|
||||||
|
// Trust proxy for express-rate-limit behind Kubernetes Ingress
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
|
||||||
// Helmet for security headers
|
// Helmet for security headers
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
|
|
@ -12,7 +15,7 @@ export const setupSecurity = (app) => {
|
||||||
scriptSrc: ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com'],
|
scriptSrc: ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com'],
|
||||||
scriptSrcElem: ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com'],
|
scriptSrcElem: ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com'],
|
||||||
imgSrc: ["'self'", 'data:', 'https:'],
|
imgSrc: ["'self'", 'data:', 'https:'],
|
||||||
connectSrc: ["'self'", 'https:', 'wss:'],
|
connectSrc: ["'self'", "ws:", "wss:", "http:", "https:"],
|
||||||
workerSrc: ["'self'", 'blob:'],
|
workerSrc: ["'self'", 'blob:'],
|
||||||
fontSrc: ["'self'", 'https:', 'data:'],
|
fontSrc: ["'self'", 'https:', 'data:'],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -293,8 +293,8 @@ router.get('/recent', async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// WebSocket for real-time alerts
|
// WebSocket for real-time alerts
|
||||||
export function setupAlertsWebSocket(server) {
|
export function setupAlertsWebSocket() {
|
||||||
const wss = new WebSocketServer({ server, path: '/ws/alerts' });
|
const wss = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
console.log('Client connected to alerts feed');
|
console.log('Client connected to alerts feed');
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ router.get('/universe', async (req, res) => {
|
||||||
change_pct: quote?.change_pct ?? null,
|
change_pct: quote?.change_pct ?? null,
|
||||||
market_cap: quote?.market_cap ?? null,
|
market_cap: quote?.market_cap ?? null,
|
||||||
volume: quote?.volume ?? null,
|
volume: quote?.volume ?? null,
|
||||||
|
day_high: quote?.high ?? null,
|
||||||
|
day_low: quote?.low ?? null,
|
||||||
high_52w: null, // populated by quoteSummary if needed
|
high_52w: null, // populated by quoteSummary if needed
|
||||||
low_52w: null,
|
low_52w: null,
|
||||||
market_state: quote?.market_state ?? null,
|
market_state: quote?.market_state ?? null,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { WebSocketServer } from 'ws';
|
||||||
import { rawQuery } from '../db.js';
|
import { rawQuery } from '../db.js';
|
||||||
import { optionsFlowQuery } from '../queries/optionsFlowQuery.js';
|
import { optionsFlowQuery } from '../queries/optionsFlowQuery.js';
|
||||||
|
|
||||||
export function setupWebSocket(server) {
|
export function setupWebSocket() {
|
||||||
const wss = new WebSocketServer({ server, path: '/ws/flow' });
|
const wss = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
console.log('Client connected to WebSocket');
|
console.log('Client connected to WebSocket');
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,25 @@ const server = app.listen(PORT, async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup WebSocket
|
// Setup WebSockets manually to handle multiple paths
|
||||||
setupWebSocket(server);
|
const flowWss = setupWebSocket();
|
||||||
setupAlertsWebSocket(server);
|
const alertsWss = setupAlertsWebSocket();
|
||||||
|
|
||||||
|
server.on('upgrade', (request, socket, head) => {
|
||||||
|
const pathname = request.url.split('?')[0];
|
||||||
|
|
||||||
|
if (pathname === '/ws/flow') {
|
||||||
|
flowWss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
|
flowWss.emit('connection', ws, request);
|
||||||
|
});
|
||||||
|
} else if (pathname === '/ws/alerts') {
|
||||||
|
alertsWss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
|
alertsWss.emit('connection', ws, request);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
|
|
|
||||||
|
|
@ -303,20 +303,26 @@ ${yahooData ? `STOCK DATA (Yahoo Finance):
|
||||||
- Volume: ${yahooData.volume?.toLocaleString() || 'N/A'}
|
- Volume: ${yahooData.volume?.toLocaleString() || 'N/A'}
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
Provide a concise analysis (2-3 sentences max) with:
|
Provide a detailed analysis categorized into three distinct trader profiles based on the data.
|
||||||
1. Overall assessment (Bullish/Bearish/Neutral)
|
|
||||||
2. Key risk factors or opportunities
|
|
||||||
3. Clear action recommendation (Enter/Wait/Avoid)
|
|
||||||
|
|
||||||
Format as JSON:
|
Format as JSON:
|
||||||
{
|
{
|
||||||
"assessment": "BULLISH|BEARISH|NEUTRAL",
|
"longTerm": {
|
||||||
"summary": "Brief 1-2 sentence summary",
|
"assessment": "BULLISH|BEARISH|NEUTRAL",
|
||||||
"keyFactors": ["factor1", "factor2", "factor3"],
|
"moatAndFundamentals": "Brief analysis of economic moat, EPS growth, and fundamental ratios (P/E, P/B)",
|
||||||
"recommendation": "ENTER|WAIT|AVOID",
|
"recommendation": "ENTER|WAIT|AVOID"
|
||||||
"reasoning": "Brief explanation",
|
},
|
||||||
"riskLevel": "LOW|MEDIUM|HIGH",
|
"swing": {
|
||||||
"timeHorizon": "INTRADAY|SWING|POSITION"
|
"assessment": "BULLISH|BEARISH|NEUTRAL",
|
||||||
|
"technicalSetup": "Analysis of technical setups, consolidation, moving averages, and RSI momentum",
|
||||||
|
"recommendation": "ENTER|WAIT|AVOID"
|
||||||
|
},
|
||||||
|
"dayTrade": {
|
||||||
|
"assessment": "BULLISH|BEARISH|NEUTRAL",
|
||||||
|
"volatilityAndLiquidity": "Analysis of immediate catalysts, intraday liquidity/volume, and volatility risks",
|
||||||
|
"recommendation": "ENTER|WAIT|AVOID"
|
||||||
|
},
|
||||||
|
"summary": "Overall 1-2 sentence conclusion covering the most viable approach"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
// Use model from env or default to stable version
|
// Use model from env or default to stable version
|
||||||
|
|
@ -346,13 +352,22 @@ Format as JSON:
|
||||||
// Fallback: create structured response from text
|
// Fallback: create structured response from text
|
||||||
console.warn('Failed to parse AI response as JSON, using fallback');
|
console.warn('Failed to parse AI response as JSON, using fallback');
|
||||||
analysis = {
|
analysis = {
|
||||||
assessment: direction,
|
longTerm: {
|
||||||
summary: responseText.substring(0, 200),
|
assessment: direction,
|
||||||
keyFactors: [],
|
moatAndFundamentals: 'Unable to perform deep fundamental analysis. Please review ratios manually.',
|
||||||
recommendation: score >= 2.0 ? 'ENTER' : 'WAIT',
|
recommendation: score >= 5.0 ? 'ENTER' : 'WAIT'
|
||||||
reasoning: responseText,
|
},
|
||||||
riskLevel: score < 2.0 ? 'HIGH' : score < 5.0 ? 'MEDIUM' : 'LOW',
|
swing: {
|
||||||
timeHorizon: 'INTRADAY'
|
assessment: direction,
|
||||||
|
technicalSetup: 'Technical analysis currently unavailable.',
|
||||||
|
recommendation: score >= 3.0 ? 'ENTER' : 'WAIT'
|
||||||
|
},
|
||||||
|
dayTrade: {
|
||||||
|
assessment: direction,
|
||||||
|
volatilityAndLiquidity: 'Analysis of intraday volume and catalysts unavailable.',
|
||||||
|
recommendation: score >= 2.0 ? 'ENTER' : 'WAIT'
|
||||||
|
},
|
||||||
|
summary: responseText.substring(0, 200) || 'Analysis unavailable'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,13 +384,10 @@ Format as JSON:
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
analysis: {
|
analysis: {
|
||||||
assessment: 'NEUTRAL',
|
longTerm: { assessment: 'NEUTRAL', moatAndFundamentals: 'Analysis unavailable', recommendation: 'WAIT' },
|
||||||
summary: 'Analysis unavailable',
|
swing: { assessment: 'NEUTRAL', technicalSetup: 'Analysis unavailable', recommendation: 'WAIT' },
|
||||||
keyFactors: [],
|
dayTrade: { assessment: 'NEUTRAL', volatilityAndLiquidity: 'Analysis unavailable', recommendation: 'WAIT' },
|
||||||
recommendation: 'WAIT',
|
summary: 'Analysis unavailable'
|
||||||
reasoning: 'Unable to generate analysis',
|
|
||||||
riskLevel: 'MEDIUM',
|
|
||||||
timeHorizon: 'INTRADAY'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/Badge';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Bell, TrendingUp, TrendingDown, AlertTriangle, Calendar, Clock } from 'lucide-react';
|
import { Bell, TrendingUp, TrendingDown, AlertTriangle, Calendar, Clock } from 'lucide-react';
|
||||||
import { requestNotificationPermission, sendNotification } from '@/utils/notifications';
|
import { requestNotificationPermission, sendNotification } from '@/utils/notifications';
|
||||||
|
import { getApiUrl } from '@/config/api';
|
||||||
|
|
||||||
export default function AlertsFeed() {
|
export default function AlertsFeed() {
|
||||||
const [alerts, setAlerts] = useState([]);
|
const [alerts, setAlerts] = useState([]);
|
||||||
|
|
|
||||||
|
|
@ -311,9 +311,9 @@ function FundamentalsTab({ f }) {
|
||||||
{
|
{
|
||||||
title: '💰 Valuation',
|
title: '💰 Valuation',
|
||||||
metrics: [
|
metrics: [
|
||||||
{ label: 'P/E Ratio (TTM)', value: fmt(f.pe_ttm, 1) },
|
{ label: 'P/E Ratio (TTM)', value: fmt(f.pe_ttm, 1), h: (f.pe_ttm >= 15 && f.pe_ttm <= 25) ? 'positive' : (f.pe_ttm < 0 || f.pe_ttm > 50) ? 'negative' : undefined },
|
||||||
{ label: 'P/E Ratio (Fwd)', value: fmt(f.pe_forward, 1) },
|
{ label: 'P/E Ratio (Fwd)', value: fmt(f.pe_forward, 1), h: (f.pe_forward >= 15 && f.pe_forward <= 25) ? 'positive' : (f.pe_forward < 0 || f.pe_forward > 50) ? 'negative' : undefined },
|
||||||
{ label: 'Price / Book', value: fmt(f.price_to_book, 2) },
|
{ label: 'Price / Book', value: fmt(f.price_to_book, 2), h: (f.price_to_book >= 1 && f.price_to_book <= 3) ? 'positive' : (f.price_to_book > 10) ? 'negative' : undefined },
|
||||||
{ label: 'Price / Sales', value: fmt(f.price_to_sales, 2) },
|
{ label: 'Price / Sales', value: fmt(f.price_to_sales, 2) },
|
||||||
{ label: 'EV / EBITDA', value: fmt(f.ev_ebitda, 1) },
|
{ label: 'EV / EBITDA', value: fmt(f.ev_ebitda, 1) },
|
||||||
{ label: 'Market Cap', value: fmtLarge(f.market_cap) },
|
{ label: 'Market Cap', value: fmtLarge(f.market_cap) },
|
||||||
|
|
@ -337,7 +337,7 @@ function FundamentalsTab({ f }) {
|
||||||
{ label: 'EBITDA Margin', value: fmtPct(f.ebitda_margins) },
|
{ label: 'EBITDA Margin', value: fmtPct(f.ebitda_margins) },
|
||||||
{ label: 'Operating Margin', value: fmtPct(f.operating_margins) },
|
{ label: 'Operating Margin', value: fmtPct(f.operating_margins) },
|
||||||
{ label: 'Profit Margin', value: fmtPct(f.profit_margins) },
|
{ label: 'Profit Margin', value: fmtPct(f.profit_margins) },
|
||||||
{ label: 'Return on Equity', value: fmtPct(f.return_on_equity), h: f.return_on_equity > 0.15 ? 'positive' : f.return_on_equity < 0 ? 'negative' : undefined },
|
{ label: 'Return on Equity', value: fmtPct(f.return_on_equity), h: (f.return_on_equity >= 0.1 && f.return_on_equity <= 0.2) ? 'positive' : f.return_on_equity < 0 ? 'negative' : undefined },
|
||||||
{ label: 'Return on Assets', value: fmtPct(f.return_on_assets) },
|
{ label: 'Return on Assets', value: fmtPct(f.return_on_assets) },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -373,7 +373,7 @@ function FundamentalsTab({ f }) {
|
||||||
{ label: 'Inst. Ownership', value: fmtPct(f.held_pct_institutions) },
|
{ label: 'Inst. Ownership', value: fmtPct(f.held_pct_institutions) },
|
||||||
{ label: 'Insider Ownership', value: fmtPct(f.held_pct_insiders) },
|
{ label: 'Insider Ownership', value: fmtPct(f.held_pct_insiders) },
|
||||||
{ label: 'Dividend Yield', value: fmtPct(f.dividend_yield), h: f.dividend_yield > 0 ? 'positive' : undefined },
|
{ label: 'Dividend Yield', value: fmtPct(f.dividend_yield), h: f.dividend_yield > 0 ? 'positive' : undefined },
|
||||||
{ label: 'Payout Ratio', value: fmtPct(f.payout_ratio) },
|
{ label: 'Payout Ratio', value: fmtPct(f.payout_ratio), h: f.payout_ratio < 0.6 ? 'positive' : f.payout_ratio > 0.7 ? 'negative' : undefined },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -495,49 +495,60 @@ export function TradeAnalysisModal({ row, isOpen, onClose }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{aiAnalysis && aiAnalysis.analysis && (
|
{aiAnalysis && aiAnalysis.analysis && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="text-sm text-slate-200 bg-slate-700/30 p-3 rounded border border-slate-600/50">
|
||||||
<Badge
|
<strong>Summary:</strong> {aiAnalysis.analysis.summary}
|
||||||
variant={
|
|
||||||
aiAnalysis.analysis.assessment === 'BULLISH' ? 'success' :
|
|
||||||
aiAnalysis.analysis.assessment === 'BEARISH' ? 'destructive' : 'secondary'
|
|
||||||
}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{aiAnalysis.analysis.assessment}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
aiAnalysis.analysis.recommendation === 'ENTER' ? 'success' :
|
|
||||||
aiAnalysis.analysis.recommendation === 'WAIT' ? 'warning' : 'destructive'
|
|
||||||
}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{aiAnalysis.analysis.recommendation}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{aiAnalysis.analysis.riskLevel} Risk
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-200">{aiAnalysis.analysis.summary}</p>
|
|
||||||
|
|
||||||
{aiAnalysis.analysis.keyFactors && aiAnalysis.analysis.keyFactors.length > 0 && (
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
{/* Long Term */}
|
||||||
<div className="text-xs text-slate-500 mb-1">Key Factors:</div>
|
{aiAnalysis.analysis.longTerm && (
|
||||||
<ul className="text-xs text-slate-300 space-y-1 list-disc list-inside">
|
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
|
||||||
{aiAnalysis.analysis.keyFactors.map((factor, idx) => (
|
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Long-Term Investor</div>
|
||||||
<li key={idx}>{factor}</li>
|
<div className="flex gap-2 mb-2">
|
||||||
))}
|
<Badge variant={aiAnalysis.analysis.longTerm.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.longTerm.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||||
</ul>
|
{aiAnalysis.analysis.longTerm.assessment}
|
||||||
</div>
|
</Badge>
|
||||||
)}
|
<Badge variant={aiAnalysis.analysis.longTerm.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.longTerm.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.longTerm.recommendation}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.longTerm.moatAndFundamentals}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{aiAnalysis.analysis.reasoning && (
|
{/* Swing Trade */}
|
||||||
<div>
|
{aiAnalysis.analysis.swing && (
|
||||||
<div className="text-xs text-slate-500 mb-1">Reasoning:</div>
|
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
|
||||||
<p className="text-xs text-slate-300">{aiAnalysis.analysis.reasoning}</p>
|
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Swing Trader</div>
|
||||||
</div>
|
<div className="flex gap-2 mb-2">
|
||||||
)}
|
<Badge variant={aiAnalysis.analysis.swing.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.swing.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.swing.assessment}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={aiAnalysis.analysis.swing.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.swing.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.swing.recommendation}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.swing.technicalSetup}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Day Trade */}
|
||||||
|
{aiAnalysis.analysis.dayTrade && (
|
||||||
|
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
|
||||||
|
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Day Trader</div>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<Badge variant={aiAnalysis.analysis.dayTrade.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.dayTrade.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.dayTrade.assessment}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={aiAnalysis.analysis.dayTrade.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.dayTrade.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.dayTrade.recommendation}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.dayTrade.volatilityAndLiquidity}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -60,56 +60,64 @@ export function AnalysisSection({ row, hasPhase1Data, onPhase1Click }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{aiAnalysis && aiAnalysis.analysis && (
|
{aiAnalysis && aiAnalysis.analysis && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<div>
|
<div className="text-sm text-slate-200 bg-slate-700/30 p-2 rounded border border-slate-600/50">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<strong>Summary:</strong> {aiAnalysis.analysis.summary}
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
aiAnalysis.analysis.assessment === 'BULLISH' ? 'success' :
|
|
||||||
aiAnalysis.analysis.assessment === 'BEARISH' ? 'destructive' : 'secondary'
|
|
||||||
}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{aiAnalysis.analysis.assessment}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
aiAnalysis.analysis.recommendation === 'ENTER' ? 'success' :
|
|
||||||
aiAnalysis.analysis.recommendation === 'WAIT' ? 'warning' : 'destructive'
|
|
||||||
}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{aiAnalysis.analysis.recommendation}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{aiAnalysis.analysis.riskLevel} Risk
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-slate-200">{aiAnalysis.analysis.summary}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aiAnalysis.analysis.keyFactors && aiAnalysis.analysis.keyFactors.length > 0 && (
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<div>
|
{/* Long Term */}
|
||||||
<div className="text-xs text-slate-500 mb-1">Key Factors:</div>
|
{aiAnalysis.analysis.longTerm && (
|
||||||
<ul className="text-xs text-slate-300 space-y-1">
|
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
|
||||||
{aiAnalysis.analysis.keyFactors.map((factor, idx) => (
|
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Long-Term Investor</div>
|
||||||
<li key={idx}>• {factor}</li>
|
<div className="flex gap-2 mb-2">
|
||||||
))}
|
<Badge variant={aiAnalysis.analysis.longTerm.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.longTerm.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||||
</ul>
|
{aiAnalysis.analysis.longTerm.assessment}
|
||||||
</div>
|
</Badge>
|
||||||
)}
|
<Badge variant={aiAnalysis.analysis.longTerm.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.longTerm.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.longTerm.recommendation}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.longTerm.moatAndFundamentals}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{aiAnalysis.analysis.reasoning && (
|
{/* Swing Trade */}
|
||||||
<div>
|
{aiAnalysis.analysis.swing && (
|
||||||
<div className="text-xs text-slate-500 mb-1">Reasoning:</div>
|
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
|
||||||
<p className="text-xs text-slate-300">{aiAnalysis.analysis.reasoning}</p>
|
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Swing Trader</div>
|
||||||
</div>
|
<div className="flex gap-2 mb-2">
|
||||||
)}
|
<Badge variant={aiAnalysis.analysis.swing.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.swing.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.swing.assessment}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={aiAnalysis.analysis.swing.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.swing.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.swing.recommendation}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.swing.technicalSetup}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-xs text-slate-400 pt-2 border-t border-slate-700">
|
{/* Day Trade */}
|
||||||
<span>Time Horizon: {aiAnalysis.analysis.timeHorizon}</span>
|
{aiAnalysis.analysis.dayTrade && (
|
||||||
|
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700/50">
|
||||||
|
<div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">Day Trader</div>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<Badge variant={aiAnalysis.analysis.dayTrade.assessment === 'BULLISH' ? 'success' : aiAnalysis.analysis.dayTrade.assessment === 'BEARISH' ? 'destructive' : 'secondary'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.dayTrade.assessment}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={aiAnalysis.analysis.dayTrade.recommendation === 'ENTER' ? 'success' : aiAnalysis.analysis.dayTrade.recommendation === 'WAIT' ? 'warning' : 'destructive'} className="text-[10px]">
|
||||||
|
{aiAnalysis.analysis.dayTrade.recommendation}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.dayTrade.volatilityAndLiquidity}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-xs text-slate-500 pt-2 border-t border-slate-700">
|
||||||
{aiAnalysis.timestamp && (
|
{aiAnalysis.timestamp && (
|
||||||
<span>• {new Date(aiAnalysis.timestamp).toLocaleTimeString()}</span>
|
<span>Generated: {new Date(aiAnalysis.timestamp).toLocaleTimeString()}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
export const API_CONFIG = {
|
export const API_CONFIG = {
|
||||||
baseUrl: import.meta.env.VITE_API_URL || '',
|
baseUrl: import.meta.env.VITE_API_URL || '',
|
||||||
wsUrl: import.meta.env.VITE_WS_URL || 'ws://localhost:5010',
|
wsUrl: import.meta.env.VITE_WS_URL || (typeof window !== 'undefined' ? (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host : ''),
|
||||||
timeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'),
|
timeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'),
|
||||||
retryAttempts: parseInt(import.meta.env.VITE_API_RETRY_ATTEMPTS || '3'),
|
retryAttempts: parseInt(import.meta.env.VITE_API_RETRY_ATTEMPTS || '3'),
|
||||||
retryDelay: parseInt(import.meta.env.VITE_API_RETRY_DELAY || '1000')
|
retryDelay: parseInt(import.meta.env.VITE_API_RETRY_DELAY || '1000')
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,15 @@ spec:
|
||||||
env:
|
env:
|
||||||
- name: NODE_ENV
|
- name: NODE_ENV
|
||||||
value: "production"
|
value: "production"
|
||||||
|
- name: CORS_ORIGIN
|
||||||
|
value: "https://market.applaude.net"
|
||||||
|
- name: USE_PYTHON_SERVICE
|
||||||
|
value: "false"
|
||||||
|
- name: DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: market-db-app
|
||||||
|
key: uri
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
tcpSocket:
|
tcpSocket:
|
||||||
port: 3010
|
port: 3010
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: Cluster
|
||||||
|
metadata:
|
||||||
|
name: market-db
|
||||||
|
namespace: ai-agents
|
||||||
|
spec:
|
||||||
|
instances: 1
|
||||||
|
storage:
|
||||||
|
size: 1Gi
|
||||||
Loading…
Reference in New Issue