chore: roll up #95 #96 #100 #101 #102 stack into dev#103
Merged
fray-cloud merged 8 commits intodevfrom May 2, 2026
Merged
Conversation
…, testnet/mainnet network PR2 of 3 in the LLM-driven Binance Futures pivot. Lays the futures execution foundation that PR3 will drive from a Claude-generated trade signal. What's reshaped: - OrderRequest/OrderResult/Order: side is now 'long'|'short' (the user-facing position direction; adapter translates internally to BUY/SELL). Add leverage, marginType, takeProfitPrice, stopLossPrice on request, plus entryPrice, liquidationPrice, tpOrderId, slOrderId on result. New OrderStatus, PositionSide, MarginType, Position, SymbolFilter exports. - ExchangeCredentials gains optional `network: 'mainnet'|'testnet'` so the adapter can pick fapi.binance.com vs testnet.binancefuture.com per call. - ExchangeKey schema: `network` column (default 'mainnet') and unique constraint widened to (userId, exchange, network) so a user can hold one key per network. - Order schema: futures fields (leverage, marginType, positionSide, entryPrice, liquidationPrice, takeProfitPrice, stopLossPrice, tpOrderId, slOrderId, realizedPnl, closedAt). Init migration regenerated. What's new: - BinanceRest rewrite against fapi.binance.com (and testnet.binancefuture.com). Reuses the existing HMAC signing path. Implements setLeverage, setMarginType (-4046 idempotent), setPositionMode (-4059 idempotent), getPosition, closePosition (reduceOnly market), placeStopLoss/placeTakeProfit (STOP_MARKET / TAKE_PROFIT_MARKET with closePosition=true and workingType=MARK_PRICE), getSymbolFilter (LOT_SIZE / PRICE_FILTER / MIN_NOTIONAL via cached exchangeInfo). - Worker saga: ConfigureFuturesAccountStep (one-way mode + margin type + leverage, all idempotent) runs before PlaceOrderStep; AttachTpSlStep runs after fill and inline-compensates by force-closing the position if either TP or SL placement fails — a naked position is the worst-case outcome. UpdateDbStep persists futures fields back to the Order row. - Api-server CreateOrderDto picks up leverage (1-20 clamped), marginType, takeProfitPrice, stopLossPrice. mode is locked to 'real' (paper retired in PR1 — testnet replaces it via ExchangeKey.network). - ClaudeToken and LlmDecisionLog Prisma models added now (unused until PR3) so the next migration is purely additive — keeps PR3 small. - Dev-only POST /debug/futures-test endpoint: takes an exchangeKeyId + symbol/side/quantity/leverage/tp/sl, calls the adapter directly (skips Kafka and the full saga), returns entry result + tp/sl orderIds + the resulting Position. Gated by NODE_ENV !== 'production'. Verified: - pnpm build green across all 9 workspace packages - Prisma migrate dev applied cleanly against fresh volume - docker compose dev: postgres/redis/kafka healthy, api-server /api/health 200, worker-service running, web /markets 200 Plan: PR3 will install Claude Code CLI in the worker image, add ClaudeToken storage + /settings/claude UI, build the LLM trade form, and wire the Claude-driven signal flow on top of this saga. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR3 of 3 in the LLM-driven Binance Futures pivot. Wires user input → Claude
signal → futures execution end-to-end.
Architecture decision: api-server hosts the Claude CLI subprocess for the
synchronous /signal call. The plan's worker-hosted variant would need Kafka
request-reply for sync HTTP, which is a heavier pattern than warranted for a
single-user app. Token decryption is contained to the LLM call site, then
released. Real-trade execution still goes through the existing worker saga.
What's added:
- Claude Code CLI installed in `coin-base` (used by api-server signal flow
and by future worker tooling). `claude --version` runs at base build.
- ClaudeTokens module: AES-256-GCM encrypted storage of per-user OAuth
tokens (`POST/GET/DELETE /claude-tokens`). Reuses existing encryption
helpers and JWT auth.
- `/settings/claude` UI: paste token, status badge, replace, delete; links
to `claude setup-token` instructions.
- LlmCliService (api-server): pure cli-runner spawn helper + queue=1
service with 30s timeout, 1 retry, strict JSON parsing, and TP/SL
geometry validation (LONG: sl<entry<tp; SHORT: tp<entry<sl). System
prompt at `src/llm/prompts/trading-system.md` ships via Nest assets.
- LlmTrades module: two-step flow.
- `POST /llm-trades/signal` — fetch candles via fapi public, call Claude,
persist `LlmDecisionLog`, return signal+TP/SL+reasoning+entryPrice.
- `POST /llm-trades/execute` — derive base-asset quantity from
bet/leverage/entry, create Order row, publish OrderRequestedEvent on
the existing topic. Picks testnet ExchangeKey by default if user has
one.
- `/llm-trade` UI: 5 inputs (symbol top-10 / interval / candle count /
leverage / bet USDT), Get Signal, response panel with TP/SL override,
confirm dialog, execute. Real-time results piggyback on existing
`order:updated` WS.
- Worker RiskGuardService: seven guards run before any real-mode order:
KILL_SWITCH_REAL_TRADING, ENABLE_REAL_MAINNET, MAX_LEVERAGE (default 20),
LLM_COOLDOWN_SECONDS per-user (Redis SETNX, default 30s),
MAX_OPEN_POSITIONS_PER_USER (default 1), DAILY_LOSS_LIMIT_USDT
(mainnet only, default 50), MAX_BET_PCT of available margin (mainnet
only, default 10%). Mainnet-specific guards are no-ops on testnet.
Hooked into `OrdersService.handleOrderRequested` so all real orders
(LLM or otherwise) clear the gate.
Verified:
- pnpm build green across all 9 workspace packages
- coin-base rebuilt; `claude --version` succeeds inside both api-server and
worker containers
- docker compose dev: postgres/redis/kafka healthy, api-server /api/health
200, web /llm-trade and /settings/claude render
Manual end-to-end smoke (user does once a Claude token is registered):
1. /settings/claude → paste OAuth token from `claude setup-token`
2. /settings → register a Binance Futures **testnet** API key
3. /llm-trade → fill 5 inputs, Get Signal, confirm, Execute
4. Verify position appears on testnet.binancefuture.com with TP/SL attached
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stack of debugging fixes discovered while running PR3 against a real Binance
Futures account. Each one was a separate dead-end before the next was visible,
so they're bundled here as one commit that makes the feature work.
1. Encryption master key — `.env.dev` shipped with `ENCRYPTION_MASTER_KEY=`
empty, so AES-256-GCM blew up with "Invalid key length" the moment the
first ClaudeToken was saved. Confirmed unrelated to this PR but blocked
smoke testing; the value is now copied from `.env`.
2. Network awareness on the read path — `GetBalancesHandler` and
`GetOpenOrdersHandler` decrypted the ExchangeKey but never read
`key.network`, so testnet credentials were always sent at the mainnet
base URL and got -2015. Both handlers now thread `network` into
`ExchangeCredentials`.
3. Settings UI for testnet vs mainnet — `/settings` (Accounts) had no way
to choose network, so users could only register mainnet keys. Added a
network toggle (defaulting to **testnet** for safety) and updated the
`createExchangeKey` API client to accept it.
4. System prompt assets — `nest-cli.json` `assets` config didn't
actually copy `trading-system.md` into `dist/llm/prompts/` under
`nest start --watch`, so the CLI runner failed with "no system prompt
file". Inlined the prompt into a TS module (`prompts/trading-system.ts`),
reverted the assets config, and removed the .md file so there's a single
source of truth.
5. Subprocess hygiene for `claude -p` — the spawned CLI inherited an open
stdin pipe and stalled 3s waiting on it before exiting 1. Switched to
`stdio: ['ignore', 'pipe', 'pipe']` so the CLI sees stdin closed
immediately. Also explicitly delete `ANTHROPIC_API_KEY` /
`ANTHROPIC_AUTH_TOKEN` from the spawn env so the user OAuth token wins.
6. `--bare` flag — `--bare` skips per-user config but, in this CLI version,
it also disables `CLAUDE_CODE_OAUTH_TOKEN` env auth and the CLI returns
"Not logged in." Dropped `--bare`; we still pass `--tools ""` to keep
the run side-effect free.
7. Lot-size precision — quantity was computed as
`(bet × leverage) / entry`.toFixed(6), which Binance rejected with -1111
on BTCUSDT (stepSize 0.001). LlmTradesService now fetches
`getSymbolFilter`, floors the raw quantity to the LOT_SIZE step, and
returns a clear error if the snapped notional is below MIN_NOTIONAL.
8. Conditional orders moved to algoOrder — Binance migrated TP/SL types
to a new endpoint on 2025-11-06; `/fapi/v1/order` now returns -4120
"use Algo Order API endpoints instead" for STOP_MARKET /
TAKE_PROFIT_MARKET regardless of params or account region (verified by
freqtrade issue #12610 + the change-log entry). Switched
`placeStopLoss`/`placeTakeProfit` to `POST /fapi/v1/algoOrder` with the
new schema:
- required `algoType: 'CONDITIONAL'`
- `stopPrice` renamed to `triggerPrice`
- response carries `algoId` instead of `orderId`
The saga's `AttachTpSlStep` is back to placing real exchange-side TP/SL
(no client-side watcher needed) and on attach failure compensates by
force-closing the position.
End-to-end smoke test against Binance Futures testnet now succeeds:
entry MARKET fills, then both TP and SL are attached as conditional algo
orders with `algoStatus: NEW` and survive the saga to UpdateDb +
PublishResult.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/[id], dashboard rebuild (#97) Resolves the four day-to-day operational gaps identified in #97 so that the post-PR3 LLM trading flow is actually usable. Backend - Portfolio: replace stale paper/real/all mode with testnet/mainnet/all driven by ExchangeKey.network; returns byNetwork breakdown so the UI can show split totals in one call. - Orders: GET /orders/:id now hydrates the row with the joined LLM decision, live mark price, and unrealized PnL; new POST /orders/:id/close publishes a Kafka close event consumed by a worker saga that calls reduceOnly MARKET closePosition. - Worker close saga also reconciles when the position is already gone on the exchange (TP/SL fired or liquidation): pre-checks getPosition and treats -2022/-4046/-2023/-4045 as "position gone" so the DB still flips to closed. - LLM trades: GET /llm-trades/decisions cursor-paginated history with order outcome joined. - Dashboard: new /dashboard/summary aggregate (today/week PnL split by network, open positions with live mark, last 5 LLM decisions) — single round-trip. - Activity: order item link now points to /orders/${id} (was the deleted /orders index route). Frontend - BaseCurrencyToggle pill in the global nav bar (KRW⇄USD, localStorage). - New formatCurrency helper returning {main, sub} so every price renderer can show primary + alt currency without bespoke math. - /portfolio: testnet/mainnet/all toggle, 모의/실거래 split totals, network badges on assets. - /orders/[id]: candle chart with entry/TP/SL price lines, PnL panel, manual Close Position button (gated to real-mode + open), LLM reasoning card. - /dashboard: PnL cards (today/week × testnet/mainnet), active positions table with one-click close, last 5 LLM decisions with outcome badges. Tests - 16 api-server + 1 worker + 7 web suites green. - New: portfolio network filter, close-order kafka emit, activity link, order detail payload, close-position-saga reconcile paths (TP/SL gone, -2022 rejection, idempotency). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inance (#98) PR3 attaches TP/SL via algoOrder, but Binance never pushes fills back to us (no User Data Stream subscription). When TP or SL fires, the position is gone on the exchange while our DB stays at status='filled', closedAt=null — effectively a permanent ghost open position. Manual close (#97) was the only recovery path. This adds a worker-side polling reconciler that closes the loop without introducing a WebSocket dependency. Worker - New PositionReconcilerService runs every 30s (RECONCILE_INTERVAL_MS env override). Queries DB for status='filled' AND closedAt IS NULL AND mode='real' orders and reconciles each. - Per-order reconcile is extracted into a deep, dep-injected reconcileOrder() module so the algorithm is testable in isolation from Kafka/Redis/Prisma wiring. - Algorithm: getPosition() to detect vanished positions, then getIncome() windowed since order.createdAt to compute authoritative realizedPnl from REALIZED_PNL + COMMISSION + FUNDING_FEE rows. INSURANCE_CLEAR rows mean liquidation. With both TP and SL registered, sign of realizedPnl decides take_profit vs stop_loss; otherwise manual_on_exchange. - Race-safe DB write: updateMany with closedAt=null guard so a concurrent manual close can't double-emit notifications. - Auth-failure cooldown: 3 consecutive auth errors per exchange key → Redis 1h cooldown skip, so one bad key doesn't loop forever. - close-position-saga now sets closeReason on its own writes ('manual' for the happy path, 'manual_on_exchange' for already-gone reconciliation). Adapter / types - New BinanceRest.getIncome() backed by /fapi/v1/income with full IncomeType union and IncomeRecord shape exported from @coin/types. - IExchangeRest gains the matching method. Schema - Order.closeReason: String? — nullable, no default (historical rows untouched). - Manual SQL migration only (DB not reachable from this env). API - OrderResponse DTO documents closeReason. - ActivityService description suffixes Korean reason label and uses 'closed' status when closedAt is set. Frontend - New CloseReasonBadge maps each reason to a colored badge. - Order detail header shows it next to status. Dashboard recent decisions outcome map prefers closeReason over the old positive/negative-PnL guess. Tests - reconcileOrder: 8 scenarios — live position skip, TP, SL, liquidation, empty income, lock-held, race-lost (manual won the update), TP/SL unregistered → manual_on_exchange. - close-position-saga and existing api/web suites still green. Resolves #98. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…a base-currency hooks Two bugs the user hit when checking testnet portfolio after #98 landed. 1. Realized PnL on the testnet card was always 0 because PortfolioService only queried status='filled'. The close-saga and reconciler both flip status to 'closed' on settlement, so every TP/SL/manual close fell out of the rollup. Fix: WHERE status IN ('filled','closed') so realizedPnl is summed from settled rows. 2. Backend `valueKrw` was misnamed — for futures it's USDT × quantity, i.e. USD. The frontend then ran formatKrw(usdAmount) and printed those USD figures with comma-separators as if they were KRW, so KRW display was effectively broken everywhere (and broke harder when the exchange-rate cache was empty). Backend - Order query in PortfolioService.getSummary now includes 'closed'. - Field rename valueKrw→valueUsd (also totalValueKrw→totalValueUsd) so the contract is honest about the unit. Same in DTOs and the frontend types. Frontend - New <MoneyValue usd={n}/> and <PnlMoney usd={n}/> shared components. They read useBaseCurrency + useExchangeRate themselves and route through formatCurrency, so a USD number displays correctly in KRW or USD with a sub-label of the alternate currency. When krwPerUsd=0 (rate not yet loaded) they fall back to USD-only — no more misnamed KRW. - Replaced every formatKrw(usd) / <PnlValue usd> usage: - /portfolio: total / realized / unrealized cards, byNetwork breakdown, asset-table, asset-card-list - /dashboard: today/week PnL cards, open positions table (entry, mark, unrealizedPnl) - /orders/[id]: entry / mark / TP / SL / realized / unrealized rows - Deleted the orphaned <PnlValue> + its test/stories. - demoPortfolio mock updated to USD scale. Tests - New: <MoneyValue> (KRW main + USD sub, USD main + KRW sub, no rate, null) and <PnlMoney> (sign, color, null) — 7 tests. - All existing api/worker/web suites still green (57 + 13 + 43). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three usability gaps reported on the LLM trade form: 1. TP/SL prices were just numbers — user couldn't tell at a glance how far they were from entry. Now each input shows raw price-distance % plus profit/ROE % (multiplied by leverage), with sign-corrected for short positions so a TP below entry on a short reads as +profit. 2. Leverage was a 1-20 number input that didn't match Binance's actual 1x-125x range or its preferred steps. Replaced with a 1-125 slider plus tick-buttons at 1/25/50/75/100/125 for one-click jumps. 3. Bet input was unbounded — user had no idea what their actual USDT balance was. Added a network toggle (testnet/mainnet), wires up the matching exchange key, fetches its balances via the existing /exchange-keys/:id/balances endpoint, displays free/total USDT, and adds 10/25/50/100% quick-fill buttons. Bet is clamped to free balance and the execute button is disabled when over. Backend: GetExchangeKeysHandler now selects `network` so the frontend can pick the right key per network without an extra round-trip. Tests still green (api 57, worker 13, web 43). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-rate fix(portfolio): include closed orders + render USD via base-currency hooks + LLM trade form polish
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Sorry @fray-cloud, your pull request is larger than the review limit of 150000 diff characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack PR들이 부모 브랜치로 머지된 채 dev에 도달하지 못해서, top-of-stack 한 번에 dev로 올림. 포함:
CI는 각 stack PR에서 이미 그린.