Skip to content

feat: Claude CLI driven LLM trade flow + token UI + 7 risk guards#96

Merged
fray-cloud merged 2 commits intofeat/binance-futures-adapterfrom
feat/llm-trade-claude-cli
May 2, 2026
Merged

feat: Claude CLI driven LLM trade flow + token UI + 7 risk guards#96
fray-cloud merged 2 commits intofeat/binance-futures-adapterfrom
feat/llm-trade-claude-cli

Conversation

@fray-cloud
Copy link
Copy Markdown
Owner

@fray-cloud fray-cloud commented Apr 30, 2026

Summary

PR3 of 3 — wires user input → Claude signal → futures execution end-to-end.

Stacks on top of #95 (PR2). Re-target to `dev` after #94#95 → this lands.

Architecture choice

The plan called for the LLM call to live in worker-service. For the synchronous `/signal` call this would have required Kafka request-reply (publish + correlation-id reply consumer + timeout match in a Map), which is heavier than warranted for a single-user app. Decision: api-server hosts the Claude CLI subprocess for the sync path, decrypts the user's OAuth token at the call site, and releases it. Real-trade execution still flows through the existing worker saga (the place where exchange API keys get decrypted) so the trust boundary for actual money movement is unchanged.

What's added

  • Claude Code CLI installed in `coin-base` (`npm install -g @anthropic-ai/claude-code`, `claude --version` runs at build).
  • ClaudeTokens module: AES-256-GCM encrypted per-user OAuth tokens at `POST/GET/DELETE /claude-tokens`. Reuses existing encryption helpers and JWT auth.
  • `/settings/claude` UI: paste / replace / delete OAuth token, with instructions for `claude setup-token`.
  • LlmCliService (api-server): pure `cli-runner` spawn helper + `p-queue`-style queue (concurrency 1) + 30s timeout + 1 retry + strict JSON parsing + 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` config.
  • LlmTrades module (api-server): two-step flow.
    • `POST /llm-trades/signal` — fetch candles via fapi public, call Claude, persist `LlmDecisionLog`, return `{signal, takeProfitPrice, stopLossPrice, reasoning, entryPrice, latencyMs, model}`.
    • `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` WebSocket.
  • Worker RiskGuardService — seven guards run before any real-mode order:
    # Guard Default Notes
    1 `KILL_SWITCH_REAL_TRADING` off Mainnet only
    2 `ENABLE_REAL_MAINNET` required not set Mainnet only
    3 `MAX_LEVERAGE` 20x All networks
    4 `LLM_COOLDOWN_SECONDS` 30s per user Redis SETNX EX
    5 `MAX_OPEN_POSITIONS_PER_USER` 1 All networks
    6 `DAILY_LOSS_LIMIT_USDT` 50 Mainnet only, summed since 00:00 UTC
    7 `MAX_BET_PCT` of available margin 10% Mainnet only
    Hooked into `OrdersService.handleOrderRequested` so all real orders (LLM or otherwise) clear the gate.

Test plan

  • `pnpm build` green across all 9 workspace packages
  • `coin-base` rebuilt; `claude --version` succeeds inside both api-server and worker containers (2.1.123)
  • 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 runs once a Claude token is registered):
    1. `/settings/claude` → paste OAuth token from `claude setup-token`
    2. `/settings` (Accounts tab) → register a Binance Futures testnet API key with `network: 'testnet'`
    3. `/llm-trade` → fill 5 inputs, Signal 받기, verify reasoning + TP/SL look sane, confirm, Execute
    4. Verify position appears on `testnet.binancefuture.com` with TP and SL attached, then close manually and confirm Order DB row + realized PnL
  • CI green
  • Test suites — known broken since PR1; will be repaired in a follow-up

Out of scope

  • Mainnet enablement gate ramp (Phase 5; user flips `ENABLE_REAL_MAINNET=true` after a week of testnet)
  • Position page / manual close-now button (Phase 4 polish)
  • LlmDecisionLog activity feed (Phase 4 polish)

🤖 Generated with Claude Code

Summary by Sourcery

Integrate a Claude CLI–driven LLM trading flow end-to-end, from encrypted user token management through signal generation to guarded futures order execution, with a dedicated UI.

New Features:

  • Add encrypted per-user Claude OAuth token storage and API endpoints for register, update, and delete operations.
  • Introduce a synchronous LLM trading signal pipeline that fetches candles, calls the Claude CLI with a trading system prompt, validates TP/SL geometry, logs decisions, and exposes it via /llm-trades APIs.
  • Add an LLM-driven futures trade execution path that derives position size from bet and leverage, creates orders, and dispatches them over the existing Kafka saga.
  • Provide web UI pages for managing Claude OAuth tokens and for configuring, requesting, and executing LLM-based trades, including navigation entry.

Enhancements:

  • Add a Nest LLM module and CLI runner wrapper for the Claude Code CLI with queuing, timeout, retry, and strict JSON parsing.
  • Configure the API server to bundle markdown prompt assets for runtime use by the LLM CLI integration.

Chores:

  • Introduce a centralized risk guard service in the worker that enforces leverage, cooldown, open-position, daily loss, bet-size, and mainnet kill-switch policies before real order execution and wire it into the existing order handling flow.

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
coin-web Ready Ready Preview, Comment Apr 30, 2026 8:22pm

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 30, 2026

Reviewer's Guide

Implements an end-to-end Claude-CLI–driven futures trading flow (token management, signal generation, and execution) plus a worker-side risk guard layer, and surfaces it via new /settings/claude and /llm-trade web UIs.

Sequence diagram for LLM signal generation via Claude CLI

sequenceDiagram
  actor User
  participant WebApp as WebApp_llm_trade_UI
  participant Api as ApiServer
  participant LlmCtrl as LlmTradesController
  participant LlmSvc as LlmTradesService
  participant TokenSvc as ClaudeTokensService
  participant Binance as BinanceRest
  participant LlmCli as LlmCliService
  participant ClaudeCLI as Claude_CLI_subprocess
  participant Prisma as PrismaService

  User->>WebApp: Click "Signal 받기"
  WebApp->>Api: POST /llm-trades/signal
  Api->>LlmCtrl: Route request
  LlmCtrl->>LlmSvc: signal(userId, RequestSignalDto)

  LlmSvc->>TokenSvc: getDecrypted(userId)
  TokenSvc-->>LlmSvc: oauthToken

  LlmSvc->>Binance: getCandles(symbol, interval, candleCount)
  Binance-->>LlmSvc: candles

  LlmSvc->>LlmCli: decide(oauthToken, symbol, interval, candles)
  LlmCli->>ClaudeCLI: runClaudeCli(prompt, oauthToken, systemPromptFile, model, timeoutMs)
  ClaudeCLI-->>LlmCli: stdout(JSON envelope), stderr, exitCode
  LlmCli-->>LlmSvc: LlmDecision(signal, takeProfitPrice, stopLossPrice, reasoning, rawResponse, latencyMs, model)

  LlmSvc->>Prisma: llmDecisionLog.create(...)
  Prisma-->>LlmSvc: LlmDecisionLog

  LlmSvc-->>LlmCtrl: decision + entryPrice + candles
  LlmCtrl-->>Api: HTTP 200 JSON
  Api-->>WebApp: SignalResponse
  WebApp-->>User: Show signal, TP, SL, reasoning
Loading

Sequence diagram for LLM trade execution with worker risk guards

sequenceDiagram
  actor User
  participant WebApp as WebApp_llm_trade_UI
  participant Api as ApiServer
  participant LlmCtrl as LlmTradesController
  participant LlmSvc as LlmTradesService
  participant PrismaApi as PrismaService_Api
  participant Kafka as KafkaBroker
  participant Worker as WorkerService
  participant OrdersSvc as OrdersService
  participant PrismaWrk as PrismaService_Worker
  participant RiskSvc as RiskGuardService
  participant Redis as Redis
  participant Exchange as BinanceFutures

  User->>WebApp: Click "거래 실행"
  WebApp->>Api: POST /llm-trades/execute
  Api->>LlmCtrl: Route request
  LlmCtrl->>LlmSvc: execute(userId, ExecuteTradeDto)

  LlmSvc->>PrismaApi: exchangeKey.findMany(userId, exchange=binance)
  PrismaApi-->>LlmSvc: exchangeKeys
  LlmSvc->>PrismaApi: order.create(..., mode=real, status=pending)
  PrismaApi-->>LlmSvc: order

  LlmSvc->>Kafka: send(TRADING_ORDER_REQUESTED, OrderRequestedEvent)

  Kafka-->>Worker: OrderRequestedEvent
  Worker->>OrdersSvc: handleOrderRequested(event)

  OrdersSvc->>PrismaWrk: exchangeKey.findFirst(id=exchangeKeyId, userId)
  PrismaWrk-->>OrdersSvc: exchangeKey(network)

  OrdersSvc->>RiskSvc: checkAll(userId, network, order)
  RiskSvc->>Redis: set cooldown key (SETNX EX)
  Redis-->>RiskSvc: ok or existing
  RiskSvc->>PrismaWrk: order.count(open positions)
  PrismaWrk-->>RiskSvc: openCount
  RiskSvc->>PrismaWrk: order.findMany(closed today, realizedPnl)
  PrismaWrk-->>RiskSvc: todayOrders
  RiskSvc-->>OrdersSvc: GuardCheck(ok or reason)

  alt GuardCheck not ok
    OrdersSvc-->>Worker: throw Error("Risk guard: reason")
    Worker-->>Kafka: OrderResultEvent(failed)
  else GuardCheck ok
    OrdersSvc->>Exchange: executeRealOrderSaga(...)
    Exchange-->>OrdersSvc: order result
    OrdersSvc->>Kafka: OrderResultEvent(success)
  end

  Kafka-->>Api: order:updated WebSocket event
  Api-->>WebApp: Order status update
  WebApp-->>User: Show execution result in activity log
Loading

ER diagram for Claude tokens, LLM decision logs, and orders

erDiagram
  USER ||--o| CLAUDE_TOKEN : has
  USER ||--o{ EXCHANGE_KEY : owns
  USER ||--o{ ORDER : places
  USER ||--o{ LLM_DECISION_LOG : creates

  EXCHANGE_KEY ||--o{ ORDER : used_for

  USER {
    string id
    string email
  }

  CLAUDE_TOKEN {
    string userId
    string encryptedToken
    datetime createdAt
    datetime updatedAt
  }

  LLM_DECISION_LOG {
    string id
    string userId
    string prompt
    jsonb parsedSignal
    string model
    integer latencyMs
    text rawResponse
    datetime createdAt
  }

  EXCHANGE_KEY {
    string id
    string userId
    string exchange
    string network
  }

  ORDER {
    string id
    string userId
    string exchangeKeyId
    string exchange
    string symbol
    string side
    string type
    string mode
    string status
    numeric quantity
    integer leverage
    string marginType
    string positionSide
    numeric takeProfitPrice
    numeric stopLossPrice
    numeric realizedPnl
    datetime closedAt
  }
Loading

Class diagram for new LLM trading, Claude token, and risk guard components

classDiagram
  class LlmCliService {
    -Logger logger
    -string model
    -QueueItem[] queue
    -boolean running
    +Promise~LlmDecision~ decide(oauthToken string, symbol string, interval string, candles Candle[])
    -Promise~void~ drain()
    -Promise~LlmDecision~ runOnce(oauthToken string, symbol string, interval string, candles Candle[])
    -string buildUserPrompt(symbol string, interval string, candles Candle[])
    -LlmDecision parse(cliStdout string, candles Candle[])
  }

  class LlmDecisionInput {
    string oauthToken
    string symbol
    string interval
    Candle[] candles
  }

  class LlmDecision {
    string signal
    string takeProfitPrice
    string stopLossPrice
    string reasoning
    string rawResponse
    number latencyMs
    string model
  }

  class ClaudeCliOptions {
    string prompt
    string oauthToken
    string systemPromptFile
    string model
    number timeoutMs
  }

  class ClaudeCliResult {
    string stdout
    string stderr
    number exitCode
    number durationMs
  }

  class LlmModule {
  }

  class ClaudeTokensService {
    -Logger logger
    -ConfigService config
    -PrismaService prisma
    -string masterKey
    +Promise~Status~ getStatus(userId string)
    +Promise~SaveResult~ save(userId string, token string)
    +Promise~void~ delete(userId string)
    +Promise~string~ getDecrypted(userId string)
  }

  class ClaudeTokensController {
    -ClaudeTokensService service
    +status(user Id)
    +save(user Id, dto SaveClaudeTokenDto)
    +remove(user Id)
  }

  class ClaudeTokensModule {
  }

  class SaveClaudeTokenDto {
    string token
  }

  class RequestSignalDto {
    string symbol
    string interval
    number candleCount
  }

  class ExecuteTradeDto {
    string symbol
    string side
    number betUsdt
    number leverage
    string takeProfitPrice
    string stopLossPrice
    string entryPrice
    string exchangeKeyId
  }

  class LlmTradesService {
    -Logger logger
    -BinanceRest binance
    -Kafka kafka
    -Producer producer
    -boolean connected
    -PrismaService prisma
    -ClaudeTokensService tokens
    -LlmCliService llm
    +Promise~SignalResult~ signal(userId string, dto RequestSignalDto)
    +Promise~ExecuteResult~ execute(userId string, dto ExecuteTradeDto)
    +Promise~void~ onModuleInit()
    +Promise~void~ onModuleDestroy()
  }

  class LlmTradesController {
    -LlmTradesService service
    +signal(user Id, dto RequestSignalDto)
    +execute(user Id, dto ExecuteTradeDto)
  }

  class LlmTradesModule {
  }

  class RiskGuardService {
    -Logger logger
    -Redis redis
    -PrismaService prisma
    +Promise~GuardCheck~ checkAll(ctx GuardContext)
    -Promise~GuardCheck~ killSwitch(ctx GuardContext)
    -Promise~GuardCheck~ maxLeverage(ctx GuardContext)
    -Promise~GuardCheck~ cooldown(ctx GuardContext)
    -Promise~GuardCheck~ maxOpenPositions(ctx GuardContext)
    -Promise~GuardCheck~ dailyLossLimit(ctx GuardContext)
    -Promise~GuardCheck~ maxBetPct(ctx GuardContext)
  }

  class GuardContext {
    string userId
    string network
    OrderRequest order
    number availableUsdt
  }

  class GuardCheck {
    boolean ok
    string reason
  }

  class RiskModule {
  }

  class OrdersService {
    -PrismaService prisma
    -RiskGuardService riskGuard
    +handleOrderRequested(event OrderRequestedEvent)
  }

  LlmModule --> LlmCliService
  LlmTradesModule --> LlmTradesService
  LlmTradesModule --> LlmTradesController
  LlmTradesModule --> LlmModule
  LlmTradesModule --> ClaudeTokensModule

  ClaudeTokensModule --> ClaudeTokensService
  ClaudeTokensModule --> ClaudeTokensController
  ClaudeTokensController --> SaveClaudeTokenDto

  LlmTradesService --> RequestSignalDto
  LlmTradesService --> ExecuteTradeDto
  LlmTradesService --> ClaudeTokensService
  LlmTradesService --> LlmCliService

  LlmTradesController --> LlmTradesService

  RiskModule --> RiskGuardService
  OrdersService --> RiskGuardService

  LlmCliService --> ClaudeCliOptions
  LlmCliService --> ClaudeCliResult
  LlmCliService --> LlmDecision
  LlmCliService --> LlmDecisionInput

  RiskGuardService --> GuardContext
  RiskGuardService --> GuardCheck
Loading

File-Level Changes

Change Details Files
Add web client APIs and pages for managing Claude OAuth tokens and driving LLM-based trade signals and execution.
  • Extend shared web api-client with Claude token CRUD functions and LLM trade signal/execute endpoints including error handling.
  • Add a Claude settings page that fetches/saves/deletes encrypted Claude OAuth tokens using react-query and shows registration status.
  • Introduce an LLM trade page and form component that collects trade parameters, requests a signal, allows TP/SL overrides, and submits execution requests, with simple confirmation and status messaging.
  • Wire the new LLM trade page into the main navigation.
apps/web/src/lib/api-client.ts
apps/web/src/components/nav-bar.tsx
apps/web/src/components/llm-trade/llm-trade-form.tsx
apps/web/src/app/settings/claude/page.tsx
apps/web/src/app/llm-trade/page.tsx
Introduce Claude token storage in the API server with AES-encrypted persistence and JWT-protected REST endpoints.
  • Create a ClaudeTokens Nest module with service that encrypts/decrypts tokens using an app master key and stores them in a claudeToken table via Prisma.
  • Expose JWT-guarded GET/POST/DELETE /claude-tokens endpoints for checking status, saving, and deleting a user’s Claude OAuth token, with DTO validation and Swagger annotations.
  • Export the ClaudeTokensService for reuse by other modules and register the module in the main AppModule.
apps/api-server/src/claude-tokens/claude-tokens.service.ts
apps/api-server/src/claude-tokens/claude-tokens.controller.ts
apps/api-server/src/claude-tokens/claude-tokens.module.ts
apps/api-server/src/claude-tokens/dto/save-claude-token.dto.ts
apps/api-server/src/app.module.ts
Add an LLM CLI wrapper module on the API server that invokes the Claude CLI subprocess with strict JSON parsing, queueing, and TP/SL geometry validation.
  • Implement a pure cli-runner that spawns the claude CLI with a JSON output envelope, bare mode, disabled tools, and a configurable system prompt file and timeout, passing the OAuth token via env.
  • Create LlmCliService that queues requests (concurrency 1), retries once on failure, times out, parses the CLI JSON response, and enforces logical constraints on LONG and SHORT TP/SL relative to last candle close.
  • Add a trading-system.md system prompt describing the required JSON-only response contract and TP/SL rules and configure Nest to bundle .md assets.
  • Provide a minimal LlmModule that exports LlmCliService for injection into other modules.
apps/api-server/src/llm/cli-runner.ts
apps/api-server/src/llm/llm-cli.service.ts
apps/api-server/src/llm/prompts/trading-system.md
apps/api-server/src/llm/llm.module.ts
apps/api-server/nest-cli.json
Implement an LlmTrades module that orchestrates candle fetch, calls the LLM CLI, logs decisions, and dispatches execution to the existing worker saga via Kafka.
  • Create DTOs for requesting a signal and executing an LLM trade, with validation on symbol, interval, candle count, leverage, and TP/SL fields.
  • Implement LlmTradesService.signal to get the user’s decrypted Claude token, fetch candles from Binance public REST, call LlmCliService, validate candles, derive entry price, and persist an LlmDecisionLog record.
  • Implement LlmTradesService.execute to choose a Binance exchange key (preferring testnet), derive quantity from bet×leverage/entry, create an Order row, and publish an OrderRequestedEvent to Kafka.
  • Expose POST /llm-trades/signal and /llm-trades/execute endpoints under JWT auth and wire the LlmTrades module into the main AppModule.
apps/api-server/src/llm-trades/llm-trades.service.ts
apps/api-server/src/llm-trades/llm-trades.controller.ts
apps/api-server/src/llm-trades/llm-trades.module.ts
apps/api-server/src/llm-trades/dto/request-signal.dto.ts
apps/api-server/src/llm-trades/dto/execute-trade.dto.ts
apps/api-server/src/app.module.ts
Add a RiskGuardService to the worker that enforces multiple safety checks before any real-mode order execution and integrate it into the existing order saga.
  • Introduce a RiskModule exporting RiskGuardService and register it in the worker AppModule.
  • Implement RiskGuardService with Redis-backed cooldown and Prisma-powered queries to enforce kill switch/mainnet enablement, max leverage, per-user cooldown, max open positions, daily loss limit, and max bet percentage rules, all configurable via environment variables and network-aware.
  • Modify OrdersService to inject RiskGuardService, resolve the exchange key’s network from Prisma, and run riskGuard.checkAll before invoking the real order execution saga, aborting with an error if any guard fails.
apps/worker-service/src/risk/risk-guard.service.ts
apps/worker-service/src/risk/risk.module.ts
apps/worker-service/src/app.module.ts
apps/worker-service/src/orders/orders.service.ts
apps/worker-service/src/orders/orders.module.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In runClaudeCli, the timeout handler calls reject but the close event will still later call resolve, so you should track a settled flag or remove listeners after timeout to avoid double-settling the promise.
  • The RiskGuardService.cooldown guard currently applies to all real-mode orders; if the intent is to only rate-limit LLM-driven trades, consider threading an isLlmOrder flag through the context so manual trades are not throttled by the same Redis cooldown key.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `runClaudeCli`, the timeout handler calls `reject` but the `close` event will still later call `resolve`, so you should track a `settled` flag or remove listeners after timeout to avoid double-settling the promise.
- The `RiskGuardService.cooldown` guard currently applies to all real-mode orders; if the intent is to only rate-limit LLM-driven trades, consider threading an `isLlmOrder` flag through the context so manual trades are not throttled by the same Redis cooldown key.

## Individual Comments

### Comment 1
<location path="apps/api-server/src/claude-tokens/claude-tokens.service.ts" line_range="37-38" />
<code_context>
+  }
+
+  async delete(userId: string): Promise<void> {
+    await this.prisma.claudeToken.delete({ where: { userId } }).catch(() => {
+      throw new NotFoundException('Claude token not registered');
+    });
+    this.logger.log(`Claude token deleted for user ${userId}`);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Catch-all delete error handling masks non-NotFound failures as 404s.

This `catch` converts every delete failure (including DB/connectivity issues) into a `NotFoundException`, which hides real errors and misleads clients. Instead, detect the specific "record not found" condition (e.g. Prisma `P2025`) and only map that to `NotFoundException`, letting other errors propagate unchanged.

Suggested implementation:

```typescript
  async delete(userId: string): Promise<void> {
    try {
      await this.prisma.claudeToken.delete({ where: { userId } });
    } catch (error) {
      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === 'P2025'
      ) {
        throw new NotFoundException('Claude token not registered');
      }
      throw error;
    }
    this.logger.log(`Claude token deleted for user ${userId}`);
  }

```

To compile correctly, ensure that `Prisma` is imported from `@prisma/client` at the top of this file. For example, if you currently have:
```ts
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
```
you should also add:
```ts
import { Prisma } from '@prisma/client';
```
Adjust the import location to match your existing import style and ordering.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +37 to +38
await this.prisma.claudeToken.delete({ where: { userId } }).catch(() => {
throw new NotFoundException('Claude token not registered');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Catch-all delete error handling masks non-NotFound failures as 404s.

This catch converts every delete failure (including DB/connectivity issues) into a NotFoundException, which hides real errors and misleads clients. Instead, detect the specific "record not found" condition (e.g. Prisma P2025) and only map that to NotFoundException, letting other errors propagate unchanged.

Suggested implementation:

  async delete(userId: string): Promise<void> {
    try {
      await this.prisma.claudeToken.delete({ where: { userId } });
    } catch (error) {
      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === 'P2025'
      ) {
        throw new NotFoundException('Claude token not registered');
      }
      throw error;
    }
    this.logger.log(`Claude token deleted for user ${userId}`);
  }

To compile correctly, ensure that Prisma is imported from @prisma/client at the top of this file. For example, if you currently have:

import { Injectable, Logger, NotFoundException } from '@nestjs/common';

you should also add:

import { Prisma } from '@prisma/client';

Adjust the import location to match your existing import style and ordering.

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>
@fray-cloud fray-cloud merged commit 4939fc9 into feat/binance-futures-adapter May 2, 2026
3 checks passed
fray-cloud added a commit that referenced this pull request May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant