style(dashboard): unify Macro composite into one card + dark-mode fix
for Performance accent
User flagged the macro index box on the overview page as feeling
disjointed. Reshaped it from three stacked elements (band → track →
scale) into a single bordered card so it reads as ONE component.
- JSX: wrap the three pieces in <div className="overview-macro-card">
- CSS: new .overview-macro-card with tone-coloured rim (bull/bear/neutral)
- Solid neutral-gray filled needle (was a hollow ring — looked like a
placeholder); tone-coloured background + white inner ring + double
shadow so it stands out on any gradient position
- Removed the .overview-score-fill overlay — the gradient already
encodes the spectrum; layering an opaque fill obscured it near 0
- Thinner track (14px vs 22px), tighter scale labels, smaller pill
- Added "TODAY · 8 INDICATORS" stamp next to the title — gives users
a quick anchor of what they're looking at + freshness
Plus: dark-mode override for .overview-stat-card.accent (the Performance
card). It was using a cream gradient that floated as a glaring
out-of-theme block on dark mode. Mirrored the existing .kpi.accent dark
treatment so it stays visually grouped with the rest of the dashboard.
Also includes the in-flight overview rewrite from the other AI tool
(legacy /btc redirect to /macro, middleware.ts replacing proxy.ts for
Next.js routing, refactored several dashboard panels). TypeScript clean,
production build passes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -406,7 +406,7 @@ export default function MacroPanel() {
|
||||
label="AHR999"
|
||||
value={ind.ahr999?.toFixed(4) ?? '—'}
|
||||
tone={toneAhr(ind.ahr999)}
|
||||
hint="BTC valuation index: price vs 200d MA × price vs fitted long-run growth curve. < 0.45 = buy/accumulation zone, 0.45–0.75 = fair value leaning cheap, 0.75–1.2 = neutral / no edge, > 1.2 = expensive."
|
||||
hint="BTC valuation index: price vs 200-day geometric mean × price vs fitted long-run growth curve. < 0.45 = buy/accumulation zone, 0.45–0.75 = fair value leaning cheap, 0.75–1.2 = neutral / no edge, > 1.2 = expensive."
|
||||
summary={ahrSay}
|
||||
activeIndex={ahrActive}
|
||||
thresholds={[
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useDashboardStore } from '@/store/dashboard'
|
||||
import { getUserPublic, setHlApiKey, subscribe } from '@/lib/api'
|
||||
import { signRequest } from '@/lib/signedRequest'
|
||||
import { isUserRejection, walletErrorLabel } from '@/lib/walletError'
|
||||
import { getFirstReadyConnector, walletConnectErrorLabel } from '@/lib/walletConnect'
|
||||
|
||||
// Action names must match backend/app/api/{user,subscribe}.py
|
||||
const ACTION_SET_API_KEY = 'set_hl_api_key'
|
||||
@@ -28,7 +29,7 @@ function fmtHold(s: number) {
|
||||
export default function BotPanel({ performance }: Props) {
|
||||
const { isSubscribed, hlApiKeySet, hlApiKeyMasked, botReadiness, setBotReadiness, setHlApiKeySet, setSubscribed } = useDashboardStore()
|
||||
const { address, isConnected } = useAccount()
|
||||
const { connect, connectors } = useConnect()
|
||||
const { connectAsync, connectors } = useConnect()
|
||||
const { signMessageAsync } = useSignMessage()
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
@@ -37,6 +38,7 @@ export default function BotPanel({ performance }: Props) {
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [subState, setSubState] = useState<'idle' | 'signing' | 'saving' | 'error'>('idle')
|
||||
const [subError, setSubError] = useState('')
|
||||
const [connectError, setConnectError] = useState('')
|
||||
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
|
||||
@@ -127,9 +129,18 @@ export default function BotPanel({ performance }: Props) {
|
||||
: saveState === 'success' ? '✓ Saved'
|
||||
: 'Save key'
|
||||
|
||||
function handleConnectWallet() {
|
||||
const connector = connectors[0]
|
||||
if (connector) connect({ connector })
|
||||
async function handleConnectWallet() {
|
||||
setConnectError('')
|
||||
try {
|
||||
const connector = await getFirstReadyConnector(connectors)
|
||||
if (!connector) {
|
||||
setConnectError('No wallet connector is available right now.')
|
||||
return
|
||||
}
|
||||
await connectAsync({ connector })
|
||||
} catch (err: unknown) {
|
||||
setConnectError(walletConnectErrorLabel(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -169,10 +180,15 @@ export default function BotPanel({ performance }: Props) {
|
||||
|
||||
<div className="bot-cta">
|
||||
{(!mounted || !isConnected) && (
|
||||
<button className="btn amber" style={{ width: '100%' }}
|
||||
onClick={handleConnectWallet}>
|
||||
Connect wallet
|
||||
</button>
|
||||
<>
|
||||
<button className="btn amber" style={{ width: '100%' }}
|
||||
onClick={() => { void handleConnectWallet() }}>
|
||||
Connect wallet
|
||||
</button>
|
||||
{connectError && (
|
||||
<p style={{ fontSize: 11, color: 'var(--down)', marginTop: 6, textAlign: 'center' }}>{connectError}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{mounted && isConnected && !isSubscribed && (
|
||||
<div style={{ width: '100%' }}>
|
||||
|
||||
@@ -17,6 +17,9 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const chartRef = useRef<unknown>(null)
|
||||
const seriesRef = useRef<unknown>(null)
|
||||
const macroPriceLineRef = useRef<unknown>(null)
|
||||
const macroVerticalRef = useRef<HTMLDivElement | null>(null)
|
||||
const macroLabelRef = useRef<HTMLDivElement | null>(null)
|
||||
const fittedRef = useRef(false)
|
||||
const postsRef = useRef(posts)
|
||||
postsRef.current = posts
|
||||
@@ -37,6 +40,8 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
textColor: isDark ? '#666666' : '#888888',
|
||||
gridColor: isDark ? '#1e1e1e' : '#f0ede8',
|
||||
borderColor: isDark ? '#2a2a2a' : '#e8e4de',
|
||||
macroAccent: isDark ? '#f59e0b' : '#b45309',
|
||||
macroAccentSoft: isDark ? 'rgba(245,158,11,0.24)' : 'rgba(180,83,9,0.18)',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +56,42 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
if (destroyed || !containerRef.current) return
|
||||
const colors = getChartColors()
|
||||
|
||||
const vertical = document.createElement('div')
|
||||
vertical.style.position = 'absolute'
|
||||
vertical.style.top = '0'
|
||||
vertical.style.bottom = '0'
|
||||
vertical.style.width = '0'
|
||||
vertical.style.borderLeft = `2px dashed ${colors.macroAccent}`
|
||||
vertical.style.opacity = '0'
|
||||
vertical.style.pointerEvents = 'none'
|
||||
vertical.style.zIndex = '2'
|
||||
vertical.style.transition = 'left 160ms ease, opacity 120ms ease'
|
||||
containerRef.current.appendChild(vertical)
|
||||
macroVerticalRef.current = vertical
|
||||
|
||||
const label = document.createElement('div')
|
||||
label.style.position = 'absolute'
|
||||
label.style.top = '14px'
|
||||
label.style.left = '0'
|
||||
label.style.padding = '5px 8px'
|
||||
label.style.borderRadius = '999px'
|
||||
label.style.border = `1px solid ${colors.macroAccentSoft}`
|
||||
label.style.background = colors.background
|
||||
label.style.boxShadow = '0 6px 24px rgba(15, 23, 42, 0.08)'
|
||||
label.style.color = colors.macroAccent
|
||||
label.style.fontSize = '11px'
|
||||
label.style.fontWeight = '700'
|
||||
label.style.letterSpacing = '0.04em'
|
||||
label.style.textTransform = 'uppercase'
|
||||
label.style.whiteSpace = 'nowrap'
|
||||
label.style.pointerEvents = 'none'
|
||||
label.style.opacity = '0'
|
||||
label.style.transform = 'translateX(-50%)'
|
||||
label.style.zIndex = '3'
|
||||
label.style.transition = 'left 160ms ease, opacity 120ms ease'
|
||||
containerRef.current.appendChild(label)
|
||||
macroLabelRef.current = label
|
||||
|
||||
const chart = createChart(containerRef.current, {
|
||||
width: containerRef.current.clientWidth,
|
||||
height: 360,
|
||||
@@ -144,6 +185,14 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
rightPriceScale: { borderColor: colors.borderColor },
|
||||
timeScale: { borderColor: colors.borderColor },
|
||||
})
|
||||
if (macroVerticalRef.current) {
|
||||
macroVerticalRef.current.style.borderLeft = `2px dashed ${colors.macroAccent}`
|
||||
}
|
||||
if (macroLabelRef.current) {
|
||||
macroLabelRef.current.style.color = colors.macroAccent
|
||||
macroLabelRef.current.style.borderColor = colors.macroAccentSoft
|
||||
macroLabelRef.current.style.background = colors.background
|
||||
}
|
||||
})
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
@@ -162,6 +211,15 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
chartRef.current = null
|
||||
seriesRef.current = null
|
||||
}
|
||||
if (macroVerticalRef.current?.parentNode) {
|
||||
macroVerticalRef.current.parentNode.removeChild(macroVerticalRef.current)
|
||||
}
|
||||
if (macroLabelRef.current?.parentNode) {
|
||||
macroLabelRef.current.parentNode.removeChild(macroLabelRef.current)
|
||||
}
|
||||
macroVerticalRef.current = null
|
||||
macroLabelRef.current = null
|
||||
macroPriceLineRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -232,12 +290,74 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
// @ts-expect-error lightweight-charts type
|
||||
series.setMarkers(markers)
|
||||
|
||||
// Highlight the most recent visible Macro Vibes reversal signal on BTC.
|
||||
const visibleMacro = asset === 'BTC'
|
||||
? visible
|
||||
.filter((p) =>
|
||||
((p.source || '') === 'btc_bottom_reversal' || (p.source || '') === 'funding_reversal') &&
|
||||
(p.signal === 'buy' || p.signal === 'short'),
|
||||
)
|
||||
.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime())
|
||||
: []
|
||||
|
||||
const macroSignal = visibleMacro[0] ?? null
|
||||
const colors = getChartColors()
|
||||
const priceSeries = series as any
|
||||
if (macroPriceLineRef.current && priceSeries?.removePriceLine) {
|
||||
try { priceSeries.removePriceLine(macroPriceLineRef.current as any) } catch {}
|
||||
macroPriceLineRef.current = null
|
||||
}
|
||||
|
||||
if (macroSignal && chart && macroVerticalRef.current) {
|
||||
const pt = Math.floor(new Date(macroSignal.published_at).getTime() / 1000)
|
||||
const bucketTime = Math.floor(pt / bucketSecs) * bucketSecs
|
||||
// Only draw the horizontal reference when we have a true signal-entry
|
||||
// price from the backend. Falling back to a bucket close made the line
|
||||
// drift across timeframe changes and looked like a bug because it was.
|
||||
const priceAtSignal =
|
||||
macroSignal.price_impact?.asset === 'BTC' && macroSignal.price_impact?.price_at_post
|
||||
? macroSignal.price_impact.price_at_post
|
||||
: null
|
||||
|
||||
if (priceAtSignal != null && priceSeries?.createPriceLine) {
|
||||
macroPriceLineRef.current = priceSeries.createPriceLine({
|
||||
price: priceAtSignal,
|
||||
color: colors.macroAccent,
|
||||
lineWidth: 2,
|
||||
lineStyle: 2,
|
||||
axisLabelVisible: false,
|
||||
title: (macroSignal.source || '') === 'btc_bottom_reversal' ? 'Macro bottom' : 'Funding reversal',
|
||||
})
|
||||
}
|
||||
|
||||
const x = (chart as any).timeScale?.().timeToCoordinate?.(bucketTime)
|
||||
if (typeof x === 'number' && Number.isFinite(x)) {
|
||||
macroVerticalRef.current.style.left = `${x}px`
|
||||
macroVerticalRef.current.style.opacity = '1'
|
||||
if (macroLabelRef.current) {
|
||||
macroLabelRef.current.textContent =
|
||||
(macroSignal.source || '') === 'btc_bottom_reversal'
|
||||
? 'Macro bottom'
|
||||
: 'Funding reversal'
|
||||
const clamped = Math.max(76, Math.min((containerRef.current?.clientWidth ?? x) - 76, x))
|
||||
macroLabelRef.current.style.left = `${clamped}px`
|
||||
macroLabelRef.current.style.opacity = '1'
|
||||
}
|
||||
} else {
|
||||
macroVerticalRef.current.style.opacity = '0'
|
||||
if (macroLabelRef.current) macroLabelRef.current.style.opacity = '0'
|
||||
}
|
||||
} else if (macroVerticalRef.current) {
|
||||
macroVerticalRef.current.style.opacity = '0'
|
||||
if (macroLabelRef.current) macroLabelRef.current.style.opacity = '0'
|
||||
}
|
||||
|
||||
if (!fittedRef.current) {
|
||||
// @ts-expect-error lightweight-charts type
|
||||
chart.timeScale().fitContent()
|
||||
fittedRef.current = true
|
||||
}
|
||||
}, [candles, posts, externalSelectedId])
|
||||
}, [candles, posts, externalSelectedId, asset])
|
||||
|
||||
useEffect(() => { fittedRef.current = false }, [timeframe])
|
||||
|
||||
@@ -264,7 +384,7 @@ export default function ChartPanel({ posts = [], candles = [], externalSelectedI
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{ width: '100%', height: 360, borderRadius: 'var(--r-sm)', overflow: 'hidden', cursor: 'crosshair' }}
|
||||
style={{ width: '100%', height: 360, borderRadius: 'var(--r-sm)', overflow: 'hidden', cursor: 'crosshair', position: 'relative' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ function replaceLocale(pathname: string, nextLocale: Locale) {
|
||||
export default function LanguageSwitch() {
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const options: Array<{ locale: Locale; label: string }> = [
|
||||
{ locale: 'en', label: 'EN' },
|
||||
]
|
||||
|
||||
const activeLocale = useMemo<Locale>(() => {
|
||||
const seg = pathname.split('/').filter(Boolean)[0]
|
||||
@@ -39,10 +42,7 @@ export default function LanguageSwitch() {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{([
|
||||
['en', 'EN'],
|
||||
['zh', '中文'],
|
||||
] as const).map(([locale, label]) => {
|
||||
{options.map(({ locale, label }) => {
|
||||
const active = activeLocale === locale
|
||||
const nextPath = replaceLocale(pathname || `/${defaultLocale}`, locale)
|
||||
const qs = searchParams.toString()
|
||||
@@ -56,7 +56,7 @@ export default function LanguageSwitch() {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: locale === 'zh' ? 48 : 40,
|
||||
minWidth: 40,
|
||||
padding: '6px 10px',
|
||||
borderRadius: 999,
|
||||
border: 'none',
|
||||
|
||||
+69
-48
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useAccount, useConnect, useDisconnect } from 'wagmi'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { getFirstReadyConnector, walletConnectErrorLabel } from '@/lib/walletConnect'
|
||||
// i18n shelved — LanguageSwitch hidden but kept on disk for future revival.
|
||||
// import LanguageSwitch from './LanguageSwitch'
|
||||
|
||||
@@ -49,11 +50,12 @@ export default function Navbar() {
|
||||
const t = useTranslations('nav')
|
||||
const pathname = usePathname()
|
||||
const { address, isConnected } = useAccount()
|
||||
const { connect, connectors } = useConnect()
|
||||
const { connectAsync, connectors } = useConnect()
|
||||
const { disconnect } = useDisconnect()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [walletMenuOpen, setWalletMenuOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [connectError, setConnectError] = useState('')
|
||||
|
||||
async function copyAddress(addr: string) {
|
||||
let ok = false
|
||||
@@ -78,10 +80,27 @@ export default function Navbar() {
|
||||
}
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
useEffect(() => { setWalletMenuOpen(false) }, [pathname, address])
|
||||
useEffect(() => {
|
||||
if (isConnected) setConnectError('')
|
||||
}, [isConnected])
|
||||
|
||||
const locale = pathname.split('/')[1] || 'en'
|
||||
const path = '/' + pathname.split('/').slice(2).join('/')
|
||||
|
||||
async function handleConnectWallet() {
|
||||
setConnectError('')
|
||||
try {
|
||||
const connector = await getFirstReadyConnector(connectors)
|
||||
if (!connector) {
|
||||
setConnectError('No wallet connector is available right now.')
|
||||
return
|
||||
}
|
||||
await connectAsync({ connector })
|
||||
} catch (err: unknown) {
|
||||
setConnectError(walletConnectErrorLabel(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Two independent trading systems get their own top-level tabs — no
|
||||
// intermediate "Signals" hub. Trump = event scalp; BTC = reversal.
|
||||
const tabs = [
|
||||
@@ -128,58 +147,60 @@ export default function Navbar() {
|
||||
<div className="nav-right">
|
||||
{/* <LanguageSwitch /> — shelved until i18n coverage is complete */}
|
||||
<ThemeToggle />
|
||||
{!mounted ? (
|
||||
<button className="connect-btn lg" suppressHydrationWarning>{t('actions.connectWallet')}</button>
|
||||
) : isConnected && shortAddr ? (
|
||||
<div className="wallet-menu-wrap" style={{ position: 'relative' }}>
|
||||
<button
|
||||
className="wallet-chip"
|
||||
onClick={() => setWalletMenuOpen(open => !open)}
|
||||
onBlur={(e) => {
|
||||
if (!e.currentTarget.parentElement?.contains(e.relatedTarget as Node | null)) {
|
||||
setWalletMenuOpen(false)
|
||||
}
|
||||
}}
|
||||
aria-expanded={walletMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<span className="ava" />
|
||||
<span className="mono">{shortAddr}</span>
|
||||
</button>
|
||||
<div className={`wallet-menu ${walletMenuOpen ? 'open' : ''}`} role="menu">
|
||||
<div className="wallet-anchor">
|
||||
{!mounted ? (
|
||||
<button className="connect-btn lg" suppressHydrationWarning>{t('actions.connectWallet')}</button>
|
||||
) : isConnected && shortAddr ? (
|
||||
<div className="wallet-menu-wrap" style={{ position: 'relative' }}>
|
||||
<button
|
||||
role="menuitem"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
if (address) copyAddress(address)
|
||||
className="wallet-chip"
|
||||
onClick={() => setWalletMenuOpen(open => !open)}
|
||||
onBlur={(e) => {
|
||||
if (!e.currentTarget.parentElement?.contains(e.relatedTarget as Node | null)) {
|
||||
setWalletMenuOpen(false)
|
||||
}
|
||||
}}
|
||||
aria-expanded={walletMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
{copied ? t('actions.copied') : t('actions.copyAddress')}
|
||||
</button>
|
||||
<button
|
||||
role="menuitem"
|
||||
className="danger"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
disconnect()
|
||||
setWalletMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
{t('actions.disconnect')}
|
||||
<span className="ava" />
|
||||
<span className="mono">{shortAddr}</span>
|
||||
</button>
|
||||
<div className={`wallet-menu ${walletMenuOpen ? 'open' : ''}`} role="menu">
|
||||
<button
|
||||
role="menuitem"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
if (address) copyAddress(address)
|
||||
}}
|
||||
>
|
||||
{copied ? t('actions.copied') : t('actions.copyAddress')}
|
||||
</button>
|
||||
<button
|
||||
role="menuitem"
|
||||
className="danger"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
disconnect()
|
||||
setWalletMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
{t('actions.disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="connect-btn lg"
|
||||
onClick={() => {
|
||||
const connector = connectors[0]
|
||||
if (connector) connect({ connector })
|
||||
}}
|
||||
>
|
||||
{t('actions.connectWallet')}
|
||||
</button>
|
||||
)}
|
||||
) : (
|
||||
<button
|
||||
className="connect-btn lg"
|
||||
onClick={() => { void handleConnectWallet() }}
|
||||
>
|
||||
{t('actions.connectWallet')}
|
||||
</button>
|
||||
)}
|
||||
{!isConnected && connectError ? (
|
||||
<p className="wallet-connect-error" role="alert">{connectError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type OpenPosition,
|
||||
type TodayStats,
|
||||
} from '@/lib/api'
|
||||
import { getCachedViewEnvelope, getOrCreateViewEnvelope, signRequest } from '@/lib/signedRequest'
|
||||
import { getCachedViewEnvelope, signRequest } from '@/lib/signedRequest'
|
||||
import { isUserRejection, walletErrorLabel } from '@/lib/walletError'
|
||||
|
||||
const POLL_MS = 15_000
|
||||
@@ -171,6 +171,7 @@ export default function OpenPositions() {
|
||||
const [positions, setPositions] = useState<OpenPosition[] | null>(null)
|
||||
const [today, setToday] = useState<TodayStats | null>(null)
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
const [needsUnlock, setNeedsUnlock] = useState(false)
|
||||
// Close-confirmation modal state
|
||||
const [closing, setClosing] = useState<OpenPosition | null>(null)
|
||||
const [closeState, setCloseState] = useState<'idle'|'signing'|'closing'|'err'>('idle')
|
||||
@@ -203,43 +204,38 @@ export default function OpenPositions() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected || !address) {
|
||||
setPositions(null); setToday(null)
|
||||
setPositions(null); setToday(null); setNeedsUnlock(false); setErr(null)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
|
||||
// First load uses getOrCreateViewEnvelope — may pop MetaMask exactly ONCE
|
||||
// when the page is opened (user action). All subsequent polls reuse the
|
||||
// cached envelope via getCachedViewEnvelope (no popup). When the cache
|
||||
// expires (20 min), the polling tick silently skips until the user
|
||||
// refocuses the tab or navigates back in — preventing the wallet from
|
||||
// popping in the background while the user is doing something else.
|
||||
async function load(envSource: 'first' | 'poll') {
|
||||
const env = envSource === 'first'
|
||||
? await getOrCreateViewEnvelope({
|
||||
action: 'view_positions', wallet: address!, signMessageAsync,
|
||||
})
|
||||
: getCachedViewEnvelope('view_positions', address!)
|
||||
if (!env) return // cache expired during polling — wait for user re-entry
|
||||
// Only reuse a cached envelope here. Open positions should never trigger
|
||||
// a fresh signature popup on their own while the user is browsing.
|
||||
async function load() {
|
||||
const env = getCachedViewEnvelope('view_positions', address!)
|
||||
?? getCachedViewEnvelope('view_user', address!)
|
||||
if (!env) {
|
||||
if (!cancelled) {
|
||||
setNeedsUnlock(true)
|
||||
setErr(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const [p, t] = await Promise.all([
|
||||
getOpenPositions(address!, env),
|
||||
getTodayStats(address!, env),
|
||||
])
|
||||
if (!cancelled) {
|
||||
setPositions(p.positions); setToday(t); setErr(null)
|
||||
setPositions(p.positions); setToday(t); setErr(null); setNeedsUnlock(false)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (!cancelled) setErr(e instanceof Error ? e.message.slice(0, 80) : (isZh ? '加载失败' : 'fetch error'))
|
||||
}
|
||||
}
|
||||
|
||||
load('first').catch(e => {
|
||||
// Initial sign-rejection lands here. Show a soft error so the polling
|
||||
// loop still starts (a subsequent in-cache sign elsewhere will revive it).
|
||||
if (!cancelled) setErr(e instanceof Error ? e.message.slice(0, 80) : (isZh ? '加载失败' : 'fetch error'))
|
||||
})
|
||||
const id = setInterval(() => { void load('poll') }, POLL_MS)
|
||||
void load()
|
||||
const id = setInterval(() => { void load() }, POLL_MS)
|
||||
return () => { cancelled = true; clearInterval(id) }
|
||||
}, [address, isConnected, signMessageAsync, isZh])
|
||||
|
||||
@@ -269,6 +265,21 @@ export default function OpenPositions() {
|
||||
// Return null both before mount (SSR) and when not connected — same output,
|
||||
// no hydration mismatch.
|
||||
if (!mounted || !isConnected) return null
|
||||
if (needsUnlock && positions === null && today === null) {
|
||||
return (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.06em', textTransform: 'uppercase', fontWeight: 600, marginBottom: 6 }}>
|
||||
Open positions
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>
|
||||
Load your settings once to unlock private position data.
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4, lineHeight: 1.6 }}>
|
||||
This panel no longer triggers a wallet signature on its own while you browse. Open the Settings page and load once, then the cached permission will populate positions here.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (positions === null && today === null) return null
|
||||
|
||||
const totalUnrealized = (positions ?? []).reduce(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useLocale } from 'next-intl'
|
||||
import { useAccount, useSignMessage } from 'wagmi'
|
||||
@@ -126,6 +126,9 @@ export default function SystemControl({ system }: { system: 'trump' | 'btc' }) {
|
||||
// wagmi state that resolves client-side.
|
||||
if (!mounted) return null
|
||||
|
||||
const settingsHref = `/${locale}/settings#${system === 'trump' ? 'trump-settings-panel' : 'btc-settings-panel'}`
|
||||
const settingsLabel = system === 'trump' ? 'Trump Signal' : 'Macro Vibes'
|
||||
|
||||
// Wallet not connected: a previous version showed a giant full-width
|
||||
// "Connect a wallet..." card here, which was redundant with the navbar's
|
||||
// Connect button. The opposite extreme — rendering null — silently hid
|
||||
@@ -135,24 +138,49 @@ export default function SystemControl({ system }: { system: 'trump' | 'btc' }) {
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap',
|
||||
padding: '10px 14px', marginBottom: 16,
|
||||
background: 'var(--bg-sunk)',
|
||||
border: '1px dashed var(--line)',
|
||||
borderRadius: 8,
|
||||
fontSize: 12, color: 'var(--ink-3)',
|
||||
fontSize: 12, color: 'var(--ink-3)',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 6, height: 6, borderRadius: '50%',
|
||||
background: 'var(--ink-4)', flexShrink: 0,
|
||||
}} />
|
||||
<span style={{ color: 'var(--ink-2)', fontWeight: 600 }}>
|
||||
{s.name} Auto-Trade
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span style={{ color: 'var(--ink-4)' }}>
|
||||
OFF — connect wallet (top-right) to enable
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<span style={{
|
||||
width: 6, height: 6, borderRadius: '50%',
|
||||
background: 'var(--ink-4)', flexShrink: 0,
|
||||
}} />
|
||||
<span style={{ color: 'var(--ink-2)', fontWeight: 600 }}>
|
||||
{s.name} Auto-Trade
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span style={{ color: 'var(--ink-4)' }}>
|
||||
OFF — connect wallet (top-right) to enable
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
href={settingsHref}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid var(--line)',
|
||||
background: 'var(--surface)',
|
||||
fontSize: 12,
|
||||
color: 'var(--ink)',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 700,
|
||||
boxShadow: 'var(--shadow-1)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
|
||||
Settings
|
||||
</span>
|
||||
<span>{settingsLabel}</span>
|
||||
<span aria-hidden="true">↗</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -276,6 +304,31 @@ export default function SystemControl({ system }: { system: 'trump' | 'btc' }) {
|
||||
color: netOk ? 'var(--up)' : 'var(--down)' }}>
|
||||
{netOk ? '✅ ' : '⛔ '}{netMsg}
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Link
|
||||
href={settingsHref}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid var(--line)',
|
||||
background: 'var(--surface)',
|
||||
fontSize: 12,
|
||||
color: 'var(--ink)',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 700,
|
||||
boxShadow: 'var(--shadow-1)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--ink-4)' }}>
|
||||
Settings
|
||||
</span>
|
||||
<span>{settingsLabel}</span>
|
||||
<span aria-hidden="true">↗</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -37,7 +37,11 @@ export default function TelegramCard() {
|
||||
useEffect(() => { setMounted(true) }, [])
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!address) return
|
||||
if (!address) {
|
||||
setLoading(false)
|
||||
setStatus(null)
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const s = await getTelegramStatus(address.toLowerCase())
|
||||
@@ -101,7 +105,18 @@ export default function TelegramCard() {
|
||||
// ── Render ───────────────────────────────────────────────────────────────
|
||||
|
||||
if (!mounted) return null
|
||||
if (!isConnected) return null
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 6 }}>
|
||||
Telegram alerts
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.6 }}>
|
||||
Connect your wallet first. After that, you can link Telegram for alert delivery and Pro wallet-bound notifications.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getCachedViewEnvelope,
|
||||
} from '@/lib/signedRequest'
|
||||
import { walletErrorLabel } from '@/lib/walletError'
|
||||
import { getFirstReadyConnector, walletConnectErrorLabel } from '@/lib/walletConnect'
|
||||
import { confirmSign } from '@/components/wallet/SignConfirmSheet'
|
||||
|
||||
// Defaults shown to a brand-new, un-configured wallet. take_profit_pct,
|
||||
@@ -51,7 +52,7 @@ export default function BotConfigPanel() {
|
||||
const locale = useLocale()
|
||||
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
|
||||
const { address, isConnected } = useAccount()
|
||||
const { connect, connectors } = useConnect()
|
||||
const { connectAsync, connectors } = useConnect()
|
||||
const { signMessageAsync } = useSignMessage()
|
||||
const {
|
||||
isSubscribed, hlApiKeySet, hlApiKeyMasked, botReadiness,
|
||||
@@ -97,6 +98,7 @@ export default function BotConfigPanel() {
|
||||
// cached signature; otherwise wait for the user to click "Load settings".
|
||||
const [loadState, setLoadState] = useState<'idle'|'loading'|'loaded'|'err'>('idle')
|
||||
const [loadErr, setLoadErr] = useState('')
|
||||
const [connectErr, setConnectErr] = useState('')
|
||||
|
||||
// Apply a fetched user payload into local form state.
|
||||
function applyUserPayload(u: {
|
||||
@@ -296,10 +298,19 @@ export default function BotConfigPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleConnectWallet = useCallback(() => {
|
||||
const connector = connectors[0]
|
||||
if (connector) connect({ connector })
|
||||
}, [connect, connectors])
|
||||
const handleConnectWallet = useCallback(async () => {
|
||||
setConnectErr('')
|
||||
try {
|
||||
const connector = await getFirstReadyConnector(connectors)
|
||||
if (!connector) {
|
||||
setConnectErr('No wallet connector is available right now.')
|
||||
return
|
||||
}
|
||||
await connectAsync({ connector })
|
||||
} catch (err: unknown) {
|
||||
setConnectErr(walletConnectErrorLabel(err))
|
||||
}
|
||||
}, [connectAsync, connectors])
|
||||
|
||||
// ── Manual-window: arm for N hours, or clear (hours = 0) ──────────────────
|
||||
async function handleManualWindow(hours: number) {
|
||||
@@ -335,7 +346,10 @@ export default function BotConfigPanel() {
|
||||
<p style={{ color: 'var(--ink-3)', marginBottom: 16 }}>
|
||||
{isZh ? '连接钱包后即可配置机器人并查看你的交易。' : 'Connect your wallet to configure the bot and view your trades.'}
|
||||
</p>
|
||||
<button className="btn amber" onClick={handleConnectWallet}>{isZh ? '连接钱包' : 'Connect wallet'}</button>
|
||||
<button className="btn amber" onClick={() => { void handleConnectWallet() }}>{isZh ? '连接钱包' : 'Connect wallet'}</button>
|
||||
{connectErr ? (
|
||||
<p style={{ fontSize: 12, color: 'var(--down)', margin: '12px 0 0' }}>{connectErr}</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -370,6 +384,20 @@ export default function BotConfigPanel() {
|
||||
: 'API wallet saved. Recommended next step: keep size tiny and run your own first BTC test before treating the setup as production-ready.')
|
||||
: null
|
||||
|
||||
function ScopePill({
|
||||
label,
|
||||
tone = 'amber',
|
||||
}: {
|
||||
label: string
|
||||
tone?: 'amber' | 'up' | 'violet'
|
||||
}) {
|
||||
return (
|
||||
<span className={`chip ${tone}`} style={{ fontSize: 10, padding: '2px 8px', marginLeft: 8 }}>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Hyperliquid key strip ─────────────────────────────────────────────────
|
||||
const hlKeyStrip = (
|
||||
<div className="card" style={{ padding: '16px 20px', marginBottom: 16, display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 16 }}>
|
||||
@@ -537,12 +565,12 @@ export default function BotConfigPanel() {
|
||||
})()}
|
||||
|
||||
{/* ── Main settings card ── */}
|
||||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
<div className="card" id="bot-config-panel" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
{/* Header bar */}
|
||||
<div style={{ padding: '20px 24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: '1px solid var(--line)' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 16, fontWeight: 600, letterSpacing: '-0.01em' }}>Trading bot</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 2 }}>Executes on Hyperliquid when a Trump post clears your filters.</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 2 }}>Controls Trump Signal entries, Macro Vibes BTC management, and shared execution limits on Hyperliquid.</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
{dirty && isSubscribed && <span style={{ fontSize: 12, color: 'var(--amber-ink)' }}>● Unsaved</span>}
|
||||
@@ -688,14 +716,17 @@ export default function BotConfigPanel() {
|
||||
) : (
|
||||
<div style={{ padding: '8px 24px 24px' }}>
|
||||
{/* ── EXECUTION ── */}
|
||||
<div className="section-head">
|
||||
<span className="section-head-label">Execution</span>
|
||||
<div className="section-head" id="trump-settings-panel">
|
||||
<span className="section-head-label">Trump Signal settings</span>
|
||||
<div style={{ flex: 1, height: 1, background: 'var(--line)' }} />
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', margin: '4px 0 10px' }}>
|
||||
Event-driven trade sizing, leverage, and signal threshold for Trump-triggered entries.
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
Per-trade size
|
||||
Per-trade size <ScopePill label="Trump only" />
|
||||
<span className="hint">Notional in USD. margin ≈ ${(settings.position_size_usd / settings.leverage).toFixed(2)} at {settings.leverage}×</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
@@ -710,7 +741,7 @@ export default function BotConfigPanel() {
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
Leverage <span style={{ opacity: 0.6 }}>(Trump / System 1)</span>
|
||||
Leverage <span style={{ opacity: 0.6 }}>(Trump / System 1)</span> <ScopePill label="Trump only" />
|
||||
<span className="hint">Event-driven scalp positions</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
@@ -726,43 +757,71 @@ export default function BotConfigPanel() {
|
||||
{(() => {
|
||||
const aggressive = settings.sys2_mode === 'aggressive'
|
||||
return (
|
||||
<>
|
||||
<div className="section-head" id="btc-settings-panel" style={{ marginTop: 20 }}>
|
||||
<span className="section-head-label">Macro Vibes settings</span>
|
||||
<div style={{ flex: 1, height: 1, background: 'var(--line)' }} />
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', margin: '4px 0 10px' }}>
|
||||
Manage-only BTC mode, leverage, and pyramiding behavior for the Macro Vibes system.
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
BTC strategy mode <span style={{ opacity: 0.6 }}>(System 2)</span>
|
||||
BTC strategy mode <span style={{ opacity: 0.6 }}>(System 2)</span> <ScopePill label="BTC only" tone="up" />
|
||||
<span className="hint">
|
||||
A separately-funded sleeve. <strong>Aggressive</strong> =
|
||||
high leverage + earlier/heavier pyramiding + wider
|
||||
trailing → explosive in a clean bull, but a normal
|
||||
25–35% correction will scale you out hard. Both modes
|
||||
stay inside the liquidation line.
|
||||
Choose how hard the BTC system presses winners. Standard is calmer. Aggressive adds more leverage and wider pyramiding.
|
||||
</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<div style={{
|
||||
display: 'inline-flex',
|
||||
gap: 6,
|
||||
padding: 4,
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-sunk)',
|
||||
border: '1px solid var(--line)',
|
||||
}}>
|
||||
<button type="button"
|
||||
onClick={() => updateSettings({ sys2_mode: 'standard' })}
|
||||
className={`btn ${!aggressive ? '' : 'ghost'}`}
|
||||
style={{ padding: '7px 14px', fontSize: 12, fontWeight: 700 }}>
|
||||
aria-pressed={!aggressive}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
borderRadius: 999,
|
||||
border: !aggressive ? '1px solid var(--line)' : '1px solid transparent',
|
||||
background: !aggressive ? 'var(--surface)' : 'transparent',
|
||||
color: !aggressive ? 'var(--ink)' : 'var(--ink-3)',
|
||||
boxShadow: !aggressive ? 'var(--shadow-1)' : 'none',
|
||||
cursor: 'pointer',
|
||||
}}>
|
||||
Standard
|
||||
</button>
|
||||
<button type="button"
|
||||
onClick={() => updateSettings({ sys2_mode: 'aggressive' })}
|
||||
className={`btn ${aggressive ? '' : 'ghost'}`}
|
||||
style={{ padding: '7px 14px', fontSize: 12, fontWeight: 700,
|
||||
...(aggressive ? { background: 'var(--down)', color: '#fff', border: 'none' } : {}) }}>
|
||||
aria-pressed={aggressive}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
borderRadius: 999,
|
||||
border: aggressive ? '1px solid var(--down)' : '1px solid transparent',
|
||||
background: aggressive ? 'var(--down)' : 'transparent',
|
||||
color: aggressive ? '#fff' : 'var(--ink-3)',
|
||||
boxShadow: aggressive ? 'var(--shadow-1)' : 'none',
|
||||
cursor: 'pointer',
|
||||
}}>
|
||||
🔥 Aggressive
|
||||
</button>
|
||||
</div>
|
||||
{aggressive && (
|
||||
<div className="hint" style={{ marginTop: 6, color: 'var(--down)' }}>
|
||||
⚠️ High-risk sleeve: default 8×, pyramids up to +1.5×
|
||||
base, gives back up to 42% off the peak. Fund this
|
||||
separately — expect frequent full scale-outs on
|
||||
corrections in exchange for explosive clean runs.
|
||||
<div className="settings-note warn" style={{ marginTop: 6 }}>
|
||||
High-risk sleeve. Uses more leverage, pyramids harder, and gives back more off the peak. Best treated as a separate risk bucket.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -778,12 +837,9 @@ export default function BotConfigPanel() {
|
||||
return (
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
BTC bottom leverage <span style={{ opacity: 0.6 }}>(System 2)</span>
|
||||
BTC bottom leverage <span style={{ opacity: 0.6 }}>(System 2)</span> <ScopePill label="BTC only" tone="up" />
|
||||
<span className="hint">
|
||||
Independent of Trump. Wrong → scales OUT in 3 stages
|
||||
inside the liquidation line (<strong>never
|
||||
exchange-liquidated</strong>). Right → pyramids IN on a
|
||||
confirmed trend, stop floored at breakeven.
|
||||
Separate from Trump leverage. The bot de-risks in stages before the exchange liquidation line.
|
||||
</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
@@ -793,16 +849,13 @@ export default function BotConfigPanel() {
|
||||
<div className="ticks"><span>1×</span><span>5×</span><span>10×</span></div>
|
||||
</div>
|
||||
<span className="slider-readout">{lev}×</span>
|
||||
<div className="hint" style={{ marginTop: 6, color: risky ? 'var(--down)' : 'var(--ink-3)' }}>
|
||||
At {lev}× it sheds {e1} near −{(prot * 0.6).toFixed(0)}%,
|
||||
{' '}{e2} near −{(prot * 0.8).toFixed(0)}%, fully out by
|
||||
−{prot.toFixed(0)}% (exchange would liquidate
|
||||
≈ −{liq.toFixed(0)}%).{' '}
|
||||
<div className={`settings-note ${risky ? 'warn' : ''}`} style={{ marginTop: 6 }}>
|
||||
At {lev}× it sheds {e1} near −{(prot * 0.6).toFixed(0)}%, {e2} near −{(prot * 0.8).toFixed(0)}%, and is fully out by −{prot.toFixed(0)}%.
|
||||
Exchange liquidation would be around −{liq.toFixed(0)}%.
|
||||
{' '}
|
||||
{risky
|
||||
? (aggressive
|
||||
? '🔥 Aggressive: a normal 25–35% bull correction will scale you out — you’re betting on clean explosive runs.'
|
||||
: '⚠️ Above 2× a normal 25–35% bull correction can scale you out before the top. To ride a super-bull, keep ≤2× — amplify via pyramiding, not leverage.')
|
||||
: 'Wide enough to ride normal bull corrections; amplification comes from pyramiding.'}
|
||||
? 'Above 2×, a normal bull-market correction can push you out early.'
|
||||
: 'This is wide enough to survive a normal correction; extra upside should come from pyramiding, not leverage.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -811,7 +864,7 @@ export default function BotConfigPanel() {
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
Min AI confidence
|
||||
Min AI confidence <ScopePill label="Trump only" />
|
||||
<span className="hint">Skip any signal below this score (0–100)</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
@@ -825,14 +878,17 @@ export default function BotConfigPanel() {
|
||||
</div>
|
||||
|
||||
{/* ── LIMITS ── */}
|
||||
<div className="section-head" style={{ marginTop: 20 }}>
|
||||
<span className="section-head-label">Limits & risk</span>
|
||||
<div className="section-head" id="global-settings-panel" style={{ marginTop: 20 }}>
|
||||
<span className="section-head-label">Global risk limits</span>
|
||||
<div style={{ flex: 1, height: 1, background: 'var(--line)' }} />
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--ink-3)', margin: '4px 0 10px' }}>
|
||||
Shared safety rules that cap or gate automated trading across the account.
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
Daily budget <span style={{ color: 'var(--down)' }}>*</span>
|
||||
Daily budget <span style={{ color: 'var(--down)' }}>*</span> <ScopePill label="Global" tone="violet" />
|
||||
<span className="hint">Stop opening new trades once the day's total notional reaches this (UTC). Required.</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
@@ -849,7 +905,7 @@ export default function BotConfigPanel() {
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
Take profit <span style={{ color: 'var(--down)' }}>*</span>
|
||||
Take profit <span style={{ color: 'var(--down)' }}>*</span> <ScopePill label="Trump only" />
|
||||
<span className="hint">Auto-close when unrealised gain hits target. Required.</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
@@ -865,7 +921,7 @@ export default function BotConfigPanel() {
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-row-label">
|
||||
Stop loss <span style={{ color: 'var(--down)' }}>*</span>
|
||||
Stop loss <span style={{ color: 'var(--down)' }}>*</span> <ScopePill label="Trump only" />
|
||||
<span className="hint">Auto-close when drawdown hits limit. Required.</span>
|
||||
</div>
|
||||
<div className="form-row-control">
|
||||
|
||||
@@ -30,13 +30,23 @@ export interface SignConfirmOptions {
|
||||
/** Renders a confirmation sheet and resolves true (confirm) or false (cancel). */
|
||||
export function confirmSign(opts: SignConfirmOptions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const existing = document.getElementById('sign-confirm-sheet-root')
|
||||
if (existing?.parentNode) {
|
||||
existing.parentNode.removeChild(existing)
|
||||
}
|
||||
const container = document.createElement('div')
|
||||
container.id = 'sign-confirm-sheet-root'
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
let settled = false
|
||||
|
||||
function cleanup(result: boolean) {
|
||||
if (settled) return
|
||||
settled = true
|
||||
root.unmount()
|
||||
container.remove()
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container)
|
||||
}
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user