From b76de36af0bb61d82c1831f9434557e4a3083dcb Mon Sep 17 00:00:00 2001 From: k Date: Wed, 27 May 2026 23:10:55 +0800 Subject: [PATCH] refactor(settings): redesign BotConfigPanel + fix paper mode + clarify all copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BotConfigPanel (full rewrite, 1113→858 lines): - Remove 3-tab layout; replace with two expandable cards (Trump Signal / Macro Vibes) + collapsed Advanced block — fewer decisions, less noise - Fix paper mode bug: UserResponse now returns paper_mode; BotConfigPanel tracks it in state; missingItems skips HL key requirement for paper users; HL key card shows paper status instead of empty input - Add "Switch to live →" upgrade path — calls /subscribe with paper_mode=false so paper users can promote without re-subscribing from scratch - Add confirmSign before manual override window (consistent with all other mutations); rename "Arm/Disarm" → "Override/Cancel override" throughout - Fix all ambiguous copy: subscribe card, Macro Vibes description (manual /adopt flow, not auto-manage), missingItems strings, TP/SL "Not saved yet" → "Required — not saved", "Daily spend cap" → "Daily trading cap", "Active schedule" → "Trading schedule", "Get started" confirmSign labels lib/api.ts: - Add paper_mode: boolean to UserData interface (was missing) OpenPositions: - "Load your settings once…" → "Sign in once to view open positions" (removes implementation-detail explanation) - "Today realised" → "Today's realized"; "banked" → "locked in" throughout - "The bot will appear here" → "When the bot opens a trade, it will show up here" SystemControl: - "Net result" → "Current status" settings/SettingsClient.tsx: - id="btc-settings" → id="macro-settings" (stale rename) Co-Authored-By: Claude Sonnet 4.5 --- app/[locale]/settings/SettingsClient.tsx | 10 +- components/positions/OpenPositions.tsx | 14 +- components/signals/SystemControl.tsx | 4 +- components/trades/BotConfigPanel.tsx | 1153 ++++++++++------------ lib/api.ts | 5 + 5 files changed, 530 insertions(+), 656 deletions(-) diff --git a/app/[locale]/settings/SettingsClient.tsx b/app/[locale]/settings/SettingsClient.tsx index ec29420..e12d884 100644 --- a/app/[locale]/settings/SettingsClient.tsx +++ b/app/[locale]/settings/SettingsClient.tsx @@ -76,16 +76,16 @@ export default function SettingsClient() {
Position size, Trump leverage, and minimum AI confidence used when a Truth Social post becomes actionable.
- Open panel ↓ + Trump Signal tab ↓ -
+
Macro Vibes
BTC manage-only settings
Strategy mode, BTC leverage, and de-risk behavior used by the Macro Vibes sleeve.
- Open panel ↓ + Macro Vibes tab ↓
@@ -94,7 +94,7 @@ export default function SettingsClient() {
Subscription, Hyperliquid API wallet, manual window, schedule, and guardrails that apply across the account.
- Open panel ↓ + Global tab ↓
@@ -126,7 +126,7 @@ export default function SettingsClient() { -
+
Execution setup
diff --git a/components/positions/OpenPositions.tsx b/components/positions/OpenPositions.tsx index 026cca4..28a765e 100644 --- a/components/positions/OpenPositions.tsx +++ b/components/positions/OpenPositions.tsx @@ -141,7 +141,7 @@ function PositionRow({ p, onClose, onToggleGrow, growBusy, isZh }: {
{p.realized_usd != null && p.realized_usd !== 0 && (
- banked {fmtMoney(p.realized_usd, { sign: true })} + locked in {fmtMoney(p.realized_usd, { sign: true })}
)}
@@ -272,10 +272,10 @@ export default function OpenPositions() { Open positions
- Load your settings once to unlock private position data. + Sign in once to view open positions
- 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. + Go to the Settings page and click Sign in to view your settings. After signing, your open positions will appear here automatically.
) @@ -331,7 +331,7 @@ export default function OpenPositions() {
- Today realised + Today's realized
@@ -342,7 +342,7 @@ export default function OpenPositions() {
{today != null && (today.open_realized_usd ?? 0) !== 0 && (
- {`incl. ${fmtMoney(today.open_realized_usd, { sign: true })} banked on open`} + {`incl. ${fmtMoney(today.open_realized_usd, { sign: true })} locked in on open trades`}
)}
@@ -387,7 +387,7 @@ export default function OpenPositions() { padding: '20px 18px', fontSize: 13, color: 'var(--ink-3)', textAlign: 'center', }}> - No open positions. The bot will appear here as soon as a signal triggers a trade. + No open positions. When the bot opens a trade, it will show up here. )} @@ -432,7 +432,7 @@ export default function OpenPositions() { {closing.realized_usd != null && closing.realized_usd !== 0 && (
- {isZh ? '已锁定(降风险)' : 'Already banked (de-risk)'} + {isZh ? '已锁定(降风险)' : 'Locked in (de-risked)'} {fmtMoney(closing.realized_usd, { sign: true })}
)} diff --git a/components/signals/SystemControl.tsx b/components/signals/SystemControl.tsx index ed4ebab..1dc9c55 100644 --- a/components/signals/SystemControl.tsx +++ b/components/signals/SystemControl.tsx @@ -126,7 +126,7 @@ 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 settingsHref = `/${locale}/settings#bot-config` const settingsLabel = system === 'trump' ? 'Trump Signal' : 'Macro Vibes' // Wallet not connected: a previous version showed a giant full-width @@ -298,7 +298,7 @@ export default function SystemControl({ system }: { system: 'trump' | 'btc' }) { }}>
- {isZh ? '⟹ 当前结果' : '⟹ Net result'} + {isZh ? '⟹ 当前状态' : '⟹ Current status'}
diff --git a/components/trades/BotConfigPanel.tsx b/components/trades/BotConfigPanel.tsx index 87cbfc9..1b23e40 100644 --- a/components/trades/BotConfigPanel.tsx +++ b/components/trades/BotConfigPanel.tsx @@ -22,17 +22,14 @@ 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, -// stop_loss_pct and daily_budget_usd are all REQUIRED before the bot will -// trade — we still initialise them so the inputs have sensible placeholders. const DEFAULT_SETTINGS: UserSettings = { leverage: 3, position_size_usd: 20, take_profit_pct: 2, stop_loss_pct: 1.5, min_confidence: 70, daily_budget_usd: 15, sys2_leverage: 2, sys2_mode: 'standard', active_from: null, active_until: null, + trump_enabled: false, macro_enabled: false, } -// ── datetime-local helpers (browser local tz ↔ ISO UTC) ────────────────────── function isoToLocalInput(iso: string | null): string { if (!iso) return '' const d = new Date(iso) @@ -42,86 +39,76 @@ function isoToLocalInput(iso: string | null): string { } function localInputToIso(v: string): string | null { if (!v) return null - const d = new Date(v) // interpreted as local time + const d = new Date(v) if (isNaN(d.getTime())) return null return d.toISOString() } -// ─── Bot config panel ───────────────────────────────────────────────────────── 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 + useLocale() // keep next-intl context hydrated even though i18n is shelved const { address, isConnected } = useAccount() const { connectAsync, connectors } = useConnect() const { signMessageAsync } = useSignMessage() const { - isSubscribed, hlApiKeySet, hlApiKeyMasked, botReadiness, + isSubscribed, hlApiKeySet, hlApiKeyMasked, setSubscribed, setBotReadiness, setHlApiKeySet, } = useDashboardStore() const [settings, setSettings] = useState(DEFAULT_SETTINGS) - // Tracks whether the SERVER already has values for these required fields. - const [tpConfigured, setTpConfigured] = useState(false) - const [slConfigured, setSlConfigured] = useState(false) - const [budgetConfigured, setBudgetConfigured] = useState(false) - const [useSchedule, setUseSchedule] = useState(false) - const [fromLocal, setFromLocal] = useState('') - const [untilLocal, setUntilLocal] = useState('') - const [dirty, setDirty] = useState(false) - const [saveState, setSaveState] = useState<'idle'|'signing'|'saving'|'ok'|'err'>('idle') - const [saveErr, setSaveErr] = useState('') + const [tpConfigured, setTpConfigured] = useState(false) + const [slConfigured, setSlConfigured] = useState(false) + const [useBudget, setUseBudget] = useState(false) + const [useSchedule, setUseSchedule] = useState(false) + const [fromLocal, setFromLocal] = useState('') + const [untilLocal, setUntilLocal] = useState('') + const [dirty, setDirty] = useState(false) + const [saveState, setSaveState] = useState<'idle'|'signing'|'saving'|'ok'|'err'>('idle') + const [saveErr, setSaveErr] = useState('') + const [apiKey, setApiKey] = useState('') + const [keyState, setKeyState] = useState<'idle'|'signing'|'saving'|'ok'|'err'>('idle') + const [keyErr, setKeyErr] = useState('') + const [subState, setSubState] = useState<'idle'|'signing'|'saving'|'err'>('idle') + const [subErr, setSubErr] = useState('') + const [subPaperChoice, setSubPaperChoice] = useState<'paper' | 'live'>('paper') + const [manualUntil, setManualUntil] = useState(null) + const [mwState, setMwState] = useState<'idle'|'signing'|'saving'|'err'>('idle') + const [mwErr, setMwErr] = useState('') + const [mwTick, setMwTick] = useState(0) + const [paperMode, setPaperMode] = useState(false) + const [showAdvanced, setShowAdvanced] = useState(false) + const [mounted, setMounted] = useState(false) + const [loadState, setLoadState] = useState<'idle'|'loading'|'loaded'|'err'>('idle') + const [loadErr, setLoadErr] = useState('') + const [connectErr, setConnectErr] = useState('') - // HL key state - const [apiKey, setApiKey] = useState('') - const [keyState, setKeyState] = useState<'idle'|'signing'|'saving'|'ok'|'err'>('idle') - const [keyErr, setKeyErr] = useState('') - - // Subscribe state. `subPaperChoice` is the user's selection on the - // pre-subscribe form ("paper" or "live"); written to Subscription.paper_mode - // when the signed subscribe goes through. - const [subState, setSubState] = useState<'idle'|'signing'|'saving'|'err'>('idle') - const [subErr, setSubErr] = useState('') - const [subPaperChoice, setSubPaperChoice] = useState<'paper' | 'live'>('paper') - - // Manual window (convex-strategy "enable for N hours" override). - // null = no override active; ISO string = bot armed until that UTC time. - const [manualUntil, setManualUntil] = useState(null) - const [mwState, setMwState] = useState<'idle'|'signing'|'saving'|'err'>('idle') - const [mwErr, setMwErr] = useState('') - const [mwTick, setMwTick] = useState(0) // forces countdown re-render - - // Mounted flag: avoid SSR/CSR mismatch since wagmi state differs between the two. - const [mounted, setMounted] = useState(false) useEffect(() => { setMounted(true) }, []) - // Settings-load state. We do NOT auto-pop MetaMask — only fetch with a - // 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: { active: boolean hl_api_key_set: boolean hl_api_key_masked: string | null + paper_mode?: boolean settings: UserSettings manual_window_until?: string | null }) { setSubscribed(u.active) + setPaperMode(!!u.paper_mode) setHlApiKeySet(u.hl_api_key_set, u.hl_api_key_masked ?? undefined) setManualUntil(u.manual_window_until ?? null) if (u.settings) { const s = u.settings setTpConfigured(s.take_profit_pct != null) setSlConfigured(s.stop_loss_pct != null) - setBudgetConfigured(s.daily_budget_usd != null) + const hasBudget = s.daily_budget_usd != null + setUseBudget(hasBudget) setBotReadiness(u.hl_api_key_set ? 'saved' : 'unknown') setSettings({ ...s, take_profit_pct: s.take_profit_pct ?? 2, stop_loss_pct: s.stop_loss_pct ?? 1.5, daily_budget_usd: s.daily_budget_usd ?? 15, + trump_enabled: s.trump_enabled ?? false, + macro_enabled: s.macro_enabled ?? false, }) const hasSched = s.active_from != null && s.active_until != null setUseSchedule(hasSched) @@ -130,8 +117,6 @@ export default function BotConfigPanel() { } } - // On connect: fetch public state only (no signature). If a fresh view - // envelope is already cached in sessionStorage, silently hydrate full data. useEffect(() => { if (!mounted || !isConnected || !address) return let cancelled = false @@ -157,10 +142,8 @@ export default function BotConfigPanel() { setLoadErr(''); setLoadState('loading') try { const ok = await confirmSign({ - label: isZh ? '查看账户设置' : 'View account settings', - description: isZh - ? '读取你在 Trump Alpha 的订阅状态、风控参数和 API Key 状态。签名仅用于身份验证,不做任何链上操作。' - : 'Read your Trump Alpha subscription state, risk settings, and API key status. The signature is only for identity verification. No on-chain action is performed.', + label: 'View account settings', + description: 'Read your Trump Alpha subscription state, risk settings, and API key status. The signature is only for identity verification — no on-chain action is performed.', }) if (!ok) { setLoadState('idle'); return } const env = await getOrCreateViewEnvelope({ action: 'view_user', wallet: address, signMessageAsync }) @@ -168,7 +151,7 @@ export default function BotConfigPanel() { applyUserPayload(u) setLoadState('loaded') } catch (e: unknown) { - setLoadErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 120)) + setLoadErr(walletErrorLabel(e, 'Cancelled', 120)) setLoadState('err') } } @@ -190,16 +173,10 @@ export default function BotConfigPanel() { if (!address) return const paperMode = subPaperChoice === 'paper' const ok = await confirmSign({ - label: paperMode - ? (isZh ? '订阅 Trump Alpha(模拟模式)' : 'Subscribe to Trump Alpha (paper mode)') - : (isZh ? '订阅 Trump Alpha(真实资金)' : 'Subscribe to Trump Alpha (live capital)'), + label: paperMode ? 'Start in paper mode' : 'Start live trading', description: paperMode - ? (isZh - ? '注册为 Paper 模式订阅者。机器人开仓不涉及真实资金,仅供测试。' - : 'Register as a paper-mode subscriber. Trades are simulated and do not touch real funds.') - : (isZh - ? '注册为正式订阅者。完成后需配置 Hyperliquid API Key 才能实际开仓。' - : 'Register as a live subscriber. You still need to configure a Hyperliquid API key before any real trade can open.'), + ? 'Trades are simulated end-to-end — no funds are at risk. You can switch to live at any time from the settings panel.' + : 'Start live trading. You\'ll need to add a Hyperliquid API key (next step) before the bot can open any real position.', danger: !paperMode, }) if (!ok) return @@ -210,14 +187,13 @@ export default function BotConfigPanel() { setSubState('saving') await subscribe(env, { paper_mode: paperMode }) setSubscribed(true) + setPaperMode(paperMode) void refreshUserState(address) setSubState('idle') - setTpConfigured(false) - setSlConfigured(false) - setBudgetConfigured(false) - setLoadState('loaded') + setTpConfigured(false); setSlConfigured(false) + setUseBudget(false); setLoadState('loaded') } catch (e: unknown) { - setSubErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 100)) + setSubErr(walletErrorLabel(e, 'Cancelled', 100)) setSubState('err') } } @@ -225,10 +201,8 @@ export default function BotConfigPanel() { async function handleSaveSettings() { if (!address) return const ok = await confirmSign({ - label: isZh ? '保存交易风控参数' : 'Save trading risk settings', - description: isZh - ? '保存杠杆、仓位大小、止盈止损比例和每日预算设置。签名后立即生效。' - : 'Save leverage, position size, take-profit, stop-loss, and daily budget settings. Changes apply immediately after signing.', + label: 'Save trading settings', + description: 'Settings are saved to the server and apply to all new trades from this point forward. Open positions are not affected.', }) if (!ok) return setSaveErr(''); setSaveState('signing') @@ -238,32 +212,26 @@ export default function BotConfigPanel() { if (useSchedule) { schedFrom = localInputToIso(fromLocal) schedUntil = localInputToIso(untilLocal) - if (!schedFrom || !schedUntil) { - setSaveErr(isZh ? '请选择开始和结束时间' : 'Pick both a start and end time'); setSaveState('err'); return - } - if (new Date(schedUntil).getTime() <= new Date(schedFrom).getTime()) { - setSaveErr(isZh ? '结束时间必须晚于开始时间' : 'End must be after start'); setSaveState('err'); return - } + if (!schedFrom || !schedUntil) { setSaveErr('Pick both a start and end time'); setSaveState('err'); return } + if (new Date(schedUntil).getTime() <= new Date(schedFrom).getTime()) { setSaveErr('End must be after start'); setSaveState('err'); return } } const tp = settings.take_profit_pct, sl = settings.stop_loss_pct, bd = settings.daily_budget_usd - if (tp == null || tp < 0.1 || tp > 50) { setSaveErr(isZh ? '止盈必须在 0.1% 到 50% 之间' : 'Take profit must be 0.1 – 50%'); setSaveState('err'); return } - if (sl == null || sl < 0.1 || sl > 50) { setSaveErr(isZh ? '止损必须在 0.1% 到 50% 之间' : 'Stop loss must be 0.1 – 50%'); setSaveState('err'); return } - if (bd == null || bd <= 0 || bd > 100000){ setSaveErr(isZh ? '每日预算必须大于 0 美元' : 'Daily budget must be > $0'); setSaveState('err'); return } - const payload: UserSettings = { - ...settings, take_profit_pct: tp, stop_loss_pct: sl, daily_budget_usd: bd, - active_from: schedFrom, active_until: schedUntil, + if (trumpOn) { + if (tp == null || tp < 0.1 || tp > 50) { setSaveErr('Take profit must be 0.1–50%'); setSaveState('err'); return } + if (sl == null || sl < 0.1 || sl > 50) { setSaveErr('Stop loss must be 0.1–50%'); setSaveState('err'); return } } + if (useBudget && (bd == null || bd <= 0 || bd > 100000)) { setSaveErr('Daily budget must be > $0'); setSaveState('err'); return } + const payload: UserSettings = { ...settings, daily_budget_usd: useBudget ? bd : null, active_from: schedFrom, active_until: schedUntil } const env = await signRequest({ action: 'set_settings', wallet: address, body: payload, signMessageAsync }) setSaveState('saving') await setUserSettings(env, payload) setSettings(payload) - setTpConfigured(true); setSlConfigured(true); setBudgetConfigured(true) - setBotReadiness('saved') - setDirty(false) + setTpConfigured(tp != null); setSlConfigured(sl != null) + setBotReadiness('saved'); setDirty(false) setSaveState('ok') - setTimeout(() => setSaveState('idle'), 2000) + setTimeout(() => setSaveState('idle'), 2500) } catch (e: unknown) { - setSaveErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 100)) + setSaveErr(walletErrorLabel(e, 'Cancelled', 100)) setSaveState('err') } } @@ -272,13 +240,11 @@ export default function BotConfigPanel() { if (!address || !apiKey.trim()) return const trimmed = apiKey.trim() if (!trimmed.startsWith('0x') || trimmed.length !== 66) { - setKeyErr(isZh ? '必须是 0x 开头加 64 位十六进制字符' : 'Must be 0x + 64 hex chars'); setKeyState('err'); return + setKeyErr('Must be 0x + 64 hex chars'); setKeyState('err'); return } const ok = await confirmSign({ - label: isZh ? '绑定 Hyperliquid API Key' : 'Link Hyperliquid API key', - description: isZh - ? 'API Key 将加密存储在服务端,用于机器人在 Hyperliquid 上代替你下单。签名用于验证是你本人操作。' - : 'The API key is stored encrypted on the server and used by the bot to place trades on Hyperliquid for you. The signature confirms this action came from you.', + label: 'Link Hyperliquid API key', + description: 'The API key is stored encrypted on the server and used by the bot to place trades on Hyperliquid for you.', danger: true, }) if (!ok) return @@ -290,10 +256,9 @@ export default function BotConfigPanel() { setHlApiKeySet(true, res.masked_key) setBotReadiness('saved') void refreshUserState(address) - setApiKey('') - setKeyState('ok') + setApiKey(''); setKeyState('ok') } catch (e: unknown) { - setKeyErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 100)) + setKeyErr(walletErrorLabel(e, 'Cancelled', 100)) setKeyState('err') } } @@ -302,432 +267,287 @@ export default function BotConfigPanel() { setConnectErr('') try { const connector = await getFirstReadyConnector(connectors) - if (!connector) { - setConnectErr('No wallet connector is available right now.') - return - } + 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) { if (!address) return + const ok = await confirmSign(hours === 0 + ? { + label: 'Cancel override window', + description: 'The bot will stop accepting new trades from this override. Any open position risk controls (stop-loss, de-risk) remain active.', + } + : { + label: `Open ${hours}h override window`, + description: `The bot will accept qualifying signals for the next ${hours} hour${hours > 1 ? 's' : ''}, regardless of your trading schedule. Use this for high-conviction catalysts (e.g. CPI, FOMC). Stop-loss is always active.`, + danger: true, + } + ) + if (!ok) return setMwErr(''); setMwState('signing') try { - const env = await signRequest({ - action: 'set_manual_window', - wallet: address, - body: { hours }, - signMessageAsync, - }) + const env = await signRequest({ action: 'set_manual_window', wallet: address, body: { hours }, signMessageAsync }) setMwState('saving') const r = await setManualWindow(env, hours) - setManualUntil(r.manual_window_until) - setMwState('idle') + setManualUntil(r.manual_window_until); setMwState('idle') } catch (e: unknown) { - setMwErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 100)) + setMwErr(walletErrorLabel(e, 'Cancelled', 100)) setMwState('err') } } - // Countdown re-render every 15s while a manual window is armed. + async function handleUpgradeToLive() { + if (!address) return + const ok = await confirmSign({ + label: 'Switch to live trading', + description: 'Your subscription will change from paper mode to live. No trade opens immediately — you still need to add a Hyperliquid API key and enable a signal module before the bot can act.', + danger: true, + }) + if (!ok) return + setSubErr(''); setSubState('signing') + try { + // paper_mode=false → signedBody=null (same as original live subscribe) + const env = await signRequest({ action: 'subscribe', wallet: address, body: null, signMessageAsync }) + setSubState('saving') + await subscribe(env, { paper_mode: false }) + setPaperMode(false) + void refreshUserState(address) + setSubState('idle') + } catch (e: unknown) { + setSubErr(walletErrorLabel(e, 'Cancelled', 100)) + setSubState('err') + } + } + useEffect(() => { if (!manualUntil) return const id = setInterval(() => setMwTick(t => t + 1), 15000) return () => clearInterval(id) }, [manualUntil]) + // ── Not connected ────────────────────────────────────────────────────────── if (!mounted || !isConnected) { return ( -
-

- {isZh ? '连接钱包后即可配置机器人并查看你的交易。' : 'Connect your wallet to configure the bot and view your trades.'} -

- - {connectErr ? ( -

{connectErr}

- ) : null} +
+
Connect your wallet to continue
+
+ Your subscription, API key, and trade settings are wallet-bound. No account needed — just sign a message. +
+ + {connectErr &&

{connectErr}

}
) } - // ── small local helpers ─────────────────────────────────────────────────── - const Switch = ({ on, onChange, tone = 'amber' }: { on: boolean; onChange: (v: boolean) => void; tone?: 'amber' | 'up' | 'down' }) => ( + // ── helpers ──────────────────────────────────────────────────────────────── + const Switch = ({ on, onChange, tone = 'amber' }: { on: boolean; onChange: (v: boolean) => void; tone?: 'amber' | 'up' }) => ( ) - const saveBtnLabel = - saveState === 'signing' ? (isZh ? '钱包签名中…' : 'Sign in wallet…') - : saveState === 'saving' ? (isZh ? '保存中…' : 'Saving…') - : saveState === 'ok' ? (isZh ? '✓ 已保存' : '✓ Saved') - : (isZh ? '保存更改' : 'Save changes') + const trumpOn = !!settings.trump_enabled + const macroOn = !!settings.macro_enabled - const keyHint = - hlApiKeySet && !apiKey - ? (isZh - ? '已保存在你的机器人配置里。如果你在 Hyperliquid 轮换了 API 钱包,重新交易前请先在这里更新。' - : 'Saved in your bot profile. If you rotate the API wallet in Hyperliquid, update it here before trading again.') - : (isZh - ? '请使用 app.hyperliquid.xyz/API 生成的 Hyperliquid API 钱包私钥。不要在这里粘贴 MetaMask 助记词或私钥。' - : 'Use the Hyperliquid API wallet private key from app.hyperliquid.xyz/API. Never paste your MetaMask secret phrase or private key here.') - - const keyStatusHint = - keyState === 'ok' - ? (isZh - ? 'API 钱包已保存。建议下一步先用极小仓位跑一次你自己的 BTC 测试,再把这套配置当成生产可用。' - : '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' - }) { + // ── Not subscribed ───────────────────────────────────────────────────────── + if (!isSubscribed) { return ( - - {label} - +
+
Set up your trading bot
+
+ The bot places trades on Hyperliquid with a trade-only API key — it can never withdraw funds. + Start in paper mode to try it safely, or choose Live if you already have a Hyperliquid API key. +
+ + {/* Paper / Live choice */} +
+
+
+ {subPaperChoice === 'paper' ? '📝 Paper mode — simulated trades, no real funds' : '💰 Live trading — real positions on Hyperliquid'} +
+
+ {subPaperChoice === 'paper' + ? 'Nothing is at risk. No Hyperliquid API key needed. You can switch to live at any time.' + : 'Real positions will be opened and managed on Hyperliquid. A trade-only API key is required before the first trade.'} +
+
+
+ Paper + setSubPaperChoice(v ? 'live' : 'paper')} + tone="amber" + /> + Live +
+
+ +
+ + {subState === 'err' && {subErr}} +
+
) } - // ── Hyperliquid key strip ───────────────────────────────────────────────── - const hlKeyStrip = ( -
-
-
- {hlApiKeySet ? '✓' : '◇'} + // ── Subscribed but settings not loaded yet ───────────────────────────────── + if (loadState !== 'loaded') { + return ( +
+
Sign in to view your settings
+
+ Your settings are private and wallet-bound. Sign once to load them — the permission is cached for 4 minutes so you won't be prompted again while you browse.
-
-
Hyperliquid API wallet
-
- {!isSubscribed - ? 'Subscribe first to link your HL account' - : hlApiKeySet && !apiKey - ? <>Linked · {hlApiKeyMasked ?? '···'} · trade-only scope - : 'Paste your API wallet private key — trade-only, never withdraw'} -
+
+ + {loadState === 'err' && {loadErr}}
-
- {isSubscribed && hlApiKeySet && !apiKey && ( - - )} - {isSubscribed && (!hlApiKeySet || apiKey) && ( - <> - { setApiKey(e.target.value); if (keyState !== 'idle') setKeyState('idle') }} - placeholder="0x…" - style={{ width: 220, fontFamily: 'var(--mono)', fontSize: 12, padding: '8px 10px' }} - /> - - - )} -
-
{keyHint}
- {keyStatusHint && ( -
{keyStatusHint}
- )} - {keyState === 'err' && ( -
{keyErr}
- )} -
- ) + ) + } - // ── Setup readiness ─────────────────────────────────────────────────────── - const missingSetup: string[] = [] - if (!isSubscribed) missingSetup.push('subscribe to the bot') - if (!hlApiKeySet) missingSetup.push('link a Hyperliquid API wallet') - if (!tpConfigured) missingSetup.push('take-profit') - if (!slConfigured) missingSetup.push('stop-loss') - if (!budgetConfigured) missingSetup.push('daily budget') - const botReady = missingSetup.length === 0 + // ── Main config (subscribed + loaded) ────────────────────────────────────── - const setupSteps = [ - { label: '1. Subscribe', done: isSubscribed }, - { label: '2. Add HL API wallet', done: hlApiKeySet }, - { label: '3. Save risk limits', done: tpConfigured && slConfigured && budgetConfigured }, - ] + // Readiness check — shown at bottom near save button + const missingItems: string[] = [] + if (!paperMode && !hlApiKeySet) missingItems.push('link a Hyperliquid API key') + if (!trumpOn && !macroOn) missingItems.push('enable Trump Signal or Macro Vibes below') + if (trumpOn && !tpConfigured) missingItems.push('set a Trump Signal take-profit %') + if (trumpOn && !slConfigured) missingItems.push('set a Trump Signal stop-loss %') + const botReady = missingItems.length === 0 - const setupGuideMessage = - !isSubscribed ? 'Start by subscribing with the connected wallet. That unlocks the bot profile for this address.' - : botReadiness === 'ready' ? 'Your setup is verified. Before relying on automation, run one tiny BTC trade with your own wallet and Hyperliquid API wallet to confirm the live path end to end.' - : !hlApiKeySet ? 'Next, paste the Hyperliquid API wallet private key you generated inside Hyperliquid. Do not paste your MetaMask private key or seed phrase here.' - : 'Your exchange key is saved. Finish take-profit, stop-loss, and daily budget, then save changes. The backend still needs to verify this setup before we mark it ready.' + // Manual window countdown + void mwTick + const untilMs = manualUntil ? new Date(manualUntil).getTime() : 0 + const remainingMin = untilMs ? Math.max(0, Math.round((untilMs - Date.now()) / 60000)) : 0 + const armed = remainingMin > 0 + const remainingLabel = remainingMin >= 60 ? `${Math.floor(remainingMin / 60)}h ${remainingMin % 60}m` : `${remainingMin}m` + const mwBusy = mwState === 'signing' || mwState === 'saving' + + const saveBtnLabel = + saveState === 'signing' ? 'Sign in wallet…' + : saveState === 'saving' ? 'Saving…' + : saveState === 'ok' ? '✓ Saved' + : 'Save changes' return ( -
- {/* ── Setup progress banner ── */} -
-
- {setupSteps.map((step) => ( - - {step.done ? '✓ ' : '○ '}{step.label} - +
+ + {/* ── HL API Key ─────────────────────────────────────────────────────── */} +
+
+ {/* Status dot */} +
+ {(paperMode || hlApiKeySet) ? '✓' : '◇'} +
+
+
Hyperliquid API wallet
+
+ {paperMode + ? 📝 Paper mode — no real money involved. Add an API key below to switch to live. + : hlApiKeySet && !apiKey + ? <>{hlApiKeyMasked ?? '···'} · trade-only · cannot withdraw + : 'Generate at app.hyperliquid.xyz/API — paste the private key here. Never your MetaMask key.'} +
+
+ {/* Paper mode: show upgrade path instead of key input */} + {paperMode && ( + + )} + {/* Live mode: show key input */} + {!paperMode && (hlApiKeySet && !apiKey ? ( + + ) : ( +
+ { setApiKey(e.target.value); if (keyState !== 'idle') setKeyState('idle') }} + placeholder="0x…" + style={{ width: 200, maxWidth: '100%', fontFamily: 'var(--mono)', fontSize: 12, padding: '8px 10px' }} + /> + +
))}
-
{setupGuideMessage}
+ {keyState === 'err' &&
{keyErr}
} + {keyState === 'ok' &&
Saved. Run one small test trade to confirm the live path end-to-end.
} + {subState === 'err' && subErr &&
{subErr}
}
- {hlKeyStrip} - - {/* ── Manual window (convex-strategy override) ── */} - {isSubscribed && (() => { - const untilMs = manualUntil ? new Date(manualUntil).getTime() : 0 - // Touch the tick counter so countdown re-renders even if `manualUntil` is unchanged. - void mwTick - const remainingMin = untilMs ? Math.max(0, Math.round((untilMs - Date.now()) / 60000)) : 0 - const armed = remainingMin > 0 - const remainingLabel = - remainingMin >= 60 ? `${Math.floor(remainingMin / 60)}h ${remainingMin % 60}m` - : `${remainingMin}m` - const busy = mwState === 'signing' || mwState === 'saving' - - return ( -
-
-
-
- {armed ? '● Manually armed' : '○ Manual window'} -
-
- {armed - ? <>Bot is live for {remainingLabel} more — overrides any schedule. - : 'Arm the bot for a specific window. Best for catalysts (CPI, FOMC, news drops).'} -
-
- -
- {!armed && ( - <> - {[1, 4, 24].map(h => ( - - ))} - - )} - {armed && ( - - )} -
-
- {mwState === 'err' && ( -
{mwErr}
- )} -
- ) - })()} - - {/* ── Main settings card ── */} -
- {/* Header bar */} -
+ {/* ── Trump Signal ───────────────────────────────────────────────────── */} +
+ {/* Header */} +
-
Trading bot
-
Controls Trump Signal entries, Macro Vibes BTC management, and shared execution limits on Hyperliquid.
-
-
- {dirty && isSubscribed && ● Unsaved} - {saveState === 'err' && {saveErr}} - {!isSubscribed ? ( - // Subscribe button moved into the body to live alongside the - // paper-vs-live choice. Header keeps only status chips. - - ○ Not subscribed - - ) : ( - <> - - {botReady ? '● Bot ready' : '● Bot paused'} - - - - )} +
+ Trump Signal +
+
+ {trumpOn + ? 'Opens positions when a Trump Truth Social post fires a signal.' + : 'Disabled — bot ignores Trump posts.'} +
+ updateSettings({ trump_enabled: v })} tone="amber" />
- {/* Setup-incomplete warning */} - {isSubscribed && loadState === 'loaded' && !botReady && ( -
- -
-
Setup incomplete — the bot will not open any trades.
-
- Finish configuring: {missingSetup.join(' · ')}. Fill every required field below and save. The bot stays paused until every box is ticked. -
-
-
- )} - - {/* Setup-complete banner */} - {isSubscribed && loadState === 'loaded' && botReady && ( -
- -
-
Setup complete on this page.
-
- Wallet subscription, API wallet storage, and risk fields are all saved. Before you rely on automation, use your own account to run one tiny BTC test and confirm the live trade path behaves the way you expect. -
-
-
- )} - - {/* Body */} - {!isSubscribed ? ( -
-
- Subscribe to activate automated trading. The bot signs positions on Hyperliquid using an API wallet scoped to trade-only — it can never withdraw. You pick the size, leverage, risk limits, and signal threshold below. -
- - {/* ── Paper-vs-Live choice (P0.2) ──────────────────────────── - New users land on PAPER by default. Forces a conscious - opt-in to live trading rather than auto-routing real money. */} -
- {([ - { - key: 'paper', - title: '📝 Start in paper mode', - tag: 'Recommended', - body: 'Trades are simulated end-to-end. No funds at risk, no HL key needed. Promote to live anytime.', - }, - { - key: 'live', - title: '💰 Start live', - tag: 'Real money', - body: 'Bot trades real positions on Hyperliquid. Requires an HL API key. Risk = your full position size.', - }, - ] as const).map(opt => { - const active = subPaperChoice === opt.key - return ( - - ) - })} -
- - {/* The subscribe button moves down here so paper choice is - resolved before the wallet popup. */} -
- - {subState === 'err' && {subErr}} -
-
- ) : loadState !== 'loaded' ? ( -
-
- Sign a one-time message with your wallet to load your bot settings and trade history. The signature is cached for 4 minutes so repeat visits won't prompt again. -
-
- - {loadState === 'err' && {loadErr}} -
-
- ) : ( -
- {/* ── EXECUTION ── */} -
- Trump Signal settings -
-
-
- Event-driven trade sizing, leverage, and signal threshold for Trump-triggered entries. -
+ {/* Form — only when enabled */} + {trumpOn && ( +
- Per-trade size - Notional in USD. margin ≈ ${(settings.position_size_usd / settings.leverage).toFixed(2)} at {settings.leverage}× + Per-trade size + Notional in USD — margin ≈ ${(settings.position_size_usd / settings.leverage).toFixed(2)} at {settings.leverage}×
@@ -741,8 +561,8 @@ export default function BotConfigPanel() {
- Leverage (Trump / System 1) - Event-driven scalp positions + Leverage + Event-driven scalp — use lower leverage if unsure
@@ -754,118 +574,10 @@ export default function BotConfigPanel() {
- {(() => { - const aggressive = settings.sys2_mode === 'aggressive' - return ( - <> -
- Macro Vibes settings -
-
-
- Manage-only BTC mode, leverage, and pyramiding behavior for the Macro Vibes system. -
-
-
- BTC strategy mode (System 2) - - Choose how hard the BTC system presses winners. Standard is calmer. Aggressive adds more leverage and wider pyramiding. - -
-
-
- - -
- {aggressive && ( -
- High-risk sleeve. Uses more leverage, pyramids harder, and gives back more off the peak. Best treated as a separate risk bucket. -
- )} -
-
- - ) - })()} - - {(() => { - const aggressive = settings.sys2_mode === 'aggressive' - const defLev = aggressive ? 8 : 2 - const lev = Math.max(1, Math.min(10, settings.sys2_leverage ?? defLev)) - const prot = Math.min(35, 0.85 * 100 / lev) - const liq = 100 / lev - const risky = lev > 2 - const e1 = aggressive ? '¼' : '⅓' - const e2 = aggressive ? '¼' : '⅓' - return ( -
-
- BTC bottom leverage (System 2) - - Separate from Trump leverage. The bot de-risks in stages before the exchange liquidation line. - -
-
-
- updateSettings({ sys2_leverage: +e.target.value })} /> -
10×
-
- {lev}× -
- 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 - ? '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.'} -
-
-
- ) - })()} -
- Min AI confidence - Skip any signal below this score (0–100) + Min AI confidence + Skip signals below this score (0 = take all, 100 = only the highest-conviction)
@@ -877,36 +589,12 @@ export default function BotConfigPanel() {
- {/* ── LIMITS ── */} -
- Global risk limits -
-
-
- Shared safety rules that cap or gate automated trading across the account. -
+
- Daily budget * - Stop opening new trades once the day's total notional reaches this (UTC). Required. -
-
-
- $ - updateSettings({ daily_budget_usd: e.target.value === '' ? null : Math.max(0, +e.target.value) })} /> - /day -
- {!budgetConfigured && Not configured} -
-
- -
-
- Take profit * - Auto-close when unrealised gain hits target. Required. + Take profit * + Auto-close when unrealised gain hits this target. Required.
@@ -915,14 +603,14 @@ export default function BotConfigPanel() { onChange={e => updateSettings({ take_profit_pct: e.target.value === '' ? null : +e.target.value })} /> % gain
- {!tpConfigured && Not configured} + {!tpConfigured && Required — not saved}
-
+
- Stop loss * - Auto-close when drawdown hits limit. Required. + Stop loss * + Auto-close when drawdown hits this limit. Required.
@@ -931,68 +619,249 @@ export default function BotConfigPanel() { onChange={e => updateSettings({ stop_loss_pct: e.target.value === '' ? null : +e.target.value })} /> % loss
- {!slConfigured && Not configured} + {!slConfigured && Required — not saved}
+
+ )} +
- {/* ── SCHEDULE ── */} -
- Active schedule -
+ {/* ── Macro Vibes ────────────────────────────────────────────────────── */} +
+ {/* Header */} +
+
+
+ Macro Vibes
+
+ {macroOn + ? 'You open a BTC long on Hyperliquid, then use /adopt in the Telegram bot to hand it to the bot for exit management.' + : 'Disabled — Macro Vibes alerts will not include bot management instructions.'} +
+
+ updateSettings({ macro_enabled: v })} tone="up" /> +
+ {/* Form — only when enabled */} + {macroOn && (() => { + const aggressive = settings.sys2_mode === 'aggressive' + const defLev = aggressive ? 8 : 2 + const lev = Math.max(1, Math.min(10, settings.sys2_leverage ?? defLev)) + const prot = Math.min(35, 0.85 * 100 / lev) + const liq = 100 / lev + const risky = lev > 2 + return ( +
+ +
+
+ BTC strategy mode + Standard is calmer. Aggressive uses higher leverage and wider pyramiding. +
+
+
+ + +
+ {aggressive && ( +
+ High-risk sleeve. More leverage, harder pyramiding, wider peak-trail. Treat as a separate risk bucket. +
+ )} +
+
+ +
+
+ BTC bottom leverage + Separate from Trump leverage. The bot de-risks in stages before exchange liquidation. +
+
+
+ updateSettings({ sys2_leverage: +e.target.value })} /> +
10×
+
+ {lev}× +
+ At {lev}× it sheds ⅓ near −{(prot * 0.6).toFixed(0)}%, ⅓ near −{(prot * 0.8).toFixed(0)}%, fully out by −{prot.toFixed(0)}%. + Exchange liquidation ≈ −{liq.toFixed(0)}%. + {risky ? ' Above 2×, a normal correction can push you out early.' : ' Wide enough to survive a normal correction.'} +
+
+
+
+ ) + })()} +
+ + {/* ── Advanced ───────────────────────────────────────────────────────── */} +
+ + + {showAdvanced && ( +
+ + {/* Daily spend cap */}
- Only run in a window - Bot ignores signals outside this range. Shown in your browser's local time. + Daily trading cap + Optional — bot stops opening new trades once total notional crosses this limit in a UTC day. +
+
+ { setUseBudget(v); setDirty(true) }} /> + {useBudget ? ( +
+ $ + updateSettings({ daily_budget_usd: e.target.value === '' ? null : Math.max(0, +e.target.value) })} /> + /day +
+ ) : ( + No cap + )} +
+
+ + {/* Schedule */} +
+
+ Trading schedule + Bot only accepts signals inside this window. Times in your browser's local timezone. Leave off to trade anytime.
- { setUseSchedule(v); setDirty(true) }} /> + { setUseSchedule(v); setDirty(true) }} /> {useSchedule && (
From { setFromLocal(e.target.value); setDirty(true) }} - style={{ width: 190 }} /> + onChange={e => { setFromLocal(e.target.value); setDirty(true) }} style={{ width: 190 }} />
Until { setUntilLocal(e.target.value); setDirty(true) }} - style={{ width: 190 }} /> + onChange={e => { setUntilLocal(e.target.value); setDirty(true) }} style={{ width: 190 }} />
)}
- {useSchedule && fromLocal && untilLocal && (() => { - const fromMs = new Date(fromLocal).getTime() - const untilMs = new Date(untilLocal).getTime() - const nowMs = Date.now() - if (isNaN(fromMs) || isNaN(untilMs) || untilMs <= fromMs) return null - const inWindow = nowMs >= fromMs && nowMs < untilMs - const durH = (untilMs - fromMs) / 3600000 - const durLabel = durH < 1 ? `${Math.round(durH * 60)} min` - : durH < 48 ? `${durH.toFixed(1)} h` - : `${(durH / 24).toFixed(1)} d` - return ( -
-
-
- - {inWindow ? '● In window now' : '○ Outside window'} - - Duration: {durLabel} -
-
- ) - })()} + {/* Manual window */} +
+
+ Override window + Open a timed override so the bot accepts signals for the next 1–24 hours — useful for high-conviction catalysts like CPI or FOMC releases. +
+
+ {armed ? ( + <> + ● Override active — {remainingLabel} remaining + + + ) : ( + [1, 4, 24].map(h => ( + + )) + )} + {mwState === 'err' && {mwErr}} +
+
+
)}
+ + {/* ── Save ───────────────────────────────────────────────────────────── */} +
+ {/* Status */} +
+ {saveState === 'err' ? ( + ⚠ {saveErr} + ) : botReady ? ( + ✓ Bot is configured and ready + ) : ( + Still needed: {missingItems.join(' · ')} + )} +
+ {/* Button */} + +
+
) } diff --git a/lib/api.ts b/lib/api.ts index 9759265..2246a3e 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -96,6 +96,10 @@ export interface UserSettings { sys2_mode?: 'standard' | 'aggressive' | null active_from: string | null // ISO UTC active_until: string | null // ISO UTC + /** Master on/off switches for each module. false = module is disabled; + * bot ignores all signals for that module even if subscribed. */ + trump_enabled?: boolean | null + macro_enabled?: boolean | null } export interface UserData { @@ -104,6 +108,7 @@ export interface UserData { subscribed_at: string | null hl_api_key_set: boolean hl_api_key_masked: string | null + paper_mode: boolean trades: BotTrade[] settings: UserSettings /** ISO-UTC timestamp until which the bot is manually armed (null = use schedule). */