Files
trumpsignal-frontend/lib/api.ts
T
k b76de36af0 refactor(settings): redesign BotConfigPanel + fix paper mode + clarify all copy
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 <noreply@anthropic.com>
2026-05-27 23:10:55 +08:00

552 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
TrumpPost, Candle, BotTrade, BotPerformance,
KolPostSummary, KolPostDetail, KolDigest,
KolWallet, KolHoldingChange, KolDivergence,
} from '@/types'
import type { SignedEnvelope } from './signedRequest'
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
function getApiOrigin() {
return typeof window === 'undefined'
? `${BASE_URL}/api`
: '/api/proxy/api'
}
async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${getApiOrigin()}${path}`, {
headers: { 'Content-Type': 'application/json' },
...init,
})
if (!res.ok) {
let detail = `API error ${res.status}`
try {
const j = await res.json()
if (j.detail) detail = `${res.status}: ${j.detail}`
} catch {}
throw new Error(detail)
}
return res.json() as Promise<T>
}
export async function getPosts(limit = 20, page = 1): Promise<TrumpPost[]> {
return fetchJson<TrumpPost[]>(`/posts?limit=${limit}&page=${page}`)
}
export async function getPost(id: number): Promise<TrumpPost> {
return fetchJson<TrumpPost>(`/posts/${id}`)
}
export async function getPrices(asset: 'BTC' | 'ETH', tf: string): Promise<Candle[]> {
return fetchJson<Candle[]>(`/prices/${asset}?tf=${tf}`)
}
export async function getTrades(
wallet: string,
env: SignedEnvelope,
limit = 20,
page = 1,
): Promise<BotTrade[]> {
const qs = [
`wallet=${wallet.toLowerCase()}`,
`ts=${env.timestamp}`,
`sig=${encodeURIComponent(env.signature)}`,
`limit=${limit}`,
`page=${page}`,
].join('&')
return fetchJson<BotTrade[]>(`/trades?${qs}`)
}
export async function getPerformance(
wallet: string,
env: SignedEnvelope,
): Promise<BotPerformance> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<BotPerformance>(`/performance?${qs}`)
}
export async function subscribe(
env: SignedEnvelope,
options: { paper_mode?: boolean } = {},
): Promise<{ status: string; wallet: string; paper_mode: boolean }> {
// paper_mode is a top-level body field (not nested) so the canonical
// signed body matches the backend's expectation: { paper_mode: true } | null.
const payload = options.paper_mode
? { ...env, paper_mode: true }
: env
return fetchJson<{ status: string; wallet: string; paper_mode: boolean }>(`/subscribe`, {
method: 'POST',
body: JSON.stringify(payload),
})
}
export interface UserSettings {
leverage: number
position_size_usd: number
take_profit_pct: number | null
stop_loss_pct: number | null
min_confidence: number
daily_budget_usd: number | null
/** System-2 (bottom reversal) leverage, 110. null = platform default.
* Independent of `leverage` (Trump). The protective stop auto-scales to
* this so the position is never exchange-liquidated. */
sys2_leverage: number | null
/** System-2 risk mode: 'standard' (cycle-rider) or 'aggressive'
* (separately-funded high-risk/high-explosiveness sleeve). */
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 {
wallet_address: string
active: boolean
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). */
manual_window_until: string | null
}
export async function setUserSettings(
env: SignedEnvelope,
settings: UserSettings,
): Promise<UserSettings> {
return fetchJson<UserSettings>(`/user/${env.wallet}/settings`, {
method: 'PUT',
body: JSON.stringify({ ...env, settings }),
})
}
export interface UserPublic {
wallet_address: string
active: boolean
hl_api_key_set: boolean
/** Phase 1 operational state — surfaced on /signals page. */
paper_mode?: boolean
manual_window_until?: string | null
circuit_breaker_tripped_at?: string | null
circuit_breaker_reason?: string | null
/** Master Auto-Trade gate. false (default) = signals shown, not traded. */
auto_trade?: boolean
}
export interface SignalSource {
source: string
count: number
latest: string | null
}
export async function getSignalSources(): Promise<{ sources: SignalSource[] }> {
return fetchJson<{ sources: SignalSource[] }>(`/signals/sources`)
}
/**
* Toggle paper mode for a wallet. Hits the dev endpoint — no signature
* required. Only available in development environments where the dev router
* is mounted; in production this will 404.
*/
export async function setPaperMode(
wallet: string,
enabled: boolean,
): Promise<{ status: string; paper_mode: boolean }> {
return fetchJson<{ status: string; paper_mode: boolean }>(
`/dev/paper-mode?wallet=${wallet.toLowerCase()}&enabled=${enabled}`,
{ method: 'POST' },
)
}
// ─── Open positions + today stats ───────────────────────────────────────────
export interface OpenPosition {
trade_id: number
asset: string
side: 'long' | 'short'
entry_price: number
current_price: number | null
size_usd: number | null
leverage: number | null
opened_at: string
hold_minutes: number
unrealized_pct: number | null
unrealized_usd: number | null
is_paper: boolean
trigger_post_id: number | null
/** System-2 staged lifecycle. size_usd above is the OPEN notional after
* de-risk; realized_usd is PnL already banked by partial de-risk. */
realized_usd?: number | null
derisk_steps?: number
addon_steps?: number
/** Per-trade pyramiding switch. */
grow_mode?: boolean
}
export interface OpenPositionsResponse {
wallet: string
count: number
positions: OpenPosition[]
}
export interface TodayStats {
wallet: string
realized_pnl_usd: number
trades_closed: number
wins: number
losses: number
open_count: number
/** Cumulative PnL banked by staged de-risk on still-open positions.
* Shown separately — not part of realized_pnl_usd (closed trades only). */
open_realized_usd?: number
}
export async function getOpenPositions(
wallet: string,
env: SignedEnvelope,
): Promise<OpenPositionsResponse> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<OpenPositionsResponse>(`/positions/open?${qs}`)
}
export async function getTodayStats(
wallet: string,
env: SignedEnvelope,
): Promise<TodayStats> {
const qs = `wallet=${wallet.toLowerCase()}&ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<TodayStats>(`/positions/today?${qs}`)
}
// ─── Scanner control (module 2) ─────────────────────────────────────────────
export interface ScannerSummary {
name: string
enabled: boolean
last_run_at: string | null
last_status: string
last_message: string | null
last_fired_at: string | null
total_runs: number
total_fires: number
consecutive_errors: number
}
export interface ScannersResponse {
count: number
enabled: number
scanners: ScannerSummary[]
}
export async function getScanners(): Promise<ScannersResponse> {
return fetchJson<ScannersResponse>(`/scanners`)
}
/**
* Toggle ONE scanner. Signed + subscriber-gated on the backend: the wallet
* must own the signature AND already be a subscriber. The signed body is
* { enabled, name } — must match the backend's canonical hash exactly.
*/
export async function toggleScanner(
env: SignedEnvelope,
name: string,
enabled: boolean,
): Promise<{ status: string; name: string; enabled: boolean }> {
return fetchJson<{ status: string; name: string; enabled: boolean }>(
`/scanners/${name}/toggle`,
{
method: 'POST',
body: JSON.stringify({ ...env, name, enabled }),
},
)
}
export async function killAllScanners(
env: SignedEnvelope,
): Promise<{ status: string; count: number }> {
return fetchJson<{ status: string; count: number }>(
`/scanners/all/disable`,
{ method: 'POST', body: JSON.stringify(env) },
)
}
export async function reviveAllScanners(
env: SignedEnvelope,
): Promise<{ status: string; count: number }> {
return fetchJson<{ status: string; count: number }>(
`/scanners/all/enable`,
{ method: 'POST', body: JSON.stringify(env) },
)
}
export interface CloseTradeResponse {
status: string
trade_id: number
exit_price: number | null
pnl_usd: number | null
reason: string
note: string | null
}
/**
* Manual emergency close. Requires a signed envelope produced for the action
* "close_trade" with body { trade_id }. The backend re-checks wallet ownership.
*/
export async function manualCloseTrade(
env: SignedEnvelope,
trade_id: number,
): Promise<CloseTradeResponse> {
return fetchJson<CloseTradeResponse>(`/positions/${trade_id}/close`, {
method: 'POST',
body: JSON.stringify({ ...env, trade_id }),
})
}
/** No-auth boolean state. Safe to call on every page load. */
export async function getUserPublic(wallet: string): Promise<UserPublic> {
return fetchJson<UserPublic>(`/user/${wallet.toLowerCase()}/public`)
}
/** Full user data (trades, settings). Requires signed envelope. */
export async function getUser(
wallet: string,
env: SignedEnvelope,
): Promise<UserData> {
const qs = `ts=${env.timestamp}&sig=${encodeURIComponent(env.signature)}`
return fetchJson<UserData>(`/user/${wallet.toLowerCase()}?${qs}`)
}
export interface WindowAccuracy {
checked: number
correct: number
accuracy_pct: number
}
export interface SignalAccuracyEntry {
count: number
m5: WindowAccuracy
m15: WindowAccuracy
m1h: WindowAccuracy
}
export interface SignalAccuracy {
overall: { m5: WindowAccuracy; m15: WindowAccuracy; m1h: WindowAccuracy }
by_signal: Record<string, SignalAccuracyEntry>
total_directional_signals: number
}
export async function getSignalAccuracy(): Promise<SignalAccuracy> {
return fetchJson<SignalAccuracy>(`/signals/accuracy`)
}
export async function setHlApiKey(
env: SignedEnvelope,
api_key: string,
): Promise<{ status: string; masked_key: string }> {
return fetchJson<{ status: string; masked_key: string }>(
`/user/${env.wallet}/hl-api-key`,
{
method: 'PUT',
body: JSON.stringify({ ...env, api_key }),
},
)
}
/**
* Arm (or clear) the manual-window override. Pass `hours = 0` to disarm.
* Backend stores `manual_window_until = now + hours`; when in the future the
* bot trades regardless of the schedule.
*/
/** Master Auto-Trade switch (signed). enabled=true also acknowledges +
* clears a tripped circuit breaker. */
export async function setAutoTrade(
env: SignedEnvelope,
enabled: boolean,
): Promise<{ auto_trade: boolean; circuit_breaker_cleared: boolean }> {
return fetchJson<{ auto_trade: boolean; circuit_breaker_cleared: boolean }>(
`/user/${env.wallet}/auto-trade`,
{ method: 'POST', body: JSON.stringify({ ...env, enabled }) },
)
}
/** Per-trade Grow (pyramiding) switch (signed). */
export async function setTradeGrow(
env: SignedEnvelope,
trade_id: number,
enabled: boolean,
): Promise<{ trade_id: number; grow_mode: boolean }> {
return fetchJson<{ trade_id: number; grow_mode: boolean }>(
`/positions/${trade_id}/grow`,
{ method: 'POST', body: JSON.stringify({ ...env, trade_id, enabled }) },
)
}
export async function setManualWindow(
env: SignedEnvelope,
hours: number,
): Promise<{ manual_window_until: string | null }> {
return fetchJson<{ manual_window_until: string | null }>(
`/user/${env.wallet}/manual-window`,
{
method: 'POST',
body: JSON.stringify({ ...env, hours }),
},
)
}
// ── KOL module ────────────────────────────────────────────────────
export async function getKolPosts(opts: {
handle?: string; source?: string; limit?: number; page?: number
} = {}): Promise<{ items: KolPostSummary[]; page: number; limit: number }> {
const p = new URLSearchParams()
if (opts.handle) p.set('handle', opts.handle)
if (opts.source) p.set('source', opts.source)
p.set('limit', String(opts.limit ?? 50))
p.set('page', String(opts.page ?? 1))
return fetchJson(`/kol/posts?${p.toString()}`)
}
export async function getKolPost(id: number): Promise<KolPostDetail> {
return fetchJson<KolPostDetail>(`/kol/posts/${id}`)
}
export async function getKolDigest(days = 7): Promise<KolDigest> {
return fetchJson<KolDigest>(`/kol/digest?days=${days}`)
}
export async function getKolWallets(handle?: string): Promise<{ wallets: KolWallet[] }> {
const p = handle ? `?handle=${handle}` : ''
return fetchJson<{ wallets: KolWallet[] }>(`/kol/wallets${p}`)
}
export async function addKolWallet(body: {
handle: string; chain: string; address: string; label?: string; source_url?: string
}): Promise<KolWallet> {
return fetchJson<KolWallet>(`/kol/wallets`, {
method: 'POST',
body: JSON.stringify(body),
})
}
export async function getKolChanges(opts: {
handle?: string; days?: number
} = {}): Promise<{ window_days: number; since: string; count: number; changes: KolHoldingChange[] }> {
const p = new URLSearchParams()
if (opts.handle) p.set('handle', opts.handle)
p.set('days', String(opts.days ?? 7))
return fetchJson(`/kol/changes?${p.toString()}`)
}
export async function getKolDivergence(opts: {
handle?: string; ticker?: string; signal_type?: 'divergence' | 'alignment'; days?: number
} = {}): Promise<{ window_days: number; since: string; count: number; items: KolDivergence[] }> {
const p = new URLSearchParams()
if (opts.handle) p.set('handle', opts.handle)
if (opts.ticker) p.set('ticker', opts.ticker)
if (opts.signal_type) p.set('signal_type', opts.signal_type)
p.set('days', String(opts.days ?? 30))
return fetchJson(`/kol/divergence?${p.toString()}`)
}
// ── Funding rate reversal (BTC tab) ─────────────────────────────────────────
export interface FundingSnapshot {
ok: boolean
error?: string
asset?: string
cadence_hours?: number
coverage_days?: number
latest_rate_pct?: number
last_24h_avg_pct?: number
cum_30d_pct?: number
extreme_threshold_pct?: number
signal_fired?: boolean
debug?: { reason?: string; direction?: string; [k: string]: unknown }
history?: { t: number; rate_pct: number }[]
}
export async function getFundingSnapshot(): Promise<FundingSnapshot> {
return fetchJson<FundingSnapshot>('/funding/snapshot')
}
// ── Macro snapshot (BTC page Macro Bottom tab) ──────────────────────────────
export interface MacroSnapshot {
ok: boolean
error?: string
date?: string
captured_at?: string
indicators?: {
ahr999: number | null
altcoin_season_index: number | null
fear_greed: number | null
fear_greed_label: string | null
btc_dominance_pct: number | null
eth_btc_ratio: number | null
stablecoin_supply_usd: number | null
etf_flow_net_usd_1d: number | null
btc_open_interest_usd: number | null
}
composite_score?: number | null
regime_label?: string | null
}
export async function getMacroSnapshot(): Promise<MacroSnapshot> {
return fetchJson<MacroSnapshot>('/macro/snapshot')
}
// ── Telegram alerts ─────────────────────────────────────────────────────────
export interface TelegramStatus {
configured: boolean
bot_username: string | null
bound: boolean
wallet_address?: string | null
tg_username?: string | null
chat_id?: number | null
alerts_enabled?: boolean | null
alert_trump?: boolean | null
alert_btc_bottom?: boolean | null
alert_funding?: boolean | null
alert_kol_divergence?: boolean | null
min_confidence?: number | null
mute_from_hour?: number | null
mute_until_hour?: number | null
total_alerts_sent?: number | null
}
export interface TelegramInitResp {
code: string; deep_link: string; expires_in_seconds: number
}
export async function getTelegramStatus(wallet: string): Promise<TelegramStatus> {
return fetchJson<TelegramStatus>(`/telegram/${wallet}/status`)
}
// SignedEnvelope is already imported at the top of this file. Below we
// reuse it directly for the Telegram helpers — no separate alias needed.
export async function tgInit(wallet: string, env: SignedEnvelope): Promise<TelegramInitResp> {
return fetchJson(`/telegram/${wallet}/init`, {
method: 'POST', body: JSON.stringify(env),
})
}
export async function tgUnbind(wallet: string, env: SignedEnvelope): Promise<{ removed: number }> {
return fetchJson(`/telegram/${wallet}/unbind`, {
method: 'POST', body: JSON.stringify(env),
})
}
export async function tgTest(wallet: string, env: SignedEnvelope): Promise<{ sent: boolean }> {
return fetchJson(`/telegram/${wallet}/test`, {
method: 'POST', body: JSON.stringify(env),
})
}
export async function tgPreferences(
wallet: string,
env: SignedEnvelope,
prefs: Partial<{
alerts_enabled: boolean; alert_trump: boolean; alert_btc_bottom: boolean
alert_funding: boolean; alert_kol_divergence: boolean
min_confidence: number; mute_from_hour: number; mute_until_hour: number
}>,
): Promise<TelegramStatus> {
return fetchJson(`/telegram/${wallet}/preferences`, {
method: 'POST', body: JSON.stringify({ ...env, ...prefs }),
})
}