repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * clawd — Perps Screen |
| #3 | * |
| #4 | * Drives the real Vulcan CLI binary (Ellipsis-Labs/vulcan-cli) for live |
| #5 | * Phoenix perpetuals data and paper trading. All trades are paper-only. |
| #6 | * |
| #7 | * Keys: [q] quote [p] paper long [s] paper short |
| #8 | * [a] paper account [x] paper positions [r] refresh [b] back |
| #9 | */ |
| #10 | |
| #11 | import chalk from 'chalk'; |
| #12 | import type { PerpMarket } from '../state.js'; |
| #13 | import { |
| #14 | getPaperPositions, |
| #15 | getPaperStatus, |
| #16 | getTraderSnapshot, |
| #17 | loadVulcanMarkets, |
| #18 | normalizeSymbol, |
| #19 | runVulcanJson, |
| #20 | vulcanInstallHint, |
| #21 | type PaperPosition, |
| #22 | type PaperStatus, |
| #23 | type VulcanMarket, |
| #24 | } from '../vulcan.js'; |
| #25 | |
| #26 | // ─── Display helpers ────────────────────────────────────────────────────────── |
| #27 | |
| #28 | function pad(s: string, n: number): string { |
| #29 | return s.length >= n ? s.slice(0, n) : `${s}${' '.repeat(n - s.length)}`; |
| #30 | } |
| #31 | |
| #32 | function fmtPrice(p: number): string { |
| #33 | if (p === 0) return chalk.gray('---'); |
| #34 | if (p >= 10_000) return chalk.yellow(`$${p.toFixed(0)}`); |
| #35 | if (p >= 1) return chalk.yellow(`$${p.toFixed(2)}`); |
| #36 | return chalk.yellow(`$${p.toFixed(4)}`); |
| #37 | } |
| #38 | |
| #39 | function fmtFunding(rate: number): string { |
| #40 | if (rate === 0) return chalk.gray('--'); |
| #41 | const pct = `${(rate * 100).toFixed(4)}%`; |
| #42 | return rate >= 0 ? chalk.green(pct) : chalk.red(pct); |
| #43 | } |
| #44 | |
| #45 | function fmtOI(oi: number): string { |
| #46 | if (oi === 0) return chalk.gray('---'); |
| #47 | if (oi >= 1_000_000) return `$${(oi / 1_000_000).toFixed(2)}M`; |
| #48 | if (oi >= 1_000) return `$${(oi / 1_000).toFixed(1)}K`; |
| #49 | return `$${oi.toFixed(0)}`; |
| #50 | } |
| #51 | |
| #52 | function fmtChange(pct: number): string { |
| #53 | if (pct === 0) return chalk.gray(' ----'); |
| #54 | const s = `${pct >= 0 ? '+' : ''}${pct.toFixed(2)}%`; |
| #55 | return pct >= 0 ? chalk.green(s) : chalk.red(s); |
| #56 | } |
| #57 | |
| #58 | function fmtPnl(pnl: number): string { |
| #59 | if (pnl === 0) return chalk.gray('$0.00'); |
| #60 | return pnl >= 0 ? chalk.green(`+$${pnl.toFixed(2)}`) : chalk.red(`-$${Math.abs(pnl).toFixed(2)}`); |
| #61 | } |
| #62 | |
| #63 | // ─── View renderers ─────────────────────────────────────────────────────────── |
| #64 | |
| #65 | type ViewMode = 'markets' | 'account' | 'positions'; |
| #66 | |
| #67 | function renderMarkets(markets: VulcanMarket[]): void { |
| #68 | process.stdout.write( |
| #69 | ` ${chalk.cyan(pad('SYMBOL', 8))}${chalk.cyan(pad('MARK PRICE', 14))}${chalk.cyan(pad('24H', 10))}${chalk.cyan(pad('FUNDING', 13))}${chalk.cyan(pad('OI', 12))}${chalk.cyan(pad('MAX LEV', 8))}\n`, |
| #70 | ); |
| #71 | process.stdout.write(` ${chalk.cyan('─'.repeat(66))}\n`); |
| #72 | |
| #73 | if (markets.length === 0) { |
| #74 | process.stdout.write(` ${chalk.gray('No markets — fetching from Vulcan...')}\n`); |
| #75 | return; |
| #76 | } |
| #77 | |
| #78 | for (const m of markets) { |
| #79 | process.stdout.write( |
| #80 | ` ${chalk.white(pad(m.symbol, 8))}${pad(fmtPrice(m.markPrice), 14)}${pad(fmtChange(m.change24h), 10)}${pad(fmtFunding(m.fundingRate), 13)}${chalk.gray(pad(fmtOI(m.openInterest), 12))}${chalk.gray(`${m.maxLeverage}x`)}\n`, |
| #81 | ); |
| #82 | } |
| #83 | } |
| #84 | |
| #85 | function renderAccount(ps: PaperStatus | null, positions: PaperPosition[]): void { |
| #86 | if (!ps) { |
| #87 | process.stdout.write(` ${chalk.gray('Paper account not initialized — run: vulcan paper init')}\n`); |
| #88 | return; |
| #89 | } |
| #90 | process.stdout.write(` ${chalk.bold.white('PAPER ACCOUNT')}\n\n`); |
| #91 | process.stdout.write(` ${chalk.cyan('Balance')} ${chalk.white(`$${ps.balance.toFixed(2)}`)} USDC\n`); |
| #92 | process.stdout.write(` ${chalk.cyan('Equity')} ${chalk.white(`$${ps.equity.toFixed(2)}`)} USDC\n`); |
| #93 | process.stdout.write(` ${chalk.cyan('Unreal PnL')} ${fmtPnl(ps.unrealizedPnl)}\n`); |
| #94 | process.stdout.write(` ${chalk.cyan('Realized')} ${fmtPnl(ps.realizedPnl)}\n`); |
| #95 | process.stdout.write(` ${chalk.cyan('Fees Paid')} ${chalk.gray(`$${ps.feesPaid.toFixed(4)}`)}\n`); |
| #96 | process.stdout.write( |
| #97 | ` ${chalk.cyan('Positions')} ${chalk.white(String(ps.openPositions))} ${chalk.cyan('Orders')} ${chalk.white(String(ps.openOrders))} ${chalk.cyan('Fills')} ${chalk.white(String(ps.fills))}\n`, |
| #98 | ); |
| #99 | process.stdout.write(` ${chalk.cyan('Exposure')} ${chalk.white(`${(ps.exposureRatio * 100).toFixed(2)}%`)}\n`); |
| #100 | |
| #101 | if (positions.length > 0) { |
| #102 | process.stdout.write(`\n ${chalk.bold.white('OPEN POSITIONS')}\n`); |
| #103 | for (const pos of positions) { |
| #104 | const sideColor = pos.side === 'buy' || pos.side === 'long' ? chalk.green : chalk.red; |
| #105 | process.stdout.write( |
| #106 | ` ${chalk.cyan(pad(pos.symbol, 6))} ${sideColor(pad(pos.side.toUpperCase(), 6))} ${chalk.white(pos.sizeTokens.toFixed(2))} @ ${chalk.yellow(`$${pos.entryPrice.toFixed(2)}`)} pnl ${fmtPnl(pos.unrealizedPnl)}\n`, |
| #107 | ); |
| #108 | } |
| #109 | } |
| #110 | } |
| #111 | |
| #112 | function renderPositions(positions: PaperPosition[], markets: VulcanMarket[]): void { |
| #113 | process.stdout.write(` ${chalk.bold.white('PAPER POSITIONS')}\n\n`); |
| #114 | if (positions.length === 0) { |
| #115 | process.stdout.write(` ${chalk.gray('No open paper positions.')}\n`); |
| #116 | return; |
| #117 | } |
| #118 | process.stdout.write( |
| #119 | ` ${chalk.cyan(pad('SYMBOL', 8))}${chalk.cyan(pad('SIDE', 7))}${chalk.cyan(pad('SIZE', 10))}${chalk.cyan(pad('ENTRY', 12))}${chalk.cyan(pad('MARK', 12))}${chalk.cyan(pad('UNREAL PNL', 12))}\n`, |
| #120 | ); |
| #121 | process.stdout.write(` ${chalk.cyan('─'.repeat(64))}\n`); |
| #122 | |
| #123 | for (const pos of positions) { |
| #124 | const liveMarket = markets.find(m => m.symbol === pos.symbol); |
| #125 | const mark = liveMarket?.markPrice ?? pos.markPrice; |
| #126 | const sideColor = pos.side === 'buy' || pos.side === 'long' ? chalk.green : chalk.red; |
| #127 | process.stdout.write( |
| #128 | ` ${chalk.white(pad(pos.symbol, 8))}${sideColor(pad(pos.side.toUpperCase(), 7))}${chalk.white(pad(pos.sizeTokens.toFixed(2), 10))}${chalk.yellow(pad(`$${pos.entryPrice.toFixed(2)}`, 12))}${chalk.yellow(pad(mark > 0 ? `$${mark.toFixed(2)}` : '---', 12))}${fmtPnl(pos.unrealizedPnl)}\n`, |
| #129 | ); |
| #130 | } |
| #131 | } |
| #132 | |
| #133 | // ─── Screen shell ───────────────────────────────────────────────────────────── |
| #134 | |
| #135 | interface ScreenState { |
| #136 | markets: VulcanMarket[]; |
| #137 | status: string; |
| #138 | orderPrompt: string | null; |
| #139 | paperStatus: PaperStatus | null; |
| #140 | paperPositions: PaperPosition[]; |
| #141 | view: ViewMode; |
| #142 | } |
| #143 | |
| #144 | function renderScreen(s: ScreenState): void { |
| #145 | process.stdout.write('\x1b[2J\x1b[H'); |
| #146 | const W = Math.min(process.stdout.columns || 100, 110); |
| #147 | const border = chalk.cyan('─'.repeat(W - 4)); |
| #148 | |
| #149 | process.stdout.write( |
| #150 | `\n ${chalk.cyanBright.bold('📈 Phoenix Perpetuals · Vulcan CLI ')}${chalk.yellow('[PAPER]')}${chalk.gray(' · paper-only mode')}\n`, |
| #151 | ); |
| #152 | process.stdout.write(` ${border}\n\n`); |
| #153 | |
| #154 | if (s.view === 'account') renderAccount(s.paperStatus, s.paperPositions); |
| #155 | else if (s.view === 'positions') renderPositions(s.paperPositions, s.markets); |
| #156 | else renderMarkets(s.markets); |
| #157 | |
| #158 | process.stdout.write(`\n ${border}\n`); |
| #159 | if (s.orderPrompt) process.stdout.write(` ${chalk.cyanBright(s.orderPrompt)}\n`); |
| #160 | process.stdout.write(` ${chalk.gray(s.status)}\n\n`); |
| #161 | process.stdout.write( |
| #162 | ` ${chalk.gray('[q] quote [p] long [s] short [a] account [x] positions [r] refresh [b] back')}\n\n`, |
| #163 | ); |
| #164 | } |
| #165 | |
| #166 | // ─── Key handlers (split for complexity budget) ─────────────────────────────── |
| #167 | |
| #168 | function handleRefresh(s: ScreenState, redraw: () => void): void { |
| #169 | s.status = 'Refreshing...'; |
| #170 | redraw(); |
| #171 | Promise.all([loadVulcanMarkets(), getPaperStatus(), getPaperPositions()]).then(([mkt, ps, pp]) => { |
| #172 | s.markets = mkt.markets; |
| #173 | s.status = mkt.status; |
| #174 | s.paperStatus = ps; |
| #175 | s.paperPositions = pp; |
| #176 | s.orderPrompt = null; |
| #177 | redraw(); |
| #178 | }).catch(() => { /* silent */ }); |
| #179 | } |
| #180 | |
| #181 | function handlePaperOrder(s: ScreenState, side: 'buy' | 'sell', redraw: () => void): void { |
| #182 | const sym = normalizeSymbol(s.markets[0]?.symbol); |
| #183 | s.view = 'markets'; |
| #184 | s.orderPrompt = `Submitting paper ${side === 'buy' ? 'long' : 'short'} ${sym} $100 notional…`; |
| #185 | s.status = 'Paper mode — no real funds at risk'; |
| #186 | redraw(); |
| #187 | |
| #188 | runVulcanJson(['paper', side, sym, '--notional-usdc', '100']).then(result => { |
| #189 | if (result.ok) { |
| #190 | s.orderPrompt = `✔ Paper ${side === 'buy' ? 'long' : 'short'} filled: ${sym} $100`; |
| #191 | s.status = 'Fill recorded — refreshing account…'; |
| #192 | return Promise.all([getPaperStatus(), getPaperPositions()]); |
| #193 | } |
| #194 | s.orderPrompt = `✘ Failed: ${result.stderr.slice(0, 60) || vulcanInstallHint()}`; |
| #195 | s.status = 'Order failed'; |
| #196 | return Promise.resolve([null, []] as [PaperStatus | null, PaperPosition[]]); |
| #197 | }).then(([ps, pp]) => { |
| #198 | if (ps !== undefined) s.paperStatus = ps; |
| #199 | if (pp !== undefined) s.paperPositions = pp; |
| #200 | redraw(); |
| #201 | }).catch(() => { redraw(); }); |
| #202 | } |
| #203 | |
| #204 | function handleSnapshot(s: ScreenState, redraw: () => void): void { |
| #205 | s.view = 'markets'; |
| #206 | s.status = 'Loading trader snapshot…'; |
| #207 | redraw(); |
| #208 | getTraderSnapshot(s.markets[0]?.symbol ?? 'SOL').then(snap => { |
| #209 | s.markets = snap.markets; |
| #210 | s.paperStatus = snap.paperStatus ?? null; |
| #211 | s.paperPositions = snap.paperPositions ?? []; |
| #212 | s.status = `Snapshot: ${snap.source} · ${snap.markets.length} markets · ${s.paperPositions.length} positions`; |
| #213 | s.orderPrompt = null; |
| #214 | redraw(); |
| #215 | }).catch(() => { redraw(); }); |
| #216 | } |
| #217 | |
| #218 | function handleQuote(s: ScreenState, redraw: () => void): void { |
| #219 | s.view = 'markets'; |
| #220 | if (s.markets.length > 0) { |
| #221 | const m = s.markets[0]!; |
| #222 | s.status = `${m.symbol} mark ${fmtPrice(m.markPrice)} funding ${(m.fundingRate * 100).toFixed(4)}% OI ${fmtOI(m.openInterest)}`; |
| #223 | } else { |
| #224 | s.status = 'No markets loaded yet.'; |
| #225 | } |
| #226 | s.orderPrompt = null; |
| #227 | redraw(); |
| #228 | } |
| #229 | |
| #230 | function handleNumberKey(s: ScreenState, chunk: string, redraw: () => void): void { |
| #231 | const idx = Number.parseInt(chunk, 10) - 1; |
| #232 | const m = s.markets[idx]; |
| #233 | if (m) { |
| #234 | s.view = 'markets'; |
| #235 | s.status = `${m.symbol} mark ${fmtPrice(m.markPrice)} funding ${(m.fundingRate * 100).toFixed(4)}% OI ${fmtOI(m.openInterest)} 24h ${fmtChange(m.change24h)} max ${m.maxLeverage}x`; |
| #236 | s.orderPrompt = null; |
| #237 | redraw(); |
| #238 | } |
| #239 | } |
| #240 | |
| #241 | // ─── Main ───────────────────────────────────────────────────────────────────── |
| #242 | |
| #243 | export async function runPerps(): Promise<void> { |
| #244 | const s: ScreenState = { |
| #245 | markets: [], |
| #246 | status: 'Fetching markets from Vulcan...', |
| #247 | orderPrompt: null, |
| #248 | paperStatus: null, |
| #249 | paperPositions: [], |
| #250 | view: 'markets', |
| #251 | }; |
| #252 | |
| #253 | const redraw = (): void => renderScreen(s); |
| #254 | redraw(); |
| #255 | |
| #256 | Promise.all([loadVulcanMarkets(), getPaperStatus(), getPaperPositions()]).then(([mkt, ps, pp]) => { |
| #257 | s.markets = mkt.markets; |
| #258 | s.status = mkt.status; |
| #259 | s.paperStatus = ps; |
| #260 | s.paperPositions = pp; |
| #261 | redraw(); |
| #262 | }).catch(err => { |
| #263 | s.status = `Load error: ${String(err).slice(0, 80)}`; |
| #264 | redraw(); |
| #265 | }); |
| #266 | |
| #267 | return new Promise<void>(resolve => { |
| #268 | if (process.stdin.setRawMode) process.stdin.setRawMode(true); |
| #269 | process.stdin.resume(); |
| #270 | process.stdin.setEncoding('utf8'); |
| #271 | |
| #272 | const stopRaw = (): void => { if (process.stdin.setRawMode) process.stdin.setRawMode(false); }; |
| #273 | |
| #274 | const setView = (v: ViewMode): void => { s.view = v; s.orderPrompt = null; redraw(); }; |
| #275 | |
| #276 | // Dispatch table: lowercase key → handler |
| #277 | const dispatch: Record<string, () => void> = { |
| #278 | r: () => handleRefresh(s, redraw), |
| #279 | a: () => setView('account'), |
| #280 | x: () => setView('positions'), |
| #281 | q: () => handleQuote(s, redraw), |
| #282 | p: () => handlePaperOrder(s, 'buy', redraw), |
| #283 | s: () => handlePaperOrder(s, 'sell', redraw), |
| #284 | t: () => handleSnapshot(s, redraw), |
| #285 | }; |
| #286 | |
| #287 | const onData = (chunk: string): void => { |
| #288 | if (chunk === 'b' || chunk === 'B' || chunk === '\x1b') { |
| #289 | process.stdin.off('data', onData); stopRaw(); resolve(); return; |
| #290 | } |
| #291 | if (chunk === '\x03') { process.stdin.off('data', onData); stopRaw(); process.exit(0); } |
| #292 | const handler = dispatch[chunk.toLowerCase()]; |
| #293 | if (handler) { handler(); return; } |
| #294 | if (/^[1-9]$/.test(chunk)) handleNumberKey(s, chunk, redraw); |
| #295 | }; |
| #296 | |
| #297 | process.stdin.on('data', onData); |
| #298 | }); |
| #299 | } |
| #300 | |
| #301 | // Re-export PerpMarket so state.ts import stays satisfied in other files |
| #302 | export type { PerpMarket }; |
| #303 |