@@ -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
2132export 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 >
0 commit comments