Skip to content

Commit bb2126c

Browse files
committed
feat: add lock strategy styling
1 parent be443d2 commit bb2126c

4 files changed

Lines changed: 290 additions & 3 deletions

File tree

frontend/src/App.css

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2834,6 +2834,113 @@ input[type=number] {
28342834
pointer-events: none;
28352835
}
28362836

2837+
.lock-strategy-btn {
2838+
position: relative;
2839+
background: rgba(0, 255, 157, 0.06);
2840+
border: 1px solid rgba(0, 255, 157, 0.4);
2841+
border-radius: 10px;
2842+
color: #00ff9d;
2843+
font-size: 12px;
2844+
font-weight: 700;
2845+
letter-spacing: 0.02em;
2846+
cursor: pointer;
2847+
outline: none;
2848+
overflow: hidden;
2849+
transition: all 0.25s ease;
2850+
}
2851+
2852+
.lock-strategy-btn:hover:not(:disabled) {
2853+
background: rgba(0, 255, 157, 0.14);
2854+
border-color: #00ff9d;
2855+
box-shadow:
2856+
0 0 10px rgba(0, 255, 157, 0.45),
2857+
0 0 24px rgba(0, 255, 157, 0.2),
2858+
inset 0 0 8px rgba(0, 255, 157, 0.1);
2859+
transform: translateY(-1px);
2860+
}
2861+
2862+
.lock-strategy-btn:active:not(:disabled) {
2863+
transform: translateY(0);
2864+
}
2865+
2866+
.lock-strategy-btn:disabled {
2867+
opacity: 0.4;
2868+
cursor: not-allowed;
2869+
border-color: var(--border-glass);
2870+
color: var(--text-secondary);
2871+
box-shadow: none;
2872+
}
2873+
2874+
/* Subtle animated neon sweep on hover */
2875+
.lock-strategy-btn::before {
2876+
content: '';
2877+
position: absolute;
2878+
top: 0;
2879+
left: -75%;
2880+
width: 50%;
2881+
height: 100%;
2882+
background: linear-gradient(
2883+
120deg,
2884+
transparent 0%,
2885+
rgba(0, 255, 157, 0.35) 50%,
2886+
transparent 100%
2887+
);
2888+
transform: skewX(-20deg);
2889+
transition: left 0.6s ease;
2890+
pointer-events: none;
2891+
}
2892+
2893+
.lock-strategy-btn:hover:not(:disabled)::before {
2894+
left: 125%;
2895+
}
2896+
2897+
/* Target / Stop Loss number inputs inside the lock-strategy-panel */
2898+
.lock-strategy-panel input[type='number'] {
2899+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
2900+
}
2901+
2902+
.lock-strategy-panel input[type='number']:focus {
2903+
border-color: var(--neon-cyan);
2904+
box-shadow: 0 0 0 2px rgba(0, 242, 254, 0.15);
2905+
}
2906+
2907+
/* Hide native number spinner arrows for a cleaner look (optional) */
2908+
.lock-strategy-panel input[type='number']::-webkit-outer-spin-button,
2909+
.lock-strategy-panel input[type='number']::-webkit-inner-spin-button {
2910+
-webkit-appearance: none;
2911+
margin: 0;
2912+
}
2913+
2914+
/* Locked state badge (shown after a successful lockInStrategy call) */
2915+
.lock-strategy-badge {
2916+
display: flex;
2917+
align-items: center;
2918+
justify-content: center;
2919+
gap: 6px;
2920+
padding: 10px 14px;
2921+
font-size: 12px;
2922+
font-weight: 700;
2923+
letter-spacing: 0.02em;
2924+
color: var(--bull-green);
2925+
background: rgba(0, 255, 157, 0.12);
2926+
border: 1px solid rgba(0, 255, 157, 0.5);
2927+
border-radius: 10px;
2928+
box-shadow: 0 0 12px rgba(0, 255, 157, 0.3);
2929+
animation: lock-badge-pop 0.35s ease;
2930+
}
2931+
2932+
@keyframes lock-badge-pop {
2933+
0% {
2934+
transform: scale(0.92);
2935+
opacity: 0;
2936+
}
2937+
100% {
2938+
transform: scale(1);
2939+
opacity: 1;
2940+
}
2941+
}
2942+
2943+
28372944
/* ─── Animations ──────────────────────────── */
28382945
@keyframes backdropFadeIn {
28392946
from { opacity: 0; }

frontend/src/App.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export default function App() {
7878
const [loadingInsight, setLoadingInsight] = useState<boolean>(false);
7979
const [insightError, setInsightError] = useState<string | null>(null);
8080

81+
// Lock-In Strategy State (live MLOps tracking via strategy_logs)
82+
const [lockingStrategy, setLockingStrategy] = useState<boolean>(false);
83+
const [strategyLocked, setStrategyLocked] = useState<boolean>(false);
84+
8185

8286

8387
const fetchWatchlistData = async () => {
@@ -290,6 +294,7 @@ export default function App() {
290294
useEffect(() => {
291295
setInsight(null);
292296
setInsightError(null);
297+
setStrategyLocked(false);
293298
}, [activeTicker]);
294299

295300
// 4. WebSocket Live Price Streaming Subscription (graphql-transport-ws protocol)
@@ -547,6 +552,7 @@ export default function App() {
547552
setLoadingInsight(true);
548553
setInsightError(null);
549554
setInsight(null);
555+
setStrategyLocked(false);
550556

551557
try {
552558
const insightData = await graphqlRequest(`
@@ -571,6 +577,52 @@ export default function App() {
571577
}
572578
};
573579

580+
// 8b. Lock In Strategy — populates strategy_logs for live AI-vs-User win-rate tracking.
581+
// NOTE: the backend's lock_in_strategy resolver computes its own AI target/stop-loss
582+
// independently (ONNX + ATR-14) and stores it as ai_entry/ai_target/ai_stop_loss.
583+
// The values passed here are the trader's OWN entry/target/stop-loss, stored as
584+
// user_entry/user_target/user_stop_loss and compared against the AI's numbers.
585+
const handleLockInStrategy = async (target: number, stopLoss: number) => {
586+
if (!insight || !activeTicker || lockingStrategy || strategyLocked) return;
587+
588+
// Entry = live current price for the active ticker (not part of the insight payload)
589+
const currentPrice = watchlistQuotes[activeTicker]?.price ?? activeStats?.close ?? null;
590+
591+
if (currentPrice === null || isNaN(target) || isNaN(stopLoss)) {
592+
addToast('Missing entry, target, or stop-loss data — cannot lock in strategy.', 'alert');
593+
return;
594+
}
595+
596+
setLockingStrategy(true);
597+
try {
598+
const data = await graphqlRequest(`
599+
mutation LockInStrategy($ticker: String!, $entry: Float!, $target: Float!, $stopLoss: Float!) {
600+
lockInStrategy(ticker: $ticker, entry: $entry, target: $target, stopLoss: $stopLoss) {
601+
success
602+
message
603+
}
604+
}
605+
`, {
606+
ticker: activeTicker,
607+
entry: currentPrice,
608+
target: target,
609+
stopLoss: stopLoss,
610+
});
611+
612+
if (data.lockInStrategy?.success) {
613+
setStrategyLocked(true);
614+
addToast(`Strategy for ${activeTicker} locked in! Tracking live outcome...`, 'success');
615+
} else {
616+
addToast(data.lockInStrategy?.message || 'Failed to lock in strategy.', 'alert');
617+
}
618+
} catch (err: any) {
619+
console.error('Lock in strategy error:', err);
620+
addToast(err.message || 'Failed to lock in strategy.', 'alert');
621+
} finally {
622+
setLockingStrategy(false);
623+
}
624+
};
625+
574626
// 9. Razorpay Payment Integration
575627
const handlePayment = async (amountInRupees: number) => {
576628
if (!user) return;
@@ -684,7 +736,11 @@ export default function App() {
684736
onCreateAlert={handleCreateAlert}
685737
onDeactivateAlert={handleDeactivateAlert}
686738
onTriggerInsight={triggerAIInsight}
687-
onResetInsight={() => { setInsight(null); setInsightError(null); }}
739+
onResetInsight={() => { setInsight(null); setInsightError(null); setStrategyLocked(false); }}
740+
currentPrice={watchlistQuotes[activeTicker]?.price ?? activeStats?.close ?? null}
741+
lockingStrategy={lockingStrategy}
742+
strategyLocked={strategyLocked}
743+
onLockInStrategy={handleLockInStrategy}
688744
onOpenRecharge={() => setCurrentView('upgrade')}
689745
onLogout={handleLogout}
690746
onLogoClick={() => setCurrentView('landing')}

frontend/src/components/AIAnalyst.tsx

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ interface AIAnalystProps {
1616
insightError: string | null;
1717
onTriggerInsight: (tradingStyle: string, riskTolerance: string) => void;
1818
onResetInsight: () => void;
19+
// Lock-In Strategy props.
20+
// NOTE: the backend's lock_in_strategy resolver independently computes its
21+
// own AI target/stop-loss via ONNX + ATR-14 (ai_entry/ai_target/ai_stop_loss)
22+
// and logs them to strategy_logs alongside whatever the user submits here
23+
// (stored as user_entry/user_target/user_stop_loss). GeminiInsightType has
24+
// no target/stopLoss fields, so the trader's own target/stop must be
25+
// captured in the UI rather than read off the insight object.
26+
currentPrice?: number | null;
27+
lockingStrategy?: boolean;
28+
strategyLocked?: boolean;
29+
onLockInStrategy?: (target: number, stopLoss: number) => void;
1930
}
2031

2132
export default function AIAnalyst({
@@ -26,7 +37,13 @@ export default function AIAnalyst({
2637
insightError,
2738
onTriggerInsight,
2839
onResetInsight,
40+
currentPrice = null,
41+
lockingStrategy = false,
42+
strategyLocked = false,
43+
onLockInStrategy,
2944
}: AIAnalystProps) {
45+
const [targetPriceInput, setTargetPriceInput] = useState<string>('');
46+
const [stopLossInput, setStopLossInput] = useState<string>('');
3047
const [activeTab, setActiveTab] = useState<'analysis' | 'history'>('analysis');
3148
const [selectedHistoryItem, setSelectedHistoryItem] = useState<any | null>(null);
3249
const [tradingStyle, setTradingStyle] = useState('swing_trading');
@@ -42,6 +59,24 @@ export default function AIAnalyst({
4259
}
4360
}, [loadingInsight]);
4461

62+
// Prefill Target Price / Stop Loss with sensible defaults whenever a fresh
63+
// strategy is generated, based on the bias direction and current price.
64+
// The trader can edit these before locking in -- they are stored as
65+
// user_target / user_stop_loss server-side and compared against the
66+
// backend's own ATR-14-computed AI levels.
67+
useEffect(() => {
68+
if (insight && currentPrice != null) {
69+
const isBullish = insight.bullishProbability >= 50;
70+
const defaultTarget = isBullish ? currentPrice * 1.05 : currentPrice * 0.95;
71+
const defaultStopLoss = isBullish ? currentPrice * 0.98 : currentPrice * 1.02;
72+
setTargetPriceInput(defaultTarget.toFixed(2));
73+
setStopLossInput(defaultStopLoss.toFixed(2));
74+
} else if (!insight) {
75+
setTargetPriceInput('');
76+
setStopLossInput('');
77+
}
78+
}, [insight, currentPrice]);
79+
4580
// Parse inline markdown: **bold**, *italic*
4681
const parseInline = (text: string, keyPrefix: string) => {
4782
// Handle **bold** and *italic* (in that order, bold first)
@@ -507,6 +542,78 @@ export default function AIAnalyst({
507542
<div className="insight-reason-body">
508543
{formatReason(insight.reason)}
509544
</div>
545+
546+
{/* Lock In Strategy — captures the trader's own entry/target/stop-loss,
547+
logged as user_entry/user_target/user_stop_loss in strategy_logs and
548+
compared server-side against the backend's own ATR-14 AI levels. */}
549+
<div className="lock-strategy-panel" style={{ marginTop: '16px', paddingTop: '12px', borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
550+
{strategyLocked ? (
551+
<div className="lock-strategy-badge" style={{ width: '100%', textAlign: 'center' }}>
552+
✓ Strategy Locked
553+
</div>
554+
) : (
555+
<>
556+
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
557+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
558+
<label style={{ fontSize: '10px', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
559+
Entry
560+
</label>
561+
<div style={{ padding: '8px 10px', borderRadius: '8px', border: '1px solid var(--border-glass)', background: 'rgba(255,255,255,0.02)', color: 'var(--text-primary)', fontSize: '12px' }}>
562+
{currentPrice != null ? `$${currentPrice.toFixed(2)}` : '—'}
563+
</div>
564+
</div>
565+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
566+
<label htmlFor="lock-target-input" style={{ fontSize: '10px', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
567+
Target
568+
</label>
569+
<input
570+
id="lock-target-input"
571+
type="number"
572+
step="0.01"
573+
value={targetPriceInput}
574+
onChange={(e) => setTargetPriceInput(e.target.value)}
575+
style={{ width: '100%', boxSizing: 'border-box', padding: '8px 10px', borderRadius: '8px', border: '1px solid var(--border-glass)', background: 'rgba(255,255,255,0.02)', color: 'var(--text-primary)', fontSize: '12px', outline: 'none' }}
576+
/>
577+
</div>
578+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
579+
<label htmlFor="lock-stoploss-input" style={{ fontSize: '10px', color: 'var(--text-secondary)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
580+
Stop Loss
581+
</label>
582+
<input
583+
id="lock-stoploss-input"
584+
type="number"
585+
step="0.01"
586+
value={stopLossInput}
587+
onChange={(e) => setStopLossInput(e.target.value)}
588+
style={{ width: '100%', boxSizing: 'border-box', padding: '8px 10px', borderRadius: '8px', border: '1px solid var(--border-glass)', background: 'rgba(255,255,255,0.02)', color: 'var(--text-primary)', fontSize: '12px', outline: 'none' }}
589+
/>
590+
</div>
591+
</div>
592+
<button
593+
className="lock-strategy-btn"
594+
disabled={
595+
lockingStrategy ||
596+
!onLockInStrategy ||
597+
currentPrice == null ||
598+
targetPriceInput.trim() === '' ||
599+
stopLossInput.trim() === '' ||
600+
isNaN(parseFloat(targetPriceInput)) ||
601+
isNaN(parseFloat(stopLossInput))
602+
}
603+
onClick={() => {
604+
const target = parseFloat(targetPriceInput);
605+
const stopLoss = parseFloat(stopLossInput);
606+
if (onLockInStrategy && !isNaN(target) && !isNaN(stopLoss)) {
607+
onLockInStrategy(target, stopLoss);
608+
}
609+
}}
610+
style={{ width: '100%', padding: '10px 14px', fontSize: '12px' }}
611+
>
612+
{lockingStrategy ? 'Locking In...' : '🔒 Lock In Strategy'}
613+
</button>
614+
</>
615+
)}
616+
</div>
510617
<div style={{ marginTop: '16px', paddingTop: '12px', borderTop: '1px solid rgba(255, 255, 255, 0.05)', fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '6px', lineHeight: '140%', textAlign: 'left' }}>
511618
<span style={{ color: 'var(--neon-cyan)', fontWeight: 600 }}>Disclaimer:</span>
512619
<span>QuantIQ AI is an automated strategy assistant and can make mistakes. All analysis is for informational purposes only and should not be considered financial advice. Please verify financial data independently.</span>

frontend/src/pages/Dashboard.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import Navbar from '../components/Navbar';
32
import WatchlistSidebar from '../components/WatchlistSidebar';
43
import StockChart from '../components/StockChart';
@@ -33,6 +32,15 @@ interface DashboardProps {
3332
onLogoClick?: () => void;
3433
onAvatarUpload?: (newUrl: string) => void;
3534
indices: any[];
35+
// Lock-In Strategy — passed straight through to AIAnalyst.
36+
// See AIAnalyst.tsx: entry is the live current price, target/stopLoss are
37+
// the trader's own numbers (stored as user_entry/user_target/user_stop_loss
38+
// in strategy_logs, compared server-side against the backend's own
39+
// ATR-14-computed AI levels).
40+
currentPrice: number | null;
41+
lockingStrategy: boolean;
42+
strategyLocked: boolean;
43+
onLockInStrategy: (target: number, stopLoss: number) => void;
3644
}
3745

3846
export default function Dashboard({
@@ -60,7 +68,12 @@ export default function Dashboard({
6068
onLogout,
6169
onLogoClick,
6270
onAvatarUpload,
63-
indices,}: DashboardProps) {
71+
indices,
72+
currentPrice,
73+
lockingStrategy,
74+
strategyLocked,
75+
onLockInStrategy,
76+
}: DashboardProps) {
6477
return (
6578
<div className="app-container animate-fade">
6679
{/* Header / Navigation */}
@@ -113,6 +126,10 @@ export default function Dashboard({
113126
insightError={insightError}
114127
onTriggerInsight={onTriggerInsight}
115128
onResetInsight={onResetInsight}
129+
currentPrice={currentPrice}
130+
lockingStrategy={lockingStrategy}
131+
strategyLocked={strategyLocked}
132+
onLockInStrategy={onLockInStrategy}
116133
/>
117134
</section>
118135

0 commit comments

Comments
 (0)