feat: add full dashboard monitoring + FEATURES.md documentation
- Create docs/FEATURES.md with complete feature reference (14 entry filters, 12 exit conditions, backtest history, risk modes, session rules, auto-trainer, active components table, architecture diagram) - Extend main_live.py _write_dashboard_status() with 10 new data sections: entryFilters, riskMode, cooldown, timeFilter, sessionMultiplier, positionDetails, autoTrainer, performance, marketClose, h1BiasDetails. Add filter tracking at each checkpoint in _trading_iteration() and 7 helper methods. - Add 9 TypeScript interfaces and extend TradingStatus in trading.ts - Create BotStatusCard (risk mode, cooldown bar, AUC, uptime, market close) and EntryFilterCard (14 filters with pass/block/skip icons) - Enhance SessionCard (lot multiplier badge + time filter status), RiskCard (risk mode badge + total loss progress bar), PositionsCard (expandable per-position details with momentum, TP probability) - Update page.tsx layout: BotStatusCard replaces SettingsCard in Row 2, EntryFilterCard added to Row 3 sidebar - Add API defaults for all new fields Dashboard now monitors 100% of bot features. Verified: Next.js build 0 errors, bot + API + dashboard all run clean, Docker rebuilt OK. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cb41bfe5ba
commit
61877480b3
@@ -54,6 +54,16 @@ DEFAULT_STATUS = {
|
||||
"regime": {"name": "", "volatility": 0.0, "confidence": 0.0},
|
||||
"positions": [],
|
||||
"logs": [],
|
||||
"entryFilters": [],
|
||||
"riskMode": {"mode": "unknown", "reason": "", "recommendedLot": 0, "maxAllowedLot": 0, "totalLoss": 0, "maxTotalLoss": 0, "remainingDailyRisk": 0},
|
||||
"cooldown": {"active": False, "secondsRemaining": 0, "totalSeconds": 150},
|
||||
"timeFilter": {"wibHour": 0, "isBlocked": False, "blockedHours": [9, 21]},
|
||||
"sessionMultiplier": 1.0,
|
||||
"positionDetails": [],
|
||||
"autoTrainer": {"lastRetrain": None, "currentAuc": None, "minAucThreshold": 0.65, "hoursSinceRetrain": 0, "nextRetrainHour": 5, "modelsFitted": False},
|
||||
"performance": {"loopCount": 0, "avgExecutionMs": 0, "uptimeHours": 0, "totalSessionTrades": 0, "totalSessionProfit": 0},
|
||||
"marketClose": {"hoursToDailyClose": 0, "hoursToWeekendClose": 0, "nearWeekend": False, "marketOpen": False},
|
||||
"h1BiasDetails": {"bias": "NEUTRAL", "ema20": 0, "price": 0},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
PositionsCard,
|
||||
LogCard,
|
||||
PriceChart,
|
||||
SettingsCard,
|
||||
BotStatusCard,
|
||||
EntryFilterCard,
|
||||
} from "@/components/dashboard";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
@@ -121,6 +122,8 @@ export default function Dashboard() {
|
||||
session={data.session}
|
||||
isGoldenTime={data.isGoldenTime}
|
||||
canTrade={data.canTrade}
|
||||
sessionMultiplier={data.sessionMultiplier}
|
||||
timeFilter={data.timeFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
@@ -129,11 +132,12 @@ export default function Dashboard() {
|
||||
dailyProfit={data.dailyProfit}
|
||||
consecutiveLosses={data.consecutiveLosses}
|
||||
riskPercent={data.riskPercent}
|
||||
riskMode={data.riskMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Row 2: Signals ── */}
|
||||
{/* ── Row 2: Signals + Bot Status ── */}
|
||||
<div
|
||||
className="grid gap-1.5 overflow-hidden"
|
||||
style={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
|
||||
@@ -171,11 +175,13 @@ export default function Dashboard() {
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
{data.settings ? (
|
||||
<SettingsCard settings={data.settings} />
|
||||
) : (
|
||||
<div className="glass rounded-lg h-full" />
|
||||
)}
|
||||
<BotStatusCard
|
||||
riskMode={data.riskMode}
|
||||
cooldown={data.cooldown}
|
||||
autoTrainer={data.autoTrainer}
|
||||
performance={data.performance}
|
||||
marketClose={data.marketClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,8 +194,14 @@ export default function Dashboard() {
|
||||
<PriceChart data={data.priceHistory} />
|
||||
</div>
|
||||
<div className="min-w-0 min-h-0 overflow-hidden flex flex-col gap-1.5">
|
||||
<div className="min-h-0" style={{ flex: '0 0 auto', maxHeight: '40%' }}>
|
||||
<EntryFilterCard filters={data.entryFilters || []} />
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<PositionsCard positions={data.positions} />
|
||||
<PositionsCard
|
||||
positions={data.positions}
|
||||
positionDetails={data.positionDetails}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<LogCard logs={data.logs} />
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Activity, Timer, Brain, Gauge, Clock } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { RiskMode, CooldownStatus, AutoTrainerStatus, PerformanceStatus, MarketCloseStatus } from "@/types/trading";
|
||||
|
||||
interface BotStatusCardProps {
|
||||
riskMode?: RiskMode;
|
||||
cooldown?: CooldownStatus;
|
||||
autoTrainer?: AutoTrainerStatus;
|
||||
performance?: PerformanceStatus;
|
||||
marketClose?: MarketCloseStatus;
|
||||
}
|
||||
|
||||
function getRiskModeVariant(mode: string) {
|
||||
switch (mode) {
|
||||
case "normal": return "success";
|
||||
case "recovery": return "warning";
|
||||
case "protected": return "danger";
|
||||
case "stopped": return "danger";
|
||||
default: return "secondary";
|
||||
}
|
||||
}
|
||||
|
||||
export function BotStatusCard({ riskMode, cooldown, autoTrainer, performance, marketClose }: BotStatusCardProps) {
|
||||
const mode = riskMode?.mode || "unknown";
|
||||
const aucColor = (autoTrainer?.currentAuc ?? 0) >= 0.7 ? "text-success" : (autoTrainer?.currentAuc ?? 0) >= 0.65 ? "text-warning" : "text-danger";
|
||||
|
||||
return (
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
Bot Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
{/* Risk Mode */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Risk Mode</span>
|
||||
<Badge
|
||||
variant={getRiskModeVariant(mode) as "success" | "warning" | "danger" | "secondary"}
|
||||
className={cn("text-[10px] h-4 px-1.5 uppercase", mode === "stopped" && "animate-pulse")}
|
||||
>
|
||||
{mode}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Cooldown */}
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Timer className="h-2.5 w-2.5" />
|
||||
Cooldown
|
||||
</span>
|
||||
<span className={cn("text-[10px] font-number", cooldown?.active ? "text-warning" : "text-muted-foreground/60")}>
|
||||
{cooldown?.active ? `${cooldown.secondsRemaining}s` : "Ready"}
|
||||
</span>
|
||||
</div>
|
||||
{cooldown?.active && (
|
||||
<div className="h-1 w-full bg-surface-light rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-warning transition-all duration-1000"
|
||||
style={{ width: `${cooldown.totalSeconds > 0 ? ((cooldown.totalSeconds - cooldown.secondsRemaining) / cooldown.totalSeconds) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auto Trainer */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Brain className="h-2.5 w-2.5" />
|
||||
Model AUC
|
||||
</span>
|
||||
<span className={cn("text-[10px] font-bold font-number", aucColor)}>
|
||||
{autoTrainer?.currentAuc != null ? autoTrainer.currentAuc.toFixed(3) : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Performance */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Gauge className="h-2.5 w-2.5" />
|
||||
Uptime
|
||||
</span>
|
||||
<span className="text-[10px] font-number text-foreground">
|
||||
{performance ? `${performance.uptimeHours}h | ${performance.loopCount} loops` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Exec Speed</span>
|
||||
<span className={cn("text-[10px] font-number", (performance?.avgExecutionMs ?? 0) > 50 ? "text-warning" : "text-success")}>
|
||||
{performance ? `${performance.avgExecutionMs}ms` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Market Close */}
|
||||
<div className="pt-0.5 border-t border-border flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="h-2.5 w-2.5" />
|
||||
Close
|
||||
</span>
|
||||
<span className={cn("text-[10px] font-number", marketClose?.nearWeekend ? "text-warning font-bold" : "text-muted-foreground")}>
|
||||
{marketClose ? `D:${marketClose.hoursToDailyClose}h W:${marketClose.hoursToWeekendClose}h` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Filter, Check, X, Minus } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { EntryFilter } from "@/types/trading";
|
||||
|
||||
interface EntryFilterCardProps {
|
||||
filters: EntryFilter[];
|
||||
}
|
||||
|
||||
export function EntryFilterCard({ filters }: EntryFilterCardProps) {
|
||||
const passedCount = filters.filter((f) => f.passed).length;
|
||||
const totalCount = filters.length;
|
||||
const hasBlocker = filters.some((f) => !f.passed);
|
||||
|
||||
// Find the first blocker index — filters after it were not evaluated
|
||||
const firstBlockerIdx = filters.findIndex((f) => !f.passed);
|
||||
|
||||
return (
|
||||
<Card className="glass h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Filter className="h-3.5 w-3.5" />
|
||||
Entry Filters
|
||||
{totalCount > 0 && (
|
||||
<Badge
|
||||
variant={hasBlocker ? "danger" : "success"}
|
||||
className="ml-auto text-[10px] h-4 px-1.5"
|
||||
>
|
||||
{passedCount}/{totalCount}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 min-h-0 overflow-auto">
|
||||
{totalCount === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<Minus className="h-4 w-4 text-muted-foreground/30 mb-1" />
|
||||
<p className="text-[10px] text-muted-foreground/60">Waiting for candle...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{filters.map((filter, idx) => {
|
||||
// Determine status: passed, blocked, or not evaluated
|
||||
const isNotEvaluated = firstBlockerIdx >= 0 && idx > firstBlockerIdx;
|
||||
const isBlocker = !filter.passed && idx === firstBlockerIdx;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${filter.name}-${idx}`}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-1.5 py-0.5 rounded text-[10px]",
|
||||
isBlocker && "bg-danger/10",
|
||||
isNotEvaluated && "opacity-40"
|
||||
)}
|
||||
>
|
||||
{isNotEvaluated ? (
|
||||
<Minus className="h-2.5 w-2.5 text-muted-foreground/40 flex-shrink-0" />
|
||||
) : filter.passed ? (
|
||||
<Check className="h-2.5 w-2.5 text-success flex-shrink-0" />
|
||||
) : (
|
||||
<X className="h-2.5 w-2.5 text-danger flex-shrink-0" />
|
||||
)}
|
||||
<span className={cn(
|
||||
"truncate flex-1",
|
||||
isBlocker ? "text-danger font-semibold" : "text-muted-foreground"
|
||||
)}>
|
||||
{filter.name}
|
||||
</span>
|
||||
<span className={cn(
|
||||
"text-[9px] truncate max-w-[80px]",
|
||||
isBlocker ? "text-danger" : "text-muted-foreground/60"
|
||||
)}>
|
||||
{filter.detail}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -11,3 +11,5 @@ export { EquityChart } from "./equity-chart";
|
||||
export { Header } from "./header";
|
||||
export { Sparkline } from "./sparkline";
|
||||
export { SettingsCard } from "./settings-card";
|
||||
export { BotStatusCard } from "./bot-status-card";
|
||||
export { EntryFilterCard } from "./entry-filter-card";
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Layers, Inbox } from "lucide-react";
|
||||
import { Layers, Inbox, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Position } from "@/types/trading";
|
||||
import type { Position, PositionDetail } from "@/types/trading";
|
||||
|
||||
interface PositionsCardProps {
|
||||
positions: Position[];
|
||||
positionDetails?: PositionDetail[];
|
||||
}
|
||||
|
||||
export function PositionsCard({ positions }: PositionsCardProps) {
|
||||
export function PositionsCard({ positions, positionDetails }: PositionsCardProps) {
|
||||
const [expandedTicket, setExpandedTicket] = useState<number | null>(null);
|
||||
|
||||
const getDetail = (ticket: number) =>
|
||||
positionDetails?.find((d) => d.ticket === ticket);
|
||||
|
||||
return (
|
||||
<Card className="glass h-full flex flex-col">
|
||||
<CardHeader>
|
||||
@@ -32,33 +39,91 @@ export function PositionsCard({ positions }: PositionsCardProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{positions.map((pos) => (
|
||||
<div
|
||||
key={pos.ticket}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-1.5 rounded-md bg-surface-light/50",
|
||||
pos.type === "BUY" ? "border-l-2 border-l-success" : "border-l-2 border-l-danger"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge
|
||||
variant={pos.type === "BUY" ? "success" : "danger"}
|
||||
className="text-[10px] h-4 px-1"
|
||||
{positions.map((pos) => {
|
||||
const detail = getDetail(pos.ticket);
|
||||
const isExpanded = expandedTicket === pos.ticket;
|
||||
const hasDetail = !!detail;
|
||||
|
||||
return (
|
||||
<div key={pos.ticket}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between p-1.5 rounded-md bg-surface-light/50",
|
||||
pos.type === "BUY" ? "border-l-2 border-l-success" : "border-l-2 border-l-danger",
|
||||
hasDetail && "cursor-pointer hover:bg-surface-light/80"
|
||||
)}
|
||||
onClick={() => hasDetail && setExpandedTicket(isExpanded ? null : pos.ticket)}
|
||||
>
|
||||
{pos.type}
|
||||
</Badge>
|
||||
<span className="text-[11px] font-number">
|
||||
{pos.volume} @ {pos.priceOpen.toFixed(2)}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge
|
||||
variant={pos.type === "BUY" ? "success" : "danger"}
|
||||
className="text-[10px] h-4 px-1"
|
||||
>
|
||||
{pos.type}
|
||||
</Badge>
|
||||
<span className="text-[11px] font-number">
|
||||
{pos.volume} @ {pos.priceOpen.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={cn(
|
||||
"text-[11px] font-bold font-number",
|
||||
pos.profit >= 0 ? "text-success" : "text-danger"
|
||||
)}>
|
||||
{pos.profit >= 0 ? "+" : ""}${pos.profit.toFixed(2)}
|
||||
</span>
|
||||
{hasDetail && (
|
||||
isExpanded
|
||||
? <ChevronUp className="h-3 w-3 text-muted-foreground/40" />
|
||||
: <ChevronDown className="h-3 w-3 text-muted-foreground/40" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && detail && (
|
||||
<div className="ml-2 mt-0.5 p-1.5 rounded bg-surface-light/30 space-y-0.5 text-[10px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Peak Profit</span>
|
||||
<span className="font-number text-success">${detail.peakProfit.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">DD from Peak</span>
|
||||
<span className={cn("font-number", detail.drawdownFromPeak > 30 ? "text-danger" : "text-muted-foreground")}>
|
||||
{detail.drawdownFromPeak.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Momentum</span>
|
||||
<span className={cn("font-number", detail.momentum > 0 ? "text-success" : detail.momentum < 0 ? "text-danger" : "text-muted-foreground")}>
|
||||
{detail.momentum > 0 ? "+" : ""}{detail.momentum}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">TP Probability</span>
|
||||
<span className={cn("font-number", detail.tpProbability >= 50 ? "text-success" : "text-warning")}>
|
||||
{detail.tpProbability}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Duration</span>
|
||||
<span className="font-number text-muted-foreground">{detail.tradeHours}h</span>
|
||||
</div>
|
||||
{(detail.reversalWarnings > 0 || detail.stalls > 0) && (
|
||||
<div className="flex gap-2 pt-0.5 border-t border-border/50">
|
||||
{detail.reversalWarnings > 0 && (
|
||||
<span className="text-warning">Rev: {detail.reversalWarnings}</span>
|
||||
)}
|
||||
{detail.stalls > 0 && (
|
||||
<span className="text-muted-foreground">Stalls: {detail.stalls}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className={cn(
|
||||
"text-[11px] font-bold font-number",
|
||||
pos.profit >= 0 ? "text-success" : "text-danger"
|
||||
)}>
|
||||
{pos.profit >= 0 ? "+" : ""}${pos.profit.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ShieldAlert, AlertTriangle } from "lucide-react";
|
||||
import { cn, formatUSD } from "@/lib/utils";
|
||||
import type { RiskMode } from "@/types/trading";
|
||||
|
||||
interface RiskCardProps {
|
||||
dailyLoss: number;
|
||||
dailyProfit: number;
|
||||
consecutiveLosses: number;
|
||||
riskPercent: number;
|
||||
riskMode?: RiskMode;
|
||||
}
|
||||
|
||||
export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercent }: RiskCardProps) {
|
||||
function getRiskModeVariant(mode: string): "success" | "warning" | "danger" | "secondary" {
|
||||
switch (mode) {
|
||||
case "normal": return "success";
|
||||
case "recovery": return "warning";
|
||||
case "protected": return "danger";
|
||||
case "stopped": return "danger";
|
||||
default: return "secondary";
|
||||
}
|
||||
}
|
||||
|
||||
export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercent, riskMode }: RiskCardProps) {
|
||||
const isCritical = riskPercent >= 100;
|
||||
const isHigh = riskPercent >= 80;
|
||||
const isMedium = riskPercent >= 50;
|
||||
@@ -28,6 +41,8 @@ export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercen
|
||||
return "bg-success";
|
||||
};
|
||||
|
||||
const mode = riskMode?.mode || "unknown";
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
"glass",
|
||||
@@ -41,8 +56,15 @@ export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercen
|
||||
)}>
|
||||
<ShieldAlert className="h-3.5 w-3.5" />
|
||||
Risk
|
||||
{/* Risk Mode Badge */}
|
||||
<Badge
|
||||
variant={getRiskModeVariant(mode)}
|
||||
className={cn("ml-auto text-[9px] h-4 px-1 uppercase", mode === "stopped" && "animate-pulse")}
|
||||
>
|
||||
{mode}
|
||||
</Badge>
|
||||
{isCritical && (
|
||||
<span className="ml-auto flex items-center gap-1 text-[10px] bg-danger text-white px-1.5 py-0.5 rounded-full animate-pulse">
|
||||
<span className="flex items-center gap-1 text-[10px] bg-danger text-white px-1.5 py-0.5 rounded-full animate-pulse">
|
||||
<AlertTriangle className="h-2.5 w-2.5" />
|
||||
BREACHED
|
||||
</span>
|
||||
@@ -81,7 +103,37 @@ export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercen
|
||||
style={{ width: `${Math.min(riskPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
{/* Remaining daily risk */}
|
||||
{riskMode && riskMode.remainingDailyRisk > 0 && (
|
||||
<div className="flex justify-between items-center mt-0.5">
|
||||
<span className="text-[9px] text-muted-foreground/60">Remaining</span>
|
||||
<span className="text-[9px] font-number text-muted-foreground/60">
|
||||
{formatUSD(riskMode.remainingDailyRisk)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Total Loss Progress */}
|
||||
{riskMode && riskMode.maxTotalLoss > 0 && (
|
||||
<div className="pt-0.5">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="text-[10px] text-muted-foreground">Total Loss</span>
|
||||
<span className="text-[10px] font-number text-muted-foreground">
|
||||
{formatUSD(riskMode.totalLoss)} / {formatUSD(riskMode.maxTotalLoss)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1 w-full bg-surface-light rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-all duration-500",
|
||||
(riskMode.totalLoss / riskMode.maxTotalLoss) >= 0.8 ? "bg-danger" : "bg-warning/60"
|
||||
)}
|
||||
style={{ width: `${riskMode.maxTotalLoss > 0 ? Math.min((riskMode.totalLoss / riskMode.maxTotalLoss) * 100, 100) : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Clock, Sparkles, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { Clock, Sparkles, CheckCircle2, XCircle, Ban } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TimeFilter } from "@/types/trading";
|
||||
|
||||
interface SessionCardProps {
|
||||
session: string;
|
||||
isGoldenTime: boolean;
|
||||
canTrade: boolean;
|
||||
sessionMultiplier?: number;
|
||||
timeFilter?: TimeFilter;
|
||||
}
|
||||
|
||||
export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProps) {
|
||||
export function SessionCard({ session, isGoldenTime, canTrade, sessionMultiplier, timeFilter }: SessionCardProps) {
|
||||
const getSessionColor = (s: string) => {
|
||||
const lower = s.toLowerCase();
|
||||
if (lower.includes("london")) return "text-info";
|
||||
@@ -20,12 +23,21 @@ export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProp
|
||||
return "text-warning";
|
||||
};
|
||||
|
||||
const mult = sessionMultiplier ?? 1.0;
|
||||
const multLabel = `${mult}x`;
|
||||
const multVariant = mult < 1 ? "warning" : mult > 1 ? "success" : "secondary";
|
||||
|
||||
return (
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Session
|
||||
{sessionMultiplier != null && (
|
||||
<Badge variant={multVariant as "warning" | "success" | "secondary"} className="ml-auto text-[10px] h-4 px-1">
|
||||
{multLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
@@ -56,6 +68,23 @@ export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProp
|
||||
{canTrade ? "CAN TRADE" : "NO TRADE"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Time Filter Status */}
|
||||
{timeFilter && (
|
||||
<div className="flex items-center gap-1.5 pt-0.5 border-t border-border">
|
||||
{timeFilter.isBlocked ? (
|
||||
<Ban className="h-3 w-3 text-danger" />
|
||||
) : (
|
||||
<Clock className="h-3 w-3 text-muted-foreground/40" />
|
||||
)}
|
||||
<span className={cn(
|
||||
"text-[10px]",
|
||||
timeFilter.isBlocked ? "text-danger font-semibold" : "text-muted-foreground"
|
||||
)}>
|
||||
WIB {timeFilter.wibHour}:00{timeFilter.isBlocked ? " BLOCKED" : ""}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,74 @@
|
||||
// Trading data types
|
||||
|
||||
export interface EntryFilter {
|
||||
name: string;
|
||||
passed: boolean;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface RiskMode {
|
||||
mode: string;
|
||||
reason: string;
|
||||
recommendedLot: number;
|
||||
maxAllowedLot: number;
|
||||
totalLoss: number;
|
||||
maxTotalLoss: number;
|
||||
remainingDailyRisk: number;
|
||||
}
|
||||
|
||||
export interface CooldownStatus {
|
||||
active: boolean;
|
||||
secondsRemaining: number;
|
||||
totalSeconds: number;
|
||||
}
|
||||
|
||||
export interface TimeFilter {
|
||||
wibHour: number;
|
||||
isBlocked: boolean;
|
||||
blockedHours: number[];
|
||||
}
|
||||
|
||||
export interface PositionDetail {
|
||||
ticket: number;
|
||||
peakProfit: number;
|
||||
drawdownFromPeak: number;
|
||||
momentum: number;
|
||||
tpProbability: number;
|
||||
reversalWarnings: number;
|
||||
stalls: number;
|
||||
tradeHours: number;
|
||||
}
|
||||
|
||||
export interface AutoTrainerStatus {
|
||||
lastRetrain: string | null;
|
||||
currentAuc: number | null;
|
||||
minAucThreshold: number;
|
||||
hoursSinceRetrain: number;
|
||||
nextRetrainHour: number;
|
||||
modelsFitted: boolean;
|
||||
}
|
||||
|
||||
export interface PerformanceStatus {
|
||||
loopCount: number;
|
||||
avgExecutionMs: number;
|
||||
uptimeHours: number;
|
||||
totalSessionTrades: number;
|
||||
totalSessionProfit: number;
|
||||
}
|
||||
|
||||
export interface MarketCloseStatus {
|
||||
hoursToDailyClose: number;
|
||||
hoursToWeekendClose: number;
|
||||
nearWeekend: boolean;
|
||||
marketOpen: boolean;
|
||||
}
|
||||
|
||||
export interface H1BiasDetails {
|
||||
bias: string;
|
||||
ema20: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface TradingStatus {
|
||||
timestamp: string;
|
||||
connected: boolean;
|
||||
@@ -63,6 +132,18 @@ export interface TradingStatus {
|
||||
dynamicThreshold?: number;
|
||||
marketQuality?: string;
|
||||
marketScore?: number;
|
||||
|
||||
// === NEW: Extended monitoring ===
|
||||
entryFilters?: EntryFilter[];
|
||||
riskMode?: RiskMode;
|
||||
cooldown?: CooldownStatus;
|
||||
timeFilter?: TimeFilter;
|
||||
sessionMultiplier?: number;
|
||||
positionDetails?: PositionDetail[];
|
||||
autoTrainer?: AutoTrainerStatus;
|
||||
performance?: PerformanceStatus;
|
||||
marketClose?: MarketCloseStatus;
|
||||
h1BiasDetails?: H1BiasDetails;
|
||||
}
|
||||
|
||||
export interface BotSettings {
|
||||
|
||||
Reference in New Issue
Block a user