-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Claude CLI driven LLM trade flow + token UI + 7 risk guards #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
fray-cloud
merged 2 commits into
feat/binance-futures-adapter
from
feat/llm-trade-claude-cli
May 2, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
33 changes: 33 additions & 0 deletions
33
apps/api-server/src/claude-tokens/claude-tokens.controller.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { Body, Controller, Delete, Get, HttpCode, Post, UseGuards } from '@nestjs/common'; | ||
| import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; | ||
| import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; | ||
| import { CurrentUser } from '../auth/decorators/current-user.decorator'; | ||
| import { ClaudeTokensService } from './claude-tokens.service'; | ||
| import { SaveClaudeTokenDto } from './dto/save-claude-token.dto'; | ||
|
|
||
| @ApiTags('Claude Tokens') | ||
| @ApiBearerAuth() | ||
| @UseGuards(JwtAuthGuard) | ||
| @Controller('claude-tokens') | ||
| export class ClaudeTokensController { | ||
| constructor(private readonly service: ClaudeTokensService) {} | ||
|
|
||
| @Get() | ||
| @ApiOperation({ summary: 'Check if a Claude OAuth token is registered for the user' }) | ||
| status(@CurrentUser() user: { id: string }) { | ||
| return this.service.getStatus(user.id); | ||
| } | ||
|
|
||
| @Post() | ||
| @ApiOperation({ summary: 'Save / replace the user Claude OAuth token (encrypted at rest)' }) | ||
| save(@CurrentUser() user: { id: string }, @Body() dto: SaveClaudeTokenDto) { | ||
| return this.service.save(user.id, dto.token); | ||
| } | ||
|
|
||
| @Delete() | ||
| @ApiOperation({ summary: 'Delete the user Claude OAuth token' }) | ||
| @HttpCode(204) | ||
| async remove(@CurrentUser() user: { id: string }) { | ||
| await this.service.delete(user.id); | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { Module } from '@nestjs/common'; | ||
| import { PrismaModule } from '../prisma/prisma.module'; | ||
| import { ClaudeTokensController } from './claude-tokens.controller'; | ||
| import { ClaudeTokensService } from './claude-tokens.service'; | ||
|
|
||
| @Module({ | ||
| imports: [PrismaModule], | ||
| controllers: [ClaudeTokensController], | ||
| providers: [ClaudeTokensService], | ||
| exports: [ClaudeTokensService], | ||
| }) | ||
| export class ClaudeTokensModule {} |
49 changes: 49 additions & 0 deletions
49
apps/api-server/src/claude-tokens/claude-tokens.service.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { Injectable, NotFoundException, Logger } from '@nestjs/common'; | ||
| import { ConfigService } from '@nestjs/config'; | ||
| import { PrismaService } from '../prisma/prisma.service'; | ||
| import { encrypt, decrypt } from '@coin/utils'; | ||
|
|
||
| @Injectable() | ||
| export class ClaudeTokensService { | ||
| private readonly logger = new Logger(ClaudeTokensService.name); | ||
|
|
||
| constructor( | ||
| private readonly prisma: PrismaService, | ||
| private readonly config: ConfigService, | ||
| ) {} | ||
|
|
||
| private get masterKey(): string { | ||
| return this.config.getOrThrow<string>('ENCRYPTION_MASTER_KEY'); | ||
| } | ||
|
|
||
| async getStatus(userId: string): Promise<{ registered: boolean; updatedAt?: Date }> { | ||
| const row = await this.prisma.claudeToken.findUnique({ where: { userId } }); | ||
| if (!row) return { registered: false }; | ||
| return { registered: true, updatedAt: row.updatedAt }; | ||
| } | ||
|
|
||
| async save(userId: string, token: string): Promise<{ updatedAt: Date }> { | ||
| const encrypted = encrypt(token, this.masterKey); | ||
| const row = await this.prisma.claudeToken.upsert({ | ||
| where: { userId }, | ||
| update: { encryptedToken: encrypted }, | ||
| create: { userId, encryptedToken: encrypted }, | ||
| }); | ||
| this.logger.log(`Claude token saved for user ${userId}`); | ||
| return { updatedAt: row.updatedAt }; | ||
| } | ||
|
|
||
| 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}`); | ||
| } | ||
|
|
||
| /** Decrypt and return the raw OAuth token. Use only at LLM call time. */ | ||
| async getDecrypted(userId: string): Promise<string> { | ||
| const row = await this.prisma.claudeToken.findUnique({ where: { userId } }); | ||
| if (!row) throw new NotFoundException('Claude token not registered'); | ||
| return decrypt(row.encryptedToken, this.masterKey); | ||
| } | ||
| } | ||
12 changes: 12 additions & 0 deletions
12
apps/api-server/src/claude-tokens/dto/save-claude-token.dto.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { IsString, MinLength } from 'class-validator'; | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
|
|
||
| export class SaveClaudeTokenDto { | ||
| @ApiProperty({ | ||
| description: 'Claude Code OAuth long-lived token (run `claude setup-token`)', | ||
| example: 'sk-ant-oat01-...', | ||
| }) | ||
| @IsString() | ||
| @MinLength(20) | ||
| token!: string; | ||
| } |
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { IsString, IsIn, IsInt, IsOptional, Min, Max } from 'class-validator'; | ||
| import { Type } from 'class-transformer'; | ||
| import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; | ||
|
|
||
| export class ExecuteTradeDto { | ||
| @ApiProperty({ description: 'USDT-M perpetual symbol', example: 'BTCUSDT' }) | ||
| @IsString() | ||
| symbol!: string; | ||
|
|
||
| @ApiProperty({ description: '포지션 방향', enum: ['long', 'short'] }) | ||
| @IsIn(['long', 'short']) | ||
| side!: 'long' | 'short'; | ||
|
|
||
| @ApiProperty({ description: '베팅비용 (증거금, USDT)', example: 50 }) | ||
| @Type(() => Number) | ||
| @Min(1) | ||
| betUsdt!: number; | ||
|
|
||
| @ApiProperty({ description: '레버리지 (1-20)', example: 5 }) | ||
| @Type(() => Number) | ||
| @IsInt() | ||
| @Min(1) | ||
| @Max(20) | ||
| leverage!: number; | ||
|
|
||
| @ApiProperty({ description: '익절 가격 (사용자가 LLM 응답에서 override 가능)', example: '67000' }) | ||
| @IsString() | ||
| takeProfitPrice!: string; | ||
|
|
||
| @ApiProperty({ description: '손절 가격', example: '63000' }) | ||
| @IsString() | ||
| stopLossPrice!: string; | ||
|
|
||
| @ApiProperty({ | ||
| description: '진입가 (LLM 응답 시점의 lastClose, 수량 계산에 사용)', | ||
| example: '65000', | ||
| }) | ||
| @IsString() | ||
| entryPrice!: string; | ||
|
|
||
| @ApiPropertyOptional({ | ||
| description: '거래소 키 ID (지정하지 않으면 testnet 키 우선)', | ||
| }) | ||
| @IsOptional() | ||
| @IsString() | ||
| exchangeKeyId?: string; | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { IsString, IsIn, IsInt, Min, Max } from 'class-validator'; | ||
| import { Type } from 'class-transformer'; | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
|
|
||
| const ALLOWED_INTERVALS = ['1m', '5m', '15m', '1h', '4h', '1d'] as const; | ||
|
|
||
| export class RequestSignalDto { | ||
| @ApiProperty({ description: 'USDT-M perpetual symbol', example: 'BTCUSDT' }) | ||
| @IsString() | ||
| symbol!: string; | ||
|
|
||
| @ApiProperty({ | ||
| description: '캔들 간격', | ||
| enum: ALLOWED_INTERVALS, | ||
| example: '5m', | ||
| }) | ||
| @IsIn(ALLOWED_INTERVALS as unknown as string[]) | ||
| interval!: string; | ||
|
|
||
| @ApiProperty({ description: '캔들 개수 (20-200)', example: 50 }) | ||
| @Type(() => Number) | ||
| @IsInt() | ||
| @Min(20) | ||
| @Max(200) | ||
| candleCount!: number; | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { Body, Controller, Post, UseGuards } from '@nestjs/common'; | ||
| import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; | ||
| import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; | ||
| import { CurrentUser } from '../auth/decorators/current-user.decorator'; | ||
| import { LlmTradesService } from './llm-trades.service'; | ||
| import { RequestSignalDto } from './dto/request-signal.dto'; | ||
| import { ExecuteTradeDto } from './dto/execute-trade.dto'; | ||
|
|
||
| @ApiTags('LLM Trades') | ||
| @ApiBearerAuth() | ||
| @UseGuards(JwtAuthGuard) | ||
| @Controller('llm-trades') | ||
| export class LlmTradesController { | ||
| constructor(private readonly service: LlmTradesService) {} | ||
|
|
||
| @Post('signal') | ||
| @ApiOperation({ | ||
| summary: 'Fetch candles + ask Claude for long/short + TP/SL (sync, ~5-10s)', | ||
| }) | ||
| signal(@CurrentUser() user: { id: string }, @Body() dto: RequestSignalDto) { | ||
| return this.service.signal(user.id, dto); | ||
| } | ||
|
|
||
| @Post('execute') | ||
| @ApiOperation({ | ||
| summary: 'Place futures market order with attached TP/SL via worker saga', | ||
| }) | ||
| execute(@CurrentUser() user: { id: string }, @Body() dto: ExecuteTradeDto) { | ||
| return this.service.execute(user.id, dto); | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { Module } from '@nestjs/common'; | ||
| import { PrismaModule } from '../prisma/prisma.module'; | ||
| import { ClaudeTokensModule } from '../claude-tokens/claude-tokens.module'; | ||
| import { LlmModule } from '../llm/llm.module'; | ||
| import { LlmTradesController } from './llm-trades.controller'; | ||
| import { LlmTradesService } from './llm-trades.service'; | ||
|
|
||
| @Module({ | ||
| imports: [PrismaModule, ClaudeTokensModule, LlmModule], | ||
| controllers: [LlmTradesController], | ||
| providers: [LlmTradesService], | ||
| }) | ||
| export class LlmTradesModule {} |
Oops, something went wrong.
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.
There was a problem hiding this comment.
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
catchconverts every delete failure (including DB/connectivity issues) into aNotFoundException, which hides real errors and misleads clients. Instead, detect the specific "record not found" condition (e.g. PrismaP2025) and only map that toNotFoundException, letting other errors propagate unchanged.Suggested implementation:
To compile correctly, ensure that
Prismais imported from@prisma/clientat the top of this file. For example, if you currently have:you should also add:
Adjust the import location to match your existing import style and ordering.