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';
|
||||
|
||||
export const setupSecurity = (app) => {
|
||||
// Trust proxy for express-rate-limit behind Kubernetes Ingress
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// Helmet for security headers
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
|
|
@ -12,7 +15,7 @@ export const setupSecurity = (app) => {
|
|||
scriptSrc: ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com'],
|
||||
scriptSrcElem: ["'self'", "'unsafe-inline'", 'https://static.cloudflareinsights.com'],
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
connectSrc: ["'self'", 'https:', 'wss:'],
|
||||
connectSrc: ["'self'", "ws:", "wss:", "http:", "https:"],
|
||||
workerSrc: ["'self'", 'blob:'],
|
||||
fontSrc: ["'self'", 'https:', 'data:'],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -293,8 +293,8 @@ router.get('/recent', async (req, res) => {
|
|||
});
|
||||
|
||||
// WebSocket for real-time alerts
|
||||
export function setupAlertsWebSocket(server) {
|
||||
const wss = new WebSocketServer({ server, path: '/ws/alerts' });
|
||||
export function setupAlertsWebSocket() {
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('Client connected to alerts feed');
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ router.get('/universe', async (req, res) => {
|
|||
change_pct: quote?.change_pct ?? null,
|
||||
market_cap: quote?.market_cap ?? null,
|
||||
volume: quote?.volume ?? null,
|
||||
day_high: quote?.high ?? null,
|
||||
day_low: quote?.low ?? null,
|
||||
high_52w: null, // populated by quoteSummary if needed
|
||||
low_52w: null,
|
||||
market_state: quote?.market_state ?? null,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { WebSocketServer } from 'ws';
|
|||
import { rawQuery } from '../db.js';
|
||||
import { optionsFlowQuery } from '../queries/optionsFlowQuery.js';
|
||||
|
||||
export function setupWebSocket(server) {
|
||||
const wss = new WebSocketServer({ server, path: '/ws/flow' });
|
||||
export function setupWebSocket() {
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('Client connected to WebSocket');
|
||||
|
|
|
|||
|
|
@ -127,9 +127,25 @@ const server = app.listen(PORT, async () => {
|
|||
}
|
||||
});
|
||||
|
||||
// Setup WebSocket
|
||||
setupWebSocket(server);
|
||||
setupAlertsWebSocket(server);
|
||||
// Setup WebSockets manually to handle multiple paths
|
||||
const flowWss = setupWebSocket();
|
||||
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
|
||||
process.on('SIGTERM', () => {
|
||||
|
|
|
|||
|
|
@ -303,20 +303,26 @@ ${yahooData ? `STOCK DATA (Yahoo Finance):
|
|||
- Volume: ${yahooData.volume?.toLocaleString() || 'N/A'}
|
||||
` : ''}
|
||||
|
||||
Provide a concise analysis (2-3 sentences max) with:
|
||||
1. Overall assessment (Bullish/Bearish/Neutral)
|
||||
2. Key risk factors or opportunities
|
||||
3. Clear action recommendation (Enter/Wait/Avoid)
|
||||
Provide a detailed analysis categorized into three distinct trader profiles based on the data.
|
||||
|
||||
Format as JSON:
|
||||
{
|
||||
"longTerm": {
|
||||
"assessment": "BULLISH|BEARISH|NEUTRAL",
|
||||
"summary": "Brief 1-2 sentence summary",
|
||||
"keyFactors": ["factor1", "factor2", "factor3"],
|
||||
"recommendation": "ENTER|WAIT|AVOID",
|
||||
"reasoning": "Brief explanation",
|
||||
"riskLevel": "LOW|MEDIUM|HIGH",
|
||||
"timeHorizon": "INTRADAY|SWING|POSITION"
|
||||
"moatAndFundamentals": "Brief analysis of economic moat, EPS growth, and fundamental ratios (P/E, P/B)",
|
||||
"recommendation": "ENTER|WAIT|AVOID"
|
||||
},
|
||||
"swing": {
|
||||
"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
|
||||
|
|
@ -346,13 +352,22 @@ Format as JSON:
|
|||
// Fallback: create structured response from text
|
||||
console.warn('Failed to parse AI response as JSON, using fallback');
|
||||
analysis = {
|
||||
longTerm: {
|
||||
assessment: direction,
|
||||
summary: responseText.substring(0, 200),
|
||||
keyFactors: [],
|
||||
recommendation: score >= 2.0 ? 'ENTER' : 'WAIT',
|
||||
reasoning: responseText,
|
||||
riskLevel: score < 2.0 ? 'HIGH' : score < 5.0 ? 'MEDIUM' : 'LOW',
|
||||
timeHorizon: 'INTRADAY'
|
||||
moatAndFundamentals: 'Unable to perform deep fundamental analysis. Please review ratios manually.',
|
||||
recommendation: score >= 5.0 ? 'ENTER' : 'WAIT'
|
||||
},
|
||||
swing: {
|
||||
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,
|
||||
error: error.message,
|
||||
analysis: {
|
||||
assessment: 'NEUTRAL',
|
||||
summary: 'Analysis unavailable',
|
||||
keyFactors: [],
|
||||
recommendation: 'WAIT',
|
||||
reasoning: 'Unable to generate analysis',
|
||||
riskLevel: 'MEDIUM',
|
||||
timeHorizon: 'INTRADAY'
|
||||
longTerm: { assessment: 'NEUTRAL', moatAndFundamentals: 'Analysis unavailable', recommendation: 'WAIT' },
|
||||
swing: { assessment: 'NEUTRAL', technicalSetup: 'Analysis unavailable', recommendation: 'WAIT' },
|
||||
dayTrade: { assessment: 'NEUTRAL', volatilityAndLiquidity: 'Analysis unavailable', recommendation: 'WAIT' },
|
||||
summary: 'Analysis unavailable'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/Badge';
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { Bell, TrendingUp, TrendingDown, AlertTriangle, Calendar, Clock } from 'lucide-react';
|
||||
import { requestNotificationPermission, sendNotification } from '@/utils/notifications';
|
||||
import { getApiUrl } from '@/config/api';
|
||||
|
||||
export default function AlertsFeed() {
|
||||
const [alerts, setAlerts] = useState([]);
|
||||
|
|
|
|||
|
|
@ -311,9 +311,9 @@ function FundamentalsTab({ f }) {
|
|||
{
|
||||
title: '💰 Valuation',
|
||||
metrics: [
|
||||
{ label: 'P/E Ratio (TTM)', value: fmt(f.pe_ttm, 1) },
|
||||
{ label: 'P/E Ratio (Fwd)', value: fmt(f.pe_forward, 1) },
|
||||
{ label: 'Price / Book', value: fmt(f.price_to_book, 2) },
|
||||
{ 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), 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), 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: 'EV / EBITDA', value: fmt(f.ev_ebitda, 1) },
|
||||
{ label: 'Market Cap', value: fmtLarge(f.market_cap) },
|
||||
|
|
@ -337,7 +337,7 @@ function FundamentalsTab({ f }) {
|
|||
{ label: 'EBITDA Margin', value: fmtPct(f.ebitda_margins) },
|
||||
{ label: 'Operating Margin', value: fmtPct(f.operating_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) },
|
||||
]
|
||||
},
|
||||
|
|
@ -373,7 +373,7 @@ function FundamentalsTab({ f }) {
|
|||
{ label: 'Inst. Ownership', value: fmtPct(f.held_pct_institutions) },
|
||||
{ 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: '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 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
aiAnalysis.analysis.assessment === 'BULLISH' ? 'success' :
|
||||
aiAnalysis.analysis.assessment === 'BEARISH' ? 'destructive' : 'secondary'
|
||||
}
|
||||
className="text-xs"
|
||||
>
|
||||
{aiAnalysis.analysis.assessment}
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-slate-200 bg-slate-700/30 p-3 rounded border border-slate-600/50">
|
||||
<strong>Summary:</strong> {aiAnalysis.analysis.summary}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Long Term */}
|
||||
{aiAnalysis.analysis.longTerm && (
|
||||
<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">Long-Term Investor</div>
|
||||
<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]">
|
||||
{aiAnalysis.analysis.longTerm.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 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-sm text-slate-200">{aiAnalysis.analysis.summary}</p>
|
||||
|
||||
{aiAnalysis.analysis.keyFactors && aiAnalysis.analysis.keyFactors.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">Key Factors:</div>
|
||||
<ul className="text-xs text-slate-300 space-y-1 list-disc list-inside">
|
||||
{aiAnalysis.analysis.keyFactors.map((factor, idx) => (
|
||||
<li key={idx}>{factor}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{aiAnalysis.analysis.longTerm.moatAndFundamentals}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{aiAnalysis.analysis.reasoning && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">Reasoning:</div>
|
||||
<p className="text-xs text-slate-300">{aiAnalysis.analysis.reasoning}</p>
|
||||
{/* Swing Trade */}
|
||||
{aiAnalysis.analysis.swing && (
|
||||
<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">Swing Trader</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>
|
||||
|
|
|
|||
|
|
@ -60,56 +60,64 @@ export function AnalysisSection({ row, hasPhase1Data, onPhase1Click }) {
|
|||
)}
|
||||
|
||||
{aiAnalysis && aiAnalysis.analysis && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<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 className="space-y-4">
|
||||
<div className="text-sm text-slate-200 bg-slate-700/30 p-2 rounded border border-slate-600/50">
|
||||
<strong>Summary:</strong> {aiAnalysis.analysis.summary}
|
||||
</div>
|
||||
|
||||
{aiAnalysis.analysis.keyFactors && aiAnalysis.analysis.keyFactors.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">Key Factors:</div>
|
||||
<ul className="text-xs text-slate-300 space-y-1">
|
||||
{aiAnalysis.analysis.keyFactors.map((factor, idx) => (
|
||||
<li key={idx}>• {factor}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{/* Long Term */}
|
||||
{aiAnalysis.analysis.longTerm && (
|
||||
<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">Long-Term Investor</div>
|
||||
<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]">
|
||||
{aiAnalysis.analysis.longTerm.assessment}
|
||||
</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 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">Reasoning:</div>
|
||||
<p className="text-xs text-slate-300">{aiAnalysis.analysis.reasoning}</p>
|
||||
{/* Swing Trade */}
|
||||
{aiAnalysis.analysis.swing && (
|
||||
<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">Swing Trader</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">
|
||||
<span>Time Horizon: {aiAnalysis.analysis.timeHorizon}</span>
|
||||
{/* 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 className="flex items-center gap-4 text-xs text-slate-500 pt-2 border-t border-slate-700">
|
||||
{aiAnalysis.timestamp && (
|
||||
<span>• {new Date(aiAnalysis.timestamp).toLocaleTimeString()}</span>
|
||||
<span>Generated: {new Date(aiAnalysis.timestamp).toLocaleTimeString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
export const API_CONFIG = {
|
||||
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'),
|
||||
retryAttempts: parseInt(import.meta.env.VITE_API_RETRY_ATTEMPTS || '3'),
|
||||
retryDelay: parseInt(import.meta.env.VITE_API_RETRY_DELAY || '1000')
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ spec:
|
|||
env:
|
||||
- name: NODE_ENV
|
||||
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:
|
||||
tcpSocket:
|
||||
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