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 | #!/usr/bin/env node |
| #2 | import { readFileSync } from "node:fs"; |
| #3 | import { spawnSync } from "node:child_process"; |
| #4 | import { dirname, join } from "node:path"; |
| #5 | import { fileURLToPath } from "node:url"; |
| #6 | import { parseArgs } from "node:util"; |
| #7 | |
| #8 | import { createState, openPosition, closePosition, unrealisedPnl } from "./state.js"; |
| #9 | import type { State } from "./state.js"; |
| #10 | import { SynthObserver, rejectMainnet } from "./observe.js"; |
| #11 | import { validate, parseRalphConfig } from "./validate.js"; |
| #12 | import type { RalphConfig, Decision } from "./validate.js"; |
| #13 | import { appendTick, clearJournal, readLastEntries } from "./journal.js"; |
| #14 | import type { TickEntry } from "./journal.js"; |
| #15 | import { claudeDecision, deterministicDecision } from "./claude-decision.js"; |
| #16 | import type { Observations } from "./claude-decision.js"; |
| #17 | |
| #18 | const __dirname = dirname(fileURLToPath(import.meta.url)); |
| #19 | |
| #20 | const { values: flags } = parseArgs({ |
| #21 | options: { |
| #22 | ticks: { type: "string", default: "50" }, |
| #23 | sleep: { type: "string", default: "" }, |
| #24 | seed: { type: "string", default: "42" }, |
| #25 | "commit-every": { type: "string", default: "0" }, |
| #26 | tui: { type: "boolean", default: false }, |
| #27 | llm: { type: "boolean", default: false }, |
| #28 | goblin: { type: "boolean", default: false }, |
| #29 | fresh: { type: "boolean", default: false }, |
| #30 | }, |
| #31 | strict: false, |
| #32 | }); |
| #33 | |
| #34 | const GOBLIN_MODE = Boolean(flags["goblin"]); |
| #35 | const RALPH_FILE = GOBLIN_MODE ? "goblin.md" : "RALPH.md"; |
| #36 | const CONFIG_PATH = join(__dirname, RALPH_FILE); |
| #37 | const CONFIG_PROMPT = readFileSync(CONFIG_PATH, "utf8"); |
| #38 | const CONFIG: RalphConfig = parseRalphConfig(CONFIG_PROMPT); |
| #39 | const TICKS = Number.parseInt(String(flags["ticks"]), 10) || (GOBLIN_MODE ? 100 : 50); |
| #40 | const SLEEP_MS = String(flags["sleep"]).trim() |
| #41 | ? Math.max(0, Math.round(Number.parseFloat(String(flags["sleep"])) * 1000)) |
| #42 | : CONFIG.tick_sleep_ms; |
| #43 | const SEED = Number.parseInt(String(flags["seed"]), 10) || 42; |
| #44 | const COMMIT_EVERY = Number.parseInt(String(flags["commit-every"]), 10) || 0; |
| #45 | const TUI_MODE = Boolean(flags["tui"]); |
| #46 | const USE_LLM = Boolean(flags["llm"]) || GOBLIN_MODE; |
| #47 | |
| #48 | function emit(obj: unknown): void { |
| #49 | if (TUI_MODE) process.stdout.write(`${JSON.stringify(obj)}\n`); |
| #50 | } |
| #51 | |
| #52 | function log(msg: string): void { |
| #53 | if (!TUI_MODE) process.stderr.write(`${msg}\n`); |
| #54 | } |
| #55 | |
| #56 | function sleep(ms: number): Promise<void> { |
| #57 | return new Promise((resolve) => setTimeout(resolve, ms)); |
| #58 | } |
| #59 | |
| #60 | function commitJournal(tick: number): void { |
| #61 | if (COMMIT_EVERY <= 0 || tick % COMMIT_EVERY !== 0) return; |
| #62 | const cwd = join(__dirname, "../../.."); |
| #63 | spawnSync("git", ["add", "src/ooda/journal/ticks.jsonl"], { cwd, stdio: "ignore" }); |
| #64 | const result = spawnSync("git", ["commit", "-m", `ooda: journal tick ${tick}`], { cwd, stdio: "ignore" }); |
| #65 | if (result.status === 0) log(`[git] committed journal at tick ${tick}`); |
| #66 | } |
| #67 | |
| #68 | function moltNote(tick: number, state: State): string | undefined { |
| #69 | if (!CONFIG.goblin || tick % 5 !== 0) return undefined; |
| #70 | const stance = state.total_pnl_lamports >= 0 ? "pressure rewarded aggression" : "chaos taxed the shell"; |
| #71 | return `molt ${tick / 5}: ${stance}; pnl=${state.total_pnl_lamports}; losses=${state.consecutive_losses}`; |
| #72 | } |
| #73 | |
| #74 | async function runLoop(): Promise<void> { |
| #75 | if (flags["fresh"]) clearJournal(); |
| #76 | |
| #77 | const rpcUrl = process.env["SOLANA_RPC_URL"] ?? "https://api.devnet.solana.com"; |
| #78 | rejectMainnet(rpcUrl); |
| #79 | |
| #80 | if (CONFIG.mode !== "paper" || CONFIG.network !== "devnet") { |
| #81 | throw new Error("[SAFETY] Goblin OODA supports paper/devnet only"); |
| #82 | } |
| #83 | |
| #84 | if (GOBLIN_MODE) { |
| #85 | log("\nGOBLIN MODE ACTIVATED - paper/devnet only"); |
| #86 | log(`max_pos=${CONFIG.max_position_size_lamports} killswitch=${CONFIG.loss_killswitch_consecutive} dark_defi=${CONFIG.dark_defi_armed}\n`); |
| #87 | } else { |
| #88 | log(`[ralph] mode=${CONFIG.mode} network=${CONFIG.network}`); |
| #89 | } |
| #90 | |
| #91 | const state = createState(); |
| #92 | const observer = new SynthObserver(SEED, 150_000, 20); |
| #93 | log(`[ralph] starting ${TICKS} ticks, sleep=${SLEEP_MS}ms, llm=${USE_LLM}, goblin=${GOBLIN_MODE}`); |
| #94 | emit({ event: "start", ticks: TICKS, config: CONFIG, goblin: GOBLIN_MODE }); |
| #95 | |
| #96 | for (let tick = 1; tick <= TICKS; tick += 1) { |
| #97 | state.tick = tick; |
| #98 | const now = new Date(); |
| #99 | const observed = observer.tick(now); |
| #100 | const candles = observed.candles; |
| #101 | state.candles = candles; |
| #102 | const currentPrice = candles.at(-1)!.c; |
| #103 | |
| #104 | const obs: Observations = { |
| #105 | tick, |
| #106 | now: now.toISOString(), |
| #107 | mode: "paper", |
| #108 | network: "devnet", |
| #109 | candles: candles.slice(-10), |
| #110 | whale_activity: observed.whale_activity, |
| #111 | book: { |
| #112 | positions: state.book.positions, |
| #113 | cash_lamports: state.book.cash_lamports, |
| #114 | }, |
| #115 | last_decisions: readLastEntries(3), |
| #116 | config: { |
| #117 | goblin: CONFIG.goblin, |
| #118 | max_position_size_lamports: CONFIG.max_position_size_lamports, |
| #119 | loss_killswitch_consecutive: CONFIG.loss_killswitch_consecutive, |
| #120 | model: CONFIG.model, |
| #121 | }, |
| #122 | }; |
| #123 | |
| #124 | let rawDecision: unknown; |
| #125 | try { |
| #126 | rawDecision = USE_LLM ? await claudeDecision(obs, CONFIG_PROMPT) : deterministicDecision(obs); |
| #127 | } catch (err) { |
| #128 | rawDecision = { action: "hold", reason: `decision error: ${String(err).slice(0, 100)}` }; |
| #129 | } |
| #130 | |
| #131 | const validation = validate(rawDecision, CONFIG, state.book); |
| #132 | const decision: Decision = validation.decision; |
| #133 | let outcome: TickEntry["outcome"] = "applied"; |
| #134 | let pnl: number | undefined; |
| #135 | |
| #136 | if (!validation.ok) { |
| #137 | outcome = "rejected"; |
| #138 | log(`[tick ${tick}] REJECTED: ${validation.violation}`); |
| #139 | } else if (decision.action === "open") { |
| #140 | openPosition(state, decision.side, decision.size_lamports, currentPrice); |
| #141 | log(`[tick ${tick}] OPEN ${decision.side} ${decision.size_lamports} @ ${currentPrice}`); |
| #142 | } else if (decision.action === "close") { |
| #143 | pnl = closePosition(state, decision.position_id, currentPrice); |
| #144 | log(`[tick ${tick}] CLOSE ${decision.position_id} pnl=${pnl}`); |
| #145 | } else { |
| #146 | log(`[tick ${tick}] HOLD - ${decision.reason}`); |
| #147 | } |
| #148 | |
| #149 | if (state.consecutive_losses >= CONFIG.loss_killswitch_consecutive) { |
| #150 | const killEntry: TickEntry = { |
| #151 | tick, |
| #152 | now: now.toISOString(), |
| #153 | candles_last3: candles.slice(-3), |
| #154 | whale_activity: observed.whale_activity, |
| #155 | book_snapshot: { ...state.book }, |
| #156 | decision, |
| #157 | outcome: "killswitch", |
| #158 | event: `killswitch: ${state.consecutive_losses} consecutive losses`, |
| #159 | total_pnl_lamports: state.total_pnl_lamports, |
| #160 | consecutive_losses: state.consecutive_losses, |
| #161 | }; |
| #162 | appendTick(killEntry); |
| #163 | emit({ event: "killswitch", tick, consecutive_losses: state.consecutive_losses, goblin: GOBLIN_MODE }); |
| #164 | log(GOBLIN_MODE ? "GOBLIN KILLSWITCH - even goblins respect the laws" : "[ralph] KILLSWITCH - halting"); |
| #165 | process.exit(1); |
| #166 | } |
| #167 | |
| #168 | const entry: TickEntry = { |
| #169 | tick, |
| #170 | now: now.toISOString(), |
| #171 | candles_last3: candles.slice(-3), |
| #172 | whale_activity: observed.whale_activity, |
| #173 | book_snapshot: { |
| #174 | positions: state.book.positions, |
| #175 | cash_lamports: state.book.cash_lamports, |
| #176 | unrealised_pnl: Math.round(unrealisedPnl(state, currentPrice)), |
| #177 | }, |
| #178 | decision, |
| #179 | outcome, |
| #180 | violation: validation.violation, |
| #181 | pnl_lamports: pnl, |
| #182 | total_pnl_lamports: state.total_pnl_lamports, |
| #183 | consecutive_losses: state.consecutive_losses, |
| #184 | molt_note: moltNote(tick, state), |
| #185 | }; |
| #186 | appendTick(entry); |
| #187 | |
| #188 | emit({ |
| #189 | event: "tick", |
| #190 | tick, |
| #191 | now: now.toISOString(), |
| #192 | price: currentPrice, |
| #193 | whale_activity: observed.whale_activity, |
| #194 | decision, |
| #195 | outcome, |
| #196 | pnl, |
| #197 | total_pnl_lamports: state.total_pnl_lamports, |
| #198 | cash_lamports: state.book.cash_lamports, |
| #199 | positions: state.book.positions.length, |
| #200 | consecutive_losses: state.consecutive_losses, |
| #201 | molt_note: entry.molt_note, |
| #202 | goblin: GOBLIN_MODE, |
| #203 | }); |
| #204 | |
| #205 | commitJournal(tick); |
| #206 | if (SLEEP_MS > 0) await sleep(SLEEP_MS); |
| #207 | } |
| #208 | |
| #209 | const summary = { |
| #210 | event: "done", |
| #211 | ticks: TICKS, |
| #212 | total_pnl_lamports: state.total_pnl_lamports, |
| #213 | total_trades: state.total_trades, |
| #214 | final_cash_lamports: state.book.cash_lamports, |
| #215 | open_positions: state.book.positions.length, |
| #216 | consecutive_losses: state.consecutive_losses, |
| #217 | goblin: GOBLIN_MODE, |
| #218 | }; |
| #219 | emit(summary); |
| #220 | log(GOBLIN_MODE ? `\nGOBLIN DONE pnl=${state.total_pnl_lamports} trades=${state.total_trades}` : `\n[ralph] done pnl=${state.total_pnl_lamports} trades=${state.total_trades}`); |
| #221 | } |
| #222 | |
| #223 | runLoop().catch((err) => { |
| #224 | process.stderr.write(`[ralph] fatal: ${String(err)}\n`); |
| #225 | process.exit(1); |
| #226 | }); |
| #227 | |
| #228 |