# coinsori — AI reference (llms.txt) > Machine-oriented reference for AI assistants. Human docs: /docs > Generated from the single source of truth (utils/ctxApi.mjs). Do not hand-edit — run `node scripts/gen-llms.mjs`. ## What coinsori is coinsori (코인소리) is a global crypto community + market data + **non-custodial** trading platform. - Community: multilingual board, realtime chat, crypto news — server-side machine translation into ~50 languages. - Market data: multi-exchange live prices over WebSocket deltas (seq/epoch consistency), candles served from coinsori's own store, the cross-exchange premium (local vs global price gap) computed against the **real USD/KRW FX rate** (never KRW-USDT). - Trading: paper trading (server-simulated) and live trading through a **local agent** on the user's own machine/VPS. Strategy studio with backtesting; strategies are written as a single JavaScript function. ## Non-negotiable architecture invariants 1. **Non-custodial**: exchange API keys NEVER reach coinsori servers in plaintext. Signing and order submission happen only on the user's device (local agent). The server never initiates trades. 2. Remote agent control is **E2E encrypted** (ECDH P-256 + HKDF + AES-256-GCM). The server relays ciphertext only — it cannot read, create, or forge orders. TOFU fingerprints detect MITM. 3. API keys must have **no withdrawal permission** — the agent verifies and rejects keys that do. 4. User strategy code never runs on the server (browser Web Worker for backtests, local agent for live). 5. **Honesty rule**: unknown values are `null`, never fabricated. No FX rate → no conversion (blank, not a guess). Indicators return null until enough data exists. Volume is null when the feed can't know it. 6. No "guaranteed profit" language. coinsori provides tools, not investment advice. ## Strategy contract (backtest and live are IDENTICAL) Write ONE JavaScript function: ```js function onUpdate(ctx) { // called once per candle (backtest) / on each update (live) // return null, ONE order object, or an ARRAY of order objects } ``` Rules: - Pure function. No network, no imports, no async, no external data. Only ctx helpers below. - `qty` is always an amount of COIN, not cash. - Market order `{ side: 'buy'|'sell', qty }` walks the orderbook: slippage applies, large orders may partially fill, taker fee (`ctx.fees.taker`). Buys auto-shrink to available cash; sells cap at holdings. - Limit order `{ side, qty, type: 'limit', price }` rests until touched, fills at the limit price with maker fee. Add `postOnly: true` to reject a limit that would cross immediately. - `{ cancel: 'all' }` cancels all resting orders. - Return ONE order object **or an ARRAY of orders** — an array is processed in order within the same bar, e.g. `return [{ cancel:'all' }, { side:'sell', qty, type:'smart', trigger:{...} }]`. ★ If you re-install trigger orders every bar (ratchet stops etc.), ALWAYS lead with `{ cancel:'all' }`. Without it pending orders ACCUMULATE; when they fire together the total exceeds what you meant to sell (in futures this can flip your position — real case 2026-08: a paper strategy went −100% in all 4 walk-forward windows from accumulated stops). - Valid `type` values: omitted (= market), `'limit'`, `'smart'`. Anything else — including `type:'stop'` — is REJECTED with a warn log (2026-08; backtest and live identical). A stop is always `type:'smart'` + `trigger:{type:'stop', px}`, never a standalone type. - Always null-guard indicators (early candles). Persist your own variables in `ctx.state`. - Use logical slot names for `ctx.ref` ('hedge', 'global') — real symbols are bound at deploy time. - Quote currency differs by exchange (Upbit=KRW, Binance=USDT); normalize with `ctx.fx` yourself. ## Order object keys - `side` — 'buy' | 'sell' - `qty` — 수량(코인). 현금이 아니다 - `type` — 'limit'=지정가, 'smart'=작업 주문(시간에 걸쳐 집행 — 체이싱·분할·트리거). 기본은 시장가. smart 는 라이브·페이퍼에서 집행되고 백테스트는 시장가로 근사 - `price` — 지정가일 때의 가격 - `postOnly` — true 면 즉시 체결될 지정가는 거부(메이커만) - `chase` — smart: 호가 추격 { escalateMs?=20000, deadlineMs?=45000, bandTicks?, minRepegMs? } — 호가 조인(메이커)→1틱 전진→마감 시 크로스(테이커). 기본 켜짐, false 면 끔 - `slices` — smart: 분할 { n, everyMs } — 수량을 n조각으로 everyMs 간격 집행 - `trigger` — smart: 조건 대기 { type:'stop'|'trail', px?, offset? } — 조건이 닿기 전엔 주문이 안 나간다. 실데이터 백테스트도 봉 h/l 로 발동 판정(2026-08, 체결 라벨 stop/trail) - `hardStopMs` — smart: 이 시간이 지나면 무조건 종료(남은 수량 포기) - `cancel` — 'all' 이면 대기 주문 전체 취소 ## ctx API — complete list (nothing else exists) ### 상태 (state) - `ctx.candle` — 현재 봉 { t, o, h, l, c, v } — t 는 실데이터면 실제 봉 시각(라이브와 동일), 합성 시세만 봉 번호 - `ctx.price` — 현재가(종가) - `ctx.closes` — 현재 봉까지의 종가 배열(미래 없음) - `ctx.i` — 현재 봉 번호 - `ctx.position` — 보유 수량(코인). 선물(usdm)에선 부호 수량 — +롱/−숏 - `ctx.entryPx` — (선물) 평균 진입가 — 포지션 없으면/spot 이면 null - `ctx.liqPx` — (선물) 격리 청산가(해석해) — 포지션 없으면/spot 이면 null - `ctx.uPnl` — (선물) 미실현손익(quote) — spot 이면 null - `ctx.leverage` — (선물) 현재 레버리지 — spot 이면 null - `ctx.funding` — (선물) 직전 적용 펀딩 rate — 아직 없으면/spot 이면 null - `ctx.marginRatio` — (선물 P2) cross 유지마진율 = 유지마진÷계정평가(1 이상 = 청산권). cross 포지션 없으면/spot 이면 null - `ctx.marginMode` — (선물 P2) 현재 종목 마진 모드 'isolated'|'cross' — spot 이면 null - `ctx.setMarginMode(sym, 'isolated'|'cross')` — (선물 P2) 마진 모드 전환 — 그 심볼 포지션이 없을 때만(거래소 동일). 반환: 적용된 모드 | null. 기본 isolated - `ctx.cash` — 주문 가능 현금 - `ctx.state` — 호출 간 유지되는 내 변수 저장소 — ★ 실행 전체 공유(다종목이면 전 종목이 같은 객체). 종목별 값은 ctx.symState 에 - `ctx.fees` — 수수료율 { maker, taker } ### 지표 (indicator) - `ctx.sma(n)` — 단순이동평균. 데이터 부족이면 null - `ctx.ema(n)` — 지수이동평균. 부족이면 null - `ctx.rsi(n)` — RSI 0~100. 부족이면 null - `ctx.high(n)` — 최근 n봉 최고 종가 - `ctx.low(n)` — 최근 n봉 최저 종가 - `ctx.change(n)` — n봉 전 대비 변화율 — 소수(0.05 = +5%. markets()의 ch* 퍼센트 단위와 다름!) ### 오더북 (book) - `ctx.book()` — 호가 전체 { bids, asks } (좋은 가격 순) - `ctx.bid()` — 최우선 매수호가 - `ctx.ask()` — 최우선 매도호가 - `ctx.mid()` — 중간가 - `ctx.spread()` — 호가 차이(절대값) - `ctx.spreadPct()` — 호가 차이(중간가 대비 %) - `ctx.fillPrice(side, qty)` — 시장가 예상 체결 { avgPx, filled }. filled 2 → missing bars, never compare symbols on it; (2) tFrom/tTo differ from the comparison target → re-measure on the same window; (3) symsIdle present → suspect a symState scoping bug first; (4) syntheticBookUsed → book-based signals cannot be judged by backtest, say so; (5) trades == 0 or abnormally few → determine bug vs no-signal; (6) deploy caps < 10% of seed → strategy is distorted, warn the user (the server also returns configWarn for this); (7) run interval ≠ backtest interval → stop and say so immediately. - **A2. Never conclude from a single window.** Do not say "good / deploy it" from the latest window alone. Use at least 3 non-overlapping past windows (fixed `end`) before using the word "verified". If it fails them, lead with that fact. - **A3. Plain language.** In user-facing sentences avoid: MDD, walk-forward, Sharpe, slippage, overfitting, lookback, parameter. Rephrase: "how much it shrank at its worst", "whether it also worked in other past periods", "whether it was tuned to just that period". Numbers must carry meaning: not "+45%" but "10,000,000 KRW becoming 14,500,000 KRW". - **A4. Failures first, and bigger.** If 2 of 20 strategies passed, report "18 failed the bar; 2 remain" — not "2 successes". Mis-set expectations are the platform's biggest churn risk. - **A5. No certainty language.** Never "this earns money / is safe / is good". Instead: "it was positive in all 4 past periods tested; that does not guarantee the future." State at least once per result presentation that backtests are estimates. ## Units, pitfalls, and debugging (READ CAREFULLY — real AI mistakes happened here) - **candle.t is now real time (2026-08)**: in real-data backtests `ctx.candle.t` carries the actual bar epoch (same contract as live) — time-of-day / weekday strategies work identically in both. Only synthetic-market backtests (no real candles) fall back to the bar index. Epoch units follow the candle source (verify against `tFrom`). - **ctx.i semantics differ between backtest and live (2026-08, defect fix — run 42)**: in backtests `ctx.i` is the 0-based bar index. In live/paper it is a MONOTONIC bar-clock counter (epoch ÷ bar interval) — it advances by exactly 1 per bar and survives agent restarts, but its absolute value is large and unrelated to the backtest index. **Only DIFFERENCES of ctx.i are meaningful** (cooldowns `ctx.i - s.lastExitI`, holding periods, evaluation windows — these work identically in both). Never use absolute comparisons like `ctx.i === 10` outside backtests, and never index `closes[ctx.i]` in live (the closes window keeps only the last 500 bars — use `closes.at(-1)` / `ctx.price`). Before this fix live `ctx.i` froze at 499 once the window filled, silently disabling every cooldown — if your strategy ran on agent < 1.44.1, re-check time-based logic. - **ctx.state is RUN-scoped, not per-symbol (2026-08, defect found in all 34 strategies)**: in a multi-symbol run every symbol's call shares ONE `ctx.state` object. Storing per-symbol values flat (`state.stopPx`, `state.holding`) makes symbols overwrite each other — one symbol trades once and the whole run stalls. Use `ctx.symState` (auto-scoped to the current symbol, same object as `state.__sym[ctx.sym]`) for per-symbol values; keep `ctx.state` for run-wide values only. BAD: `ctx.state.stopPx = px` · GOOD: `ctx.symState.stopPx = px`. - **Universe size: use `ctx.syms`, not `ctx.markets().length` (2026-08)**: `markets()` only contains symbols whose data has arrived — on early calls that can be just one row, which broke per-symbol budgeting (budget = equity/1 = everything). `ctx.syms` lists the FULL assigned universe from the first call, in backtest and live alike. - **Idle-symbol hint (multi-symbol results)**: if some symbols traded 0 times while others traded, the result carries `symsIdle: [...]`. It cannot distinguish "no signal" from a state-scoping bug — treat it as a pointer to check `symState` usage first. - **exec.fillPct is a RATIO**: 0.5 = 50% fills, valid range 0 0) return { side: 'sell', qty: ctx.position }` every bar until position is 0 — a single sell may not close you. - **openOrders() shape**: array of `{ id, side, qty, price }` (resting limits only; market orders never rest). `{cancel:'all'}` cancels the whole run's resting orders — per-symbol cancel does not exist, so in scanner runs keep resting orders on one symbol at a time. - **Backtest window defaults to the latest N bars** — the anchor drifts between runs. For fixed periods and walk-forward validation pass `end` (dev API backtest jobs — see the dev loop section) and verify via `tFrom`/`tTo` in the result. The studio web backtest is still latest-N only. - **Fees**: backtest defaults are maker 0.05% / taker 0.1% (override via `market.fees`). Live/paper uses per-exchange estimates — read `ctx.fees` at runtime instead of hardcoding. - **ctx.ref()/ctx.fx() in dev-API backtest jobs return null** — jobs don't carry auxiliary series or FX yet (the studio's web backtest binds them). Strategies relying on ref/fx must be validated in the studio; guard with null checks so they don't crash in jobs. To validate premium/FX math OUTSIDE the strategy, use the public data endpoints: `GET /api/market/candles?ex=&sym=&interval=&limit=500[&before=]` (paginate with `before`) and `GET /api/market/fx?quote=KRW` (daily FX) — compute externally, then confirm the strategy itself in the studio. Also public (collection started 2026-08 — history grows from there, no backfill exists): `GET /api/market/oi?sym=&limit=500[&before=]` (Binance USDT-M open interest, 5m rows {ts, oi(COIN qty), oiUsd(USD notional|null)}; response carries `gapPct` = missing-row rate % against the 5m grid of the queried window (same fingerprint idea as candle gapPct; null when <2 rows). Exchange keeps only 30 days, so our DB is the long-term record) and `GET /api/market/liqs?sym=&limit=500[&before=]` (per-minute forced-liquidation aggregates {ts, longUsd, shortUsd, cnt}; realtime-only stream, gaps mean the collector was down — never interpolate). Both are raw material for crowding/positioning ideas (funding-filter lineage). In LIVE/PAPER runs the same data is available as `ctx.binanceOi()` / `ctx.binanceLiqs(n)` (agent ≥ v1.45; Binance-prefixed because the source is Binance USDT-M regardless of the run's exchange); in BACKTESTS both return null until history reaches walk-forward depth — a strategy using them must null-guard and cannot be validated by backtest yet (paper-validate instead). - **Multi-symbol fills index**: in multi-symbol results `fills[i][0]` is the index in the MERGED event sequence (all symbols, time-sorted) — NOT a uniform bar grid. Never convert it to a timestamp via `tFrom + idx × interval` (real case: "last signal −3992h in the future"). If you need fill times, measure with a single-symbol job. - **Unit mismatch trap**: `ctx.markets()` rows carry `ch1/ch5/ch60` in **PERCENT** (2 means +2%), but `ctx.change(n)` returns a **FRACTION** (0.02 means +2%). They differ by 100x — never compare one against a threshold written for the other. (A real strategy required "+200% in 1h" this way and traded zero times.) Ranking/sorting by markets() ch values is unit-safe; thresholds are not. - **Single-symbol runs**: in a single-symbol run or backtest, `ctx.markets()` contains exactly ONE row (that symbol) — it is never empty while the price is known. Do not early-return on `rows.length < 2` unless you intend to skip single-symbol mode. - **Warmup**: every indicator returns null until n candles exist. If the backtest period is shorter than your longest lookback, the strategy never fires. Ensure period ≫ max(n). In live/paper runs (2026-08) **ALL watched symbols are backfilled** from coinsori's candle store at start, in the run's own bar interval (up to 500 bars each) — a 4h/55-lookback strategy starts with warm indicators instead of waiting 9 days. The run's bar interval is chosen at deploy time and should MATCH the backtest interval (the live ring buffer builds bars at that interval; on a mismatch the same strategy sees different signals). Startup logs report per-symbol backfilled bars and, when short, the estimated warmup-complete time — still null-guard indicators. - **Thin synthetic book (backtest)**: the orderbook is derived from candle volatility, so large qty can partially fill or slip hard. Check `ctx.fillPrice(side, qty)` before sizing big orders. - **Backtest data shortfall**: candles load in pages (up to 500/request, paginated to the requested count, max 5000). If the exchange has less history than requested, the UI shows "requested X bars → only Y loaded" — treat Y as the real sample size. A zero-trade result on a short sample often means the move you filter for simply never happened in that window, not that the strategy is broken. - **Zero-trades debugging pattern**: count every rejection reason in `ctx.state` and log a summary periodically, e.g. ```js const s = ctx.state; s.blocked = s.blocked || {} // ... s.blocked.rsi = (s.blocked.rsi||0) + 1 ... if (ctx.i % 500 === 0) ctx.log('blocked', JSON.stringify(s.blocked)) ``` This shows which filter blocks entries instead of guessing. - `qty` is coin amount; to spend all cash at market use `qty: ctx.cash / ctx.price` (fees auto-shrink buys). - Volume (`ctx.vol`/`volumes`/`avgVol`) can be **null** (unknown ≠ 0) — a volume filter without a null check silently inverts on exchanges where volume is unavailable. ## Execution model - Backtest: runs in a browser Web Worker on coinsori candle history; the orderbook is synthesized from candle volatility (deterministic approximation) — treat slippage as an estimate. **Model change (2026-08):** spread is now normalized to per-minute volatility (interval-aware) — long-interval bars no longer produce absurdly wide books (market orders used to cost ~5% round trip on 1h bars; that was a model artifact, not reality). Results before this change are not comparable. - Live: the local agent subscribes to exchange feeds, calls onUpdate with the same ctx, enforces user-set guardrails (order caps, loss kill-switch with fee-aware P&L ledger), then signs and submits orders directly to the exchange. Kill-switch release is a monotonic timestamp the agent compares — the server cannot command trading on. - Scanner runs: one deployment may watch many symbols; ctx.sym tells which symbol this call is for and ctx.markets() gives compact rows for all watched symbols (never full candle arrays for other symbols). - Cluster (planned, see TRADING-CLUSTER.md): multiple agents partition the symbol set via server-issued leases (rendezvous hashing, epoch + TTL). Ownership is exclusive per symbol; expiry stops trading on that symbol (fail-safe). The server still cannot create orders. ## Automated dev loop (dev API, elite plan) — for AI agents developing strategies - **Session start protocol (new chat / new token)**: call `GET /api/dev/context` FIRST and treat the SERVER as the source of truth — not your own memory of previous sessions. Then reconcile: - Your memory references a strategy the server does NOT list → the token is almost certainly `scope=own` (isolated: it only sees strategies it created — an empty list does NOT mean the account is empty). Say so explicitly and offer the user exactly two paths: (a) issue a token with "All my strategies" scope to CONTINUE the existing strategy (recommended when resuming work — notes/backtest history stay attached), or (b) start a FRESH strategy under this isolated token (new project). NEVER reconstruct the old strategy from memory — the rebuilt code will differ in details and every comparison against its previous backtests becomes invalid. - You have no memory of this account → just proceed: create a strategy and start the loop (announce "scoped token — starting a new strategy" so the user isn't surprised by an empty list). - With a personal access token (`csd_...`, Bearer auth) you can iterate without the human copy-pasting: `GET /api/dev/context` (start here) → read code → `PUT .../code` (always pass `note`: what/why) → `POST /api/dev/strategies/:id/backtests` {ex, sym, interval, bars<=100000, cash, market?, end?, syms?, exec?, guard?, marketType?, futures?, funding?} (`market.book`/`market.fees` numeric overrides pass through — e.g. `{book:{liqNotional:5e8}}`. `end` = epoch sec/ms or ISO, exclusive — backtests the `bars` bars BEFORE that time. `end` must be in the PAST — today/future values are rejected with 400. Omit `end` for the latest window, but note the anchor then drifts per run — ALWAYS fix `end` for comparisons. Unknown body keys are rejected with 400 (never silently ignored). The result echoes `tFrom`/`tTo` (epoch sec of the first/last bar actually used) and `gapPct` (missing-bar rate % — when high, the same bar count spans a LONGER period; a dataWarn fires above 5%. Never compare symbols whose gapPct or spans differ materially). Candle API errors are typed: **404 = no data for that symbol/exchange (retrying is pointless — pick another), 5xx = transient (retry once)**. Always verify the period from the result itself; without `end` the latest-N anchor drifts between runs, so fix `end` for any comparison. Use it for walk-forward validation: tune on one period, then verify the SAME params on disjoint earlier periods. If results only hold on the tuning period, the strategy is overfit.) → poll - **Multi-symbol backtests (2026-08)**: pass `syms: ["BTC","ETH","SOL"]` (2–10, replaces `sym`) to run ONE shared wallet across all symbols with bars merged in true time order — allocation contention ("first signal drains the cash") is now reproducible in backtests. Same live contract: `ctx.sym` is the symbol of the current bar, `ctx.markets()` lists all, orders route via `{sym}` (outside the universe → rejected + warn log). Result adds `syms`, `perSym` {trades, realized, position, lastPx}, portfolio-level return/mdd, and fills carry the symbol as the 6th element. `bars` is capped at 100000/len(syms). refs/fx cannot be combined with `syms` yet (400). - **Execution model (2026-08)**: `exec: {delayBars?: 0–10, fillPct?: 00 means longs pay. Result echoes `fundingPaid` (+paid/−received), `fundingApplied`, and — CRITICAL — `fundingMissing` (ALWAYS boolean on usdm results): `true` = the run effectively had NO funding (no data, or the series never overlapped the run window — a warn log names the cause); `false` positively asserts funding WAS applied. Never compare or report a perp result without checking this flag AND `spec.fundingSource` — checking only result fields caused a real misreading (a comparison flipped when server auto-attach kicked in between measurements). **Funding is AUTO-ATTACHED (P4, 2026-08)**: for `marketType:'usdm'` jobs with no injected `funding`, the server attaches real Binance funding history covering the run window and marks `spec.fundingSource: 'server'` on the job (your injected series takes precedence and is marked `'injected'`). If the store has no coverage for that symbol/period nothing is attached and the result honestly shows `fundingMissing: true`. Read `spec.fundingSource` from the job object to know which funding a result used — never assume. - **Futures P2 (2026-08) — limit/trigger orders, cross margin, multi-symbol**: · Limit/stop/trail orders now work in usdm with the same order shapes as spot, plus `reduceOnly` (clamped at fill time — never flips the position). Futures limit orders reserve NOTHING at placement; margin is checked at fill (insufficient → scaled down + warn log, same rule as market orders). Resting fills pay maker fee, triggers pay taker. · `futures.marginMode: 'isolated'(default) | 'cross'` — the default stays isolated for spec continuity with P1 results. Per-symbol switch via `ctx.setMarginMode(sym, mode)` (only while that symbol has no position, like real exchanges). · Cross semantics: no per-symbol liqPx (`ctx.liqPx` is null for cross — watch `ctx.marginRatio` = maintenance/equity, liquidation at >= 1). When account equity drops to the total maintenance margin, ALL cross positions are force-closed at adverse bar extremes with a 2x taker liquidation fee; the wallet absorbs losses and CAN go negative (no insurance fund modeled — deliberately worse than real exchanges). · Multi-symbol futures: `syms` works with `marketType:'usdm'` — shared wallet, per-symbol positions/margin modes. Funding must be a PER-SYMBOL map `{SYM: {times, rates}}` (a single series for all symbols would be fabricated data — rejected). `perSym` gains `entryPx`, `liquidations`, `fundingPaid`, `marginMode` so you can see which symbol liquidated or paid. - **COIN-M inverse (FUTURES P3, 2026-08)**: `marketType:'coinm'` + `ex:'binancecoinm'` — backtests AND paper deploys (`POST /api/dev/runs`; dev API is paper-only as always; live exists via web UI). SINGLE-symbol only (each symbol settles in its own coin — a shared wallet would mix coins; run one deploy per symbol, which matches the exchange: Binance has no cross-coin margin sharing either). Paper `paperSeed` is in COIN units (10 = 10 BTC — a USD-sized default would be absurd); guardrail `maxLossQuote` is in coin too, while `maxOrderNotional`/`maxPosNotional` stay USD. Units flip: `qty` = CONTRACTS (1 contract = `futures.ctrSize` USD, default 100 for BTC / 10 otherwise — the binance rule; result echoes `ctrSize`), while `cash`/margin/PnL/fees are in the COIN (e.g. BTC), so `cash:10` means 10 BTC, and `return`/`final` are coin-denominated. PnL = qty×ctrSize×(1/entry − 1/exit) (longs still profit when price rises). `ctx.liqPx` uses the inverse analytic solution; a 1x short is fully hedged in coin terms → `liqPx` null and it can never liquidate (this is correct, not a bug). Guardrail notionals (maxOrderNotional/maxPosNotional) are in USD (|contracts|×ctrSize). Funding: injected series only — the server does NOT auto-attach (its store holds USDT-M rates, which would be wrong data for coinm); without injection `fundingMissing:true` as usual. Funding pay = qty×ctrSize/price×rate, settled in coin. - **Futures PAPER runs (P6, 2026-08)**: `POST /api/dev/runs` accepts `marketType:'usdm'` + `futures:{leverage,marginMode}` — the dev API is ALWAYS paper (live futures exist as of P7 but only through the web UI; dev tokens can never initiate live orders — non-negotiable), custom strategies only, and `ex` MUST be `'binanceusdm'` (futures prices only — spot prices for a perp rehearsal would be wrong data). ctx carries the same futures fields as backtests (signed position, entryPx, liqPx, marginRatio, marginMode); liquidations sweep every tick. Funding settles in paper (P6b, 2026-08, agent >= 1.40): every 8h boundary (00/08/16 UTC) the agent fetches the SETTLED binance rate and applies `pay = qty×px×rate` to the wallet (same formula as backtests; sign as-is — longs pay positive rates, shorts receive). `ctx.funding` is the last settled rate, null before the first settlement (never fabricated). Missed boundaries (agent down) are back-applied up to 3, at current price (logged as approximation). Positions opened AFTER a boundary are never charged retroactively. `reduceOnly` now applies to `smart` orders too. Remaining paper-vs-backtest difference (deliberate, honest): insufficient-margin entries are REJECTED rather than scaled down. NOT yet supported (explicit 400/log, never silently ignored): live futures (P7), COIN-M (P3), hedge mode (v2), margin add/remove on isolated positions (v2). - **Jobs interrupted by an agent restart/update auto-recover — do NOT resubmit**: on a graceful shutdown (update/restart) the agent RELEASES running jobs back to the queue instantly (0s wait); on a crash, the heartbeat stops and the job is reclaimed within ~90s. The job object shows `stale: true` while a dead execution awaits reclaim ("will re-run — just wait"). Resubmitting duplicates the work and burns the daily quota. - **After an agent update, do NOT restart finished batches blindly — check `engineHash`**: every job result carries `engineHash` (backtest-engine fingerprint) and `agentVersion`. Results with the SAME engineHash are comparable regardless of when they ran — an agent update that doesn't change the engine leaves your previous batch fully valid. Only when engineHash differs must the jobs you want to compare be re-run (mixing engineHash values in one comparison is invalid). `GET /api/dev/backtests/:id` until status done|failed → analyze result → repeat. Leave findings as notes. - **Don't over-anchor on the current code.** Before tuning parameters, ask whether the approach itself is right — parameter-tweaking a bad idea converges to a well-tuned bad idea. Keep separate draft strategies (`POST /api/dev/strategies`) to A/B genuinely different approaches (trend / mean-reversion / scanner momentum) instead of endlessly mutating one strategy. - **Reference other strategies.** Public strategies are fully open by design (open-code policy): `GET /api/studio/strategies?sort=perf` (ranking), code at `GET /api/studio/strategies/:id`. Reading how others structure entries/exits is encouraged — credit borrowed ideas in your notes. - **Strategy bookkeeping (multi-day)**: `PATCH /api/dev/strategies/:id/meta` sets `status` (draft/testing/adopted/rejected/archived) and `tags` (e.g. 'axis:sizing,breakout'). The strategy list returns both — the next session reads the list, not 30 notes, to know where things stand. Update status whenever a verdict is reached. - **Cross-comparison**: `GET /api/dev/backtests?limit=100` returns recent jobs across strategies with return/mdd/trades/gapPct/tFrom/tTo in one call — build comparison tables from this. - **Paper vs backtest**: `GET /api/dev/runs/:id/fills` returns fills + wallet for a run — auto-compare what the backtest predicted against what paper actually filled. - **Paper deploys via dev API (2026-08, PAPER-ONLY by user decision)**: `GET /api/dev/runs` (all runs), `POST /api/dev/runs` {strategyId, ex, syms|sym, interval?, paperSeed?, caps?, agentId?, allowDup?} creates a PAPER run (agentId omitted → most recent online agent). `PATCH /api/dev/runs/:id` {status: active|paused} and `DELETE /api/dev/runs/:id` work on paper runs only — live runs return 403 (live deploys/controls are human-only, web UI). Deploy pins the code AT DEPLOY TIME: after editing a strategy, redeploy (delete + POST) or the old code keeps running. Duplicate deploys of the same strategy/agent/symbols are rejected with 409 unless allowDup:true (orders would double). Deploy warnings ride INSIDE the run object — `{ run: { id, ..., configWarn?: [...] } }`, NOT top-level (a real defect report was filed from looking at the top level only). configWarn lists contradictory settings that make numbers look normal while distorting the strategy (paper caps < 10% of seed; coinm seed > 1000 coins; 2+ symbols without a `symState` trace). Warnings, not blocks — read them and fix the deploy. - Every note stores a code snapshot — the notes timeline IS the version history. Read any version with `GET /api/dev/notes/:id/code`; restoring is available to the human in the studio (and you via re-PUT). - **Token scope**: scope=own tokens (the default) see only strategies created with that token — an empty strategy list does NOT mean the account is empty. Create your own and proceed. - Empty account? `POST /api/dev/strategies` {name, code} creates a draft custom strategy — don't stall waiting for the human. Drafts stay private; **deploying to live still happens only in the web UI, by the human**. - Backtest jobs run on the **user's own agent** (non-custodial — the server never executes user code). If the response says agentOnline=false the job waits until the agent connects; tell the user honestly. - **Backtest jobs are single-symbol** (one `sym` per job). Multi-symbol scanner backtesting does not exist yet — ctx.markets() contains only that symbol, so a scanner's "picking among symbols" part is NOT validated. To compare symbols, submit one job per symbol (mind the concurrency limit). - Set `cash` to market scale (KRW markets ~10000000, USDT ~10000) — wrong scale makes qty look broken. - **Work on your own strategy.** Unless the user pointed you at a specific one, create yours with `POST /api/dev/strategies` and develop there — existing strategies are the user's assets. - **Job submission pacing**: leave ~2s between job submissions — rapid-fire submissions can trip candle-API rate limits (502s that look like outages). Parallelism comes from the concurrency slots, not from submission speed. ## Idea space — axes to explore (pick an UNEXPLORED axis, not another variant) A strategy idea is a combination of these axes. When one axis stops yielding, that is NOT "out of ideas" — it is "that axis is explored". Move to an unexplored axis. 1. Signal source: price / volume / bar shape (o,h,l,c) / orderbook·liquidity / cross-symbol relations 2. Signal horizon: 1 bar ~ hundreds of bars (short = weak persistence, long = few trades — find balance) 3. Entry structure: single signal / multi-signal voting / regime switching / added filters 4. Entry price: market now / resting limit on pullback / staged entries 5. Bet sizing: all-in / fixed-risk / performance-scaled / pyramiding 6. Exit style: channel exit / trigger stop / trailing / time-based / partial take-profit 7. Portfolio: single strategy / parallel strategies / core holding + overlay 8. Target: symbol set, bar interval (changing WHERE it runs, not the strategy) Rule of thumb: axes 1–3 (signal) have little room left once a good strategy exists; axes 4–7 (money & execution) usually hold the remaining edge. ★ Axis limits: orderbook/liquidity signals CANNOT be judged by backtest (the book is synthetic) — validate those on paper from the start. ★ Re-arming stops WITHOUT reduceOnly (real bug, 2026-08): if you re-place a stop every time your level moves and reduceOnly is unavailable (e.g. paper smart orders), the old stops STAY armed — they accumulate, fire together, and can flip you short past your position (-100% across 4 windows, measured). Always `{cancel:'all'}` before re-arming, or track and cancel the previous order id. ★ Futures stop placement (measured, 2026-08): at high leverage a PERCENTAGE safety margin collapses — at 20x a "30% margin" stop sat only 2.1% above the liquidation price (one bar's range). Size the stop-to-liqPx gap in ABSOLUTE terms (e.g. N× recent bar range), not as a percentage of margin, and always verify `ctx.liqPx` (isolated) or `ctx.marginRatio` (cross) at entry time. ## Multi-day development — pacing rules - The dev loop is a MULTI-DAY effort. Do not try to exhaust the idea space in one session. - Spend ~20–30% of the daily backtest quota per session (e.g. 200–300 of 1000). Keep the rest for fair-comparison re-measurements and data verification. - End every session with a **backlog note**: axes tried, axes remaining, next 3 candidates. The next session starts by reading that note. - "Nothing left to improve" is close to a forbidden conclusion. Instead choose one of: (a) move to an unexplored axis, (b) wait for paper results as new material, (c) change the target (symbols/interval). - Diminishing returns is not failure — it is the completion signal for that axis. Record it as such. ## Comparison hygiene — mistakes that flip conclusions (all happened for real) Before comparing anything, check: 1. **tFrom/tTo** — did both strategies see the SAME period? Different warmups shift the window. When comparing against a long-warmup strategy, re-measure the baseline on the same bars. (Real case: a regime-switch strategy looked 3x better — re-measured on the same window, it lost.) 2. **gapPct** — with different missing-bar rates, equal bar counts mean different periods. (Real case: 43% gaps made 2000 bars span 586 days instead of 333.) 3. **Fix `end`** — without it the latest-N anchor drifts and results are not reproducible. 4. **Never judge on the current period alone** — recent winners often collapse in past regimes. (Real case: +313% on the current period, −25% on the one before.) 5. Some axes are untestable in backtests (synthetic book) — see Idea space above; paper-validate. 6. **Futures: match `spec.fundingSource`** ('server'|'injected') on BOTH sides before comparing — server funding auto-attach changes returns for the identical spec (real case: −27.02% → −27.60% was reported as an engine regression; it was funding attach turning on). The flag is echoed in the job's `spec`, not in `result`. To exclude funding entirely, inject `funding:{times:[],rates:[0]}`. - Limits: concurrent jobs per plan (elite: 3 — submit several symbols/intervals in parallel and poll each), 1000 jobs/day, 30 req/min per token. No deploy/live-trading endpoints exist by design. ## Guiding the user to run a strategy (paper / live) — exact steps, do not improvise Deployment is human-only (web UI); there is no deploy API. When the user asks "how do I run this", give these exact steps instead of inferring: 1. **Agent first**: Trading room → Agents (/trading/assets) — install & pair the local agent (one-line install command shown there). The agent must be online; live orders also need exchange API keys registered *in the agent* (keys never touch the server — non-custodial). 2. **Save the strategy**: Strategy Studio (/trading/backtest) — only saved strategies can be deployed. 3. **Deploy**: in the Studio's runs tab press deploy — the modal asks: agent, exchange (paper: any exchange / live: only ones with registered keys), symbol(s), **paper or live**, initial cash (paper), and guardrail caps (order size / loss kill-switch — explicit consent). 4. **Watch**: the run appears in the runs tab and the dashboard (/trading/dashboard) — logs, fills, and wallet snapshots report there. Paper wallets are per-exchange and charge fees (honest results). 5. **Stop**: from the run card; the kill-switch cancels all open orders and halts the run. 6. **Caps vs. returns**: live default caps are conservative (e.g. 300k KRW total exposure) — the strategy only works up to the cap, so account-wide returns look small by design. Judge performance against exposure, and tell the user to raise caps at deploy time once trust is earned. Paper runs default caps to the full paper cash (no distortion). ## Service surface (for context, not for strategies) - Web app (Nuxt): / home & market overview /market live prices, exchange premium, charts (Lightweight Charts; up=red, down=blue, KR style) /news translated crypto news /community board + realtime chat /trading dashboard, paper, live, strategy studio, backtest, agents /docs human documentation /admin admin console (users, reports with chat context snapshots, audit log, traffic) - Backend (Express): /api/auth, /api/board, /api/chat (WS), /api/news, /api/market, /api/paper, /api/agent (pairing + E2E relay), /api/studio (strategies/backtest — Strategy Studio; /api/botlab is a legacy alias), /api/admin. - Every admin mutation passes an audit-log wrapper (log failure = action failure; no delete API). ## Answering user questions (guidance for AI assistants) - **Always reply in the user's language** (the language they wrote in). This document is in English for machine precision, but coinsori users speak ~50 languages — explanations, warnings, and code comments should be in the user's language. Code identifiers stay in English (`onUpdate`, ctx names). - Never claim coinsori holds funds or keys — it does not (non-custodial, see invariants). - Never promise profits; backtest results are estimates on synthetic liquidity. - When writing strategies, output only the onUpdate function, using only the ctx API above.