Add human status labels to all indicators and small UI tweaks.
Interpret RSI, ADX, EMA, ATR, volume profile, and pivots like EMA cross; tighten exit timeline spacing; risk presets are 1/2/3/5%.
This commit is contained in:
@@ -22,11 +22,11 @@ export function ExitPlanPanel({ plan, direction }: { plan: ExitPlan; direction:
|
|||||||
<span className="text-[11.5px] font-medium text-gray-300">{plan.headline}</span>
|
<span className="text-[11.5px] font-medium text-gray-300">{plan.headline}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ol className="relative mt-4">
|
<ol className="relative mt-3">
|
||||||
{steps.map((step, i) => {
|
{steps.map((step, i) => {
|
||||||
const isLast = i === steps.length - 1;
|
const isLast = i === steps.length - 1;
|
||||||
return (
|
return (
|
||||||
<li key={step.title} className="relative flex gap-3">
|
<li key={step.title} className="relative flex gap-2.5">
|
||||||
{/* Spine column: continuous connector from this node into the next */}
|
{/* Spine column: continuous connector from this node into the next */}
|
||||||
<div className="relative flex w-5 shrink-0 flex-col items-center self-stretch">
|
<div className="relative flex w-5 shrink-0 flex-col items-center self-stretch">
|
||||||
<span
|
<span
|
||||||
@@ -43,15 +43,15 @@ export function ExitPlanPanel({ plan, direction }: { plan: ExitPlan; direction:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`min-w-0 flex-1 pt-0.5 ${isLast ? 'pb-0' : 'pb-5'}`}>
|
<div className={`min-w-0 flex-1 pt-0.5 ${isLast ? 'pb-0' : 'pb-2.5'}`}>
|
||||||
<p className="text-[12px] font-medium text-gray-200">{step.title}</p>
|
<p className="text-[12px] font-medium leading-tight text-gray-200">{step.title}</p>
|
||||||
{step.primary && (
|
{step.primary && (
|
||||||
<p className="num mt-0.5 text-[13px] font-semibold tracking-tight text-gray-100">
|
<p className="num mt-0.5 text-[13px] font-semibold leading-tight tracking-tight text-gray-100">
|
||||||
{step.primary}
|
{step.primary}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{step.detail && (
|
{step.detail && (
|
||||||
<p className="mt-0.5 text-[11.5px] leading-snug text-gray-500">{step.detail}</p>
|
<p className="mt-0.5 text-[11px] leading-snug text-gray-500">{step.detail}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -79,25 +79,108 @@ function clusterLevels(nums: number[], refPrice?: number): { shown: string[]; mo
|
|||||||
return { shown, more: clusters.length - Math.min(clusters.length, MAX_CLUSTERS) };
|
return { shown, more: clusters.length - Math.min(clusters.length, MAX_CLUSTERS) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The one-line human read of an indicator, where its meaning is standard. */
|
/**
|
||||||
function interpretation(result: IndicatorResult): { text: string; tone: string } | null {
|
* One-line human read of an indicator — same slot as EMA cross's
|
||||||
|
* bullish/bearish/neutral. Vocab matches what the metric means (directional
|
||||||
|
* vs regime), not forced into bullish/bearish for everything.
|
||||||
|
*/
|
||||||
|
function interpretation(
|
||||||
|
result: IndicatorResult,
|
||||||
|
refPrice?: number,
|
||||||
|
): { text: string; tone: string } | null {
|
||||||
const v = result.values as Record<string, unknown>;
|
const v = result.values as Record<string, unknown>;
|
||||||
const num = (k: string) => (typeof v[k] === 'number' ? (v[k] as number) : null);
|
const num = (k: string) => (typeof v[k] === 'number' ? (v[k] as number) : null);
|
||||||
switch (result.indicator_type) {
|
const type = result.indicator_type.toLowerCase();
|
||||||
case 'RSI': {
|
|
||||||
const rsi = num('rsi') ?? num('RSI') ?? num('value');
|
switch (type) {
|
||||||
|
case 'rsi': {
|
||||||
|
const rsi = num('rsi') ?? num('value');
|
||||||
if (rsi == null) return null;
|
if (rsi == null) return null;
|
||||||
if (rsi >= 70) return { text: 'overbought', tone: 'text-red-300' };
|
if (rsi >= 70) return { text: 'overbought', tone: 'text-red-300' };
|
||||||
if (rsi <= 30) return { text: 'oversold', tone: 'text-emerald-300' };
|
if (rsi <= 30) return { text: 'oversold', tone: 'text-emerald-300' };
|
||||||
return { text: 'neutral zone', tone: 'text-gray-400' };
|
if (rsi >= 55) return { text: 'bullish momentum', tone: 'text-emerald-300' };
|
||||||
|
if (rsi <= 45) return { text: 'bearish momentum', tone: 'text-red-300' };
|
||||||
|
return { text: 'neutral', tone: 'text-gray-400' };
|
||||||
}
|
}
|
||||||
case 'ADX': {
|
|
||||||
const adx = num('adx') ?? num('ADX') ?? num('value');
|
case 'adx': {
|
||||||
|
const adx = num('adx') ?? num('value');
|
||||||
|
const plusDi = num('plus_di');
|
||||||
|
const minusDi = num('minus_di');
|
||||||
if (adx == null) return null;
|
if (adx == null) return null;
|
||||||
if (adx >= 40) return { text: 'strong trend', tone: 'text-emerald-300' };
|
if (adx < 20) return { text: 'no trend / range', tone: 'text-gray-400' };
|
||||||
if (adx >= 25) return { text: 'trending', tone: 'text-gray-300' };
|
if (adx < 25) return { text: 'trend forming', tone: 'text-gray-300' };
|
||||||
return { text: 'weak / no trend', tone: 'text-gray-400' };
|
const bullish = plusDi != null && minusDi != null && plusDi > minusDi;
|
||||||
|
const bearish = plusDi != null && minusDi != null && minusDi > plusDi;
|
||||||
|
if (adx >= 40) {
|
||||||
|
if (bullish) return { text: 'strong bullish trend', tone: 'text-emerald-300' };
|
||||||
|
if (bearish) return { text: 'strong bearish trend', tone: 'text-red-300' };
|
||||||
|
return { text: 'strong trend', tone: 'text-emerald-300' };
|
||||||
|
}
|
||||||
|
if (bullish) return { text: 'bullish trend', tone: 'text-emerald-300' };
|
||||||
|
if (bearish) return { text: 'bearish trend', tone: 'text-red-300' };
|
||||||
|
return { text: 'trending', tone: 'text-gray-300' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'ema': {
|
||||||
|
const ema = num('ema');
|
||||||
|
const close = num('latest_close');
|
||||||
|
if (ema == null || close == null || ema === 0) return null;
|
||||||
|
const pct = ((close - ema) / ema) * 100;
|
||||||
|
if (pct >= 0.5) return { text: 'above EMA · bullish', tone: 'text-emerald-300' };
|
||||||
|
if (pct <= -0.5) return { text: 'below EMA · bearish', tone: 'text-red-300' };
|
||||||
|
return { text: 'at EMA · neutral', tone: 'text-gray-400' };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'atr': {
|
||||||
|
const atrPct = num('atr_percent');
|
||||||
|
if (atrPct == null) return null;
|
||||||
|
if (atrPct < 1.5) return { text: 'compressed', tone: 'text-emerald-300' };
|
||||||
|
if (atrPct < 3) return { text: 'normal volatility', tone: 'text-gray-400' };
|
||||||
|
if (atrPct < 5) return { text: 'elevated', tone: 'text-amber-300' };
|
||||||
|
return { text: 'high volatility', tone: 'text-red-300' };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'volume_profile': {
|
||||||
|
const vaLow = num('value_area_low');
|
||||||
|
const vaHigh = num('value_area_high');
|
||||||
|
const poc = num('poc');
|
||||||
|
const price = refPrice ?? num('latest_close');
|
||||||
|
if (price == null || vaLow == null || vaHigh == null) return null;
|
||||||
|
if (price > vaHigh) return { text: 'above value · extended', tone: 'text-amber-300' };
|
||||||
|
if (price < vaLow) return { text: 'below value · discounted', tone: 'text-emerald-300' };
|
||||||
|
// Inside value area: near POC vs mid-range.
|
||||||
|
if (poc != null) {
|
||||||
|
const span = Math.max(vaHigh - vaLow, Math.abs(price) * 0.001);
|
||||||
|
if (Math.abs(price - poc) / span <= 0.15) {
|
||||||
|
return { text: 'at value · balanced', tone: 'text-gray-300' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { text: 'in value area', tone: 'text-gray-400' };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pivot_points': {
|
||||||
|
const price = refPrice;
|
||||||
|
if (price == null || !(price > 0)) return null;
|
||||||
|
const highs = parseNums(v.swing_highs) ?? [];
|
||||||
|
const lows = parseNums(v.swing_lows) ?? [];
|
||||||
|
// parseNums requires ≥2 elements; allow singletons for pivots.
|
||||||
|
const rawHighs = Array.isArray(v.swing_highs)
|
||||||
|
? (v.swing_highs as unknown[]).filter((x): x is number => typeof x === 'number')
|
||||||
|
: highs;
|
||||||
|
const rawLows = Array.isArray(v.swing_lows)
|
||||||
|
? (v.swing_lows as unknown[]).filter((x): x is number => typeof x === 'number')
|
||||||
|
: lows;
|
||||||
|
if (rawHighs.length === 0 && rawLows.length === 0) return null;
|
||||||
|
const near = (level: number) => Math.abs(level - price) / price <= 0.02;
|
||||||
|
const nearHigh = rawHighs.some(near);
|
||||||
|
const nearLow = rawLows.some(near);
|
||||||
|
if (nearHigh && nearLow) return { text: 'congested structure', tone: 'text-gray-400' };
|
||||||
|
if (nearLow) return { text: 'near support pivot', tone: 'text-emerald-300' };
|
||||||
|
if (nearHigh) return { text: 'near resistance pivot', tone: 'text-red-300' };
|
||||||
|
return { text: 'between pivots', tone: 'text-gray-400' };
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -164,7 +247,7 @@ function IndicatorCard({ symbol, type, refPrice }: { symbol: string; type: strin
|
|||||||
{query.data && (
|
{query.data && (
|
||||||
<>
|
<>
|
||||||
{(() => {
|
{(() => {
|
||||||
const read = interpretation(query.data);
|
const read = interpretation(query.data, refPrice);
|
||||||
return read ? (
|
return read ? (
|
||||||
<p className={`mt-1.5 text-sm font-semibold ${read.tone}`}>{read.text}</p>
|
<p className={`mt-1.5 text-sm font-semibold ${read.tone}`}>{read.text}</p>
|
||||||
) : null;
|
) : null;
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ function SetupCard({ setup, action, currentPrice, risk, regime, exitPolicy, sele
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RISK_PRESETS = [0.5, 1, 2, 3];
|
const RISK_PRESETS = [1, 2, 3, 5];
|
||||||
|
|
||||||
/** Compact, set-once sizing controls: a clean account field (no spinners) and a
|
/** Compact, set-once sizing controls: a clean account field (no spinners) and a
|
||||||
* segmented risk-% selector — risk is almost always one of a few values. */
|
* segmented risk-% selector — risk is almost always one of a few values. */
|
||||||
|
|||||||
Reference in New Issue
Block a user