feat(llm-trade): enrich prompt with indicators, structure, and futures sentiment (#99)#104
feat(llm-trade): enrich prompt with indicators, structure, and futures sentiment (#99)#104fray-cloud wants to merge 1 commit intodevfrom
Conversation
…s sentiment (#99) Closes #99. Until now Claude received only OHLCV candles + symbol/interval — sufficient for "raw price" reasoning but missing the trend strength, volatility, and positioning signals a trading analyst actually uses. Signal quality was inconsistent and indicators had to be inferred from scratch every call. This PR adds three layers to the user prompt: 1. Inline-indicator enriched candles. Each candle row now carries EMA20/50/ 200, RSI14, Bollinger Bands(20, 2σ), MACD(12/26/9), and ATR14 alongside OHLCV — the LLM can read trend alignment and momentum off a single row. 2. structure summary — swing high/low over the prompt window plus an ATR-derived suggested SL distance, anchoring TP/SL placement to objective reference points instead of arbitrary % targets. 3. sentiment section — current funding rate + last 8 settlements + 7d avg, plus 24-point OI history with 24h % change. Tells the LLM how positioned the market is and whether new money is flowing in. Schema decisions (from the grilling): - candleCount slider relabeled "프롬프트 캔들 수", range 30-120 (default 60). Backend always fetches MAX(220, prompt+50) so EMA200 is stable regardless of what the user picks. - Timestamps are ISO 8601 truncated to interval precision (1m → minute, 1h → hour, 1d → date) so the LLM can recognize sessions and the 8h funding cycle without arithmetic. - Funding/OI live in their own `sentiment` section at their natural cadence (funding 8h, OI 24×1h) rather than being inlined per candle — avoids repeating the same funding value 60 times on a 5m window. - Sentiment fetch failures degrade to null instead of failing the signal. System prompt instructs the LLM to treat null as "missing, do not guess." Modules - New @coin/indicators workspace package — wraps technicalindicators behind a single computeIndicators(candles) entry point. Aligned IndicatorRow per candle with explicit nulls during warmup. Owned types live in @coin/types. - New BinanceRest methods: getFundingRateHistory, getCurrentFundingRate, getOpenInterest, getOpenInterestHistory. All public (no signature). - New MarketContextService in api-server — orchestrates fetch + compute + structure + sentiment with Redis caching (funding 1h, OI 60s) and a deep buildMarketContext core that's pure modulo deps for unit testing. - LlmCliService.decide now accepts a marketContext directly; LlmTradesService. signal builds and forwards it. The DB log captures schema metadata so old vs new prompts are distinguishable. - Updated TRADING_SYSTEM_PROMPT documents the new schema (field names, units, null semantics) and tells the model to calibrate TP/SL to structure.atrPct and structure.suggestedSlPct. Tests - @coin/indicators: 5 tests — alignment with input length, warmup nulls, RSI behavior on trending vs flat closes, BB middle = SMA20, ATR positive. - buildMarketContext: 9 tests — prompt window slicing, 220-candle floor, empty-candles error, degraded sentiment on adapter throws, structure swing/ATR derivation, OI 24h change. - All existing api (66) + worker (13) + web (43) suites still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideAdds a new indicators package and market-context pipeline so LLM trades use enriched candles, structural stats, and futures sentiment, plus new Binance futures data endpoints and a retuned trading system prompt and UI slider. Sequence diagram for enriched LLM trade signal flowsequenceDiagram
actor User
participant WebApp
participant ApiServer as ApiServer_LlmTradesController
participant LlmTradesService
participant MarketContextService
participant Redis
participant BinanceRest
participant LlmCliService
participant ClaudeCli as Claude_CLI
User->>WebApp: Submit LLM trade form
WebApp->>ApiServer: POST /llm-trades/signal
ApiServer->>LlmTradesService: signal(userId,dto)
LlmTradesService->>LlmTradesService: getDecryptedToken(userId)
LlmTradesService->>MarketContextService: build(symbol,interval,promptCandleCount)
MarketContextService->>BinanceRest: getCandles(symbol,interval,fetchCount>=max(220,prompt+50))
MarketContextService->>Redis: GET funding:history:symbol
Redis-->>MarketContextService: cache hit or miss
alt funding history cache miss
MarketContextService->>BinanceRest: getFundingRateHistory(symbol,8)
MarketContextService->>Redis: SET funding:history:symbol
end
MarketContextService->>Redis: GET funding:current:symbol
alt funding current cache miss
MarketContextService->>BinanceRest: getCurrentFundingRate(symbol)
MarketContextService->>Redis: SET funding:current:symbol
end
MarketContextService->>Redis: GET oi:history:symbol:period
alt oi history cache miss
MarketContextService->>BinanceRest: getOpenInterestHistory(symbol,period,24)
MarketContextService->>Redis: SET oi:history:symbol:period
end
MarketContextService->>MarketContextService: computeIndicators(candles)
MarketContextService->>MarketContextService: derive structure and sentiment
MarketContextService-->>LlmTradesService: MarketContext
LlmTradesService->>LlmCliService: decide(oauthToken,marketContext)
LlmCliService->>ClaudeCli: runClaudeCli(TRADING_SYSTEM_PROMPT,JSON.stringify(marketContext))
ClaudeCli-->>LlmCliService: raw JSON decision
LlmCliService->>LlmCliService: parse and validate using last candle close
LlmCliService-->>LlmTradesService: LlmDecision
LlmTradesService->>LlmTradesService: compute entryPrice from last candle
LlmTradesService->>ApiServer: save llmDecisionLog
LlmTradesService-->>ApiServer: decision + entryPrice + marketContext
ApiServer-->>WebApp: HTTP response
WebApp-->>User: Show signal, TP, SL, context
Class diagram for indicators package and indicator typesclassDiagram
class Candle {
<<interface>>
+string exchange
+string symbol
+string interval
+string open
+string high
+string low
+string close
+string volume
+number timestamp
}
class IndicatorRow {
<<interface>>
+number nullable ema20
+number nullable ema50
+number nullable ema200
+number nullable rsi14
+number nullable bbUpper
+number nullable bbMiddle
+number nullable bbLower
+number nullable macd
+number nullable macdSignal
+number nullable macdHistogram
+number nullable atr14
}
class IndicatorSeries {
<<interface>>
+IndicatorRow[] rows
}
class ComputeOptions {
<<interface>>
+number[3] nullable emaPeriods
+number nullable rsiPeriod
+number nullable bbPeriod
+number nullable bbStdDev
+number nullable macdFast
+number nullable macdSlow
+number nullable macdSignal
+number nullable atrPeriod
}
class IndicatorsModule {
+computeIndicators(candles:Candle[],opts:ComputeOptions) IndicatorSeries
}
Candle --> IndicatorSeries : input
IndicatorSeries --> IndicatorRow : contains
IndicatorsModule --> Candle : reads
IndicatorsModule --> IndicatorSeries : returns
IndicatorsModule --> IndicatorRow : populates
Class diagram for market context, services, and Binance futures sentimentclassDiagram
class FundingRateRecord {
<<interface>>
+string symbol
+number fundingTime
+string fundingRate
}
class OpenInterestSnapshot {
<<interface>>
+string symbol
+string openInterest
+number timestamp
}
class OpenInterestPoint {
<<interface>>
+string symbol
+string sumOpenInterest
+string sumOpenInterestValueUsdt
+number timestamp
}
class IndicatorRow {
<<interface>>
+number nullable ema20
+number nullable ema50
+number nullable ema200
+number nullable rsi14
+number nullable bbUpper
+number nullable bbMiddle
+number nullable bbLower
+number nullable macd
+number nullable macdSignal
+number nullable macdHistogram
+number nullable atr14
}
class EnrichedCandleRow {
<<interface>>
+string t
+string o
+string h
+string l
+string c
+string v
+number nullable ema20
+number nullable ema50
+number nullable ema200
+number nullable rsi14
+number nullable bbUpper
+number nullable bbMiddle
+number nullable bbLower
+number nullable macd
+number nullable macdSignal
+number nullable macdHistogram
+number nullable atr14
}
class StructureSummary {
<<interface>>
+number nullable swingHigh
+string nullable swingHighAt
+number nullable swingLow
+string nullable swingLowAt
+number nullable atrPct
+number nullable suggestedSlPct
}
class FundingSentiment {
<<interface>>
+string nullable current
+number nullable nextFundingTime
+SettlementPoint[] lastSettlements
+number nullable avg7d
}
class SettlementPoint {
<<type>>
+string t
+string rate
}
class OpenInterestSentiment {
<<interface>>
+number nullable currentUsdt
+number nullable change24hPct
+OiHistoryPoint[] history
}
class OiHistoryPoint {
<<type>>
+string t
+number oi
}
class SentimentSummary {
<<interface>>
+FundingSentiment nullable fundingRate
+OpenInterestSentiment nullable openInterest
}
class MarketContext {
<<interface>>
+string symbol
+string interval
+EnrichedCandleRow[] candles
+StructureSummary structure
+SentimentSummary sentiment
}
class BuildOptions {
<<interface>>
+string symbol
+string interval
+number promptCandleCount
}
class BuildDeps {
<<interface>>
+fetchCandles(symbol:string,interval:string,count:number) Promise~Candle[]~
+fetchFundingHistory(symbol:string,limit:number) Promise~FundingRateRecord[]~
+fetchCurrentFunding(symbol:string) Promise~FundingCurrent~
+fetchOpenInterestHistory(symbol:string,period:string,limit:number) Promise~OpenInterestPoint[]~
}
class FundingCurrent {
<<type>>
+string symbol
+string lastFundingRate
+number nextFundingTime
}
class MarketContextBuilder {
+MIN_FETCH_COUNT:number
+buildMarketContext(opts:BuildOptions,deps:BuildDeps) Promise~MarketContext~
+formatTimestamp(epochMs:number,interval:string) string
}
class MarketContextService {
+build(symbol:string,interval:string,promptCandleCount:number) Promise~MarketContext~
-cached~T~(key:string,ttlSec:number,fetcher:Function) Promise~T~
}
class IExchangeRest {
<<interface>>
+getFundingRateHistory(symbol:string,limit?:number) Promise~FundingRateRecord[]~
+getCurrentFundingRate(symbol:string) Promise~FundingCurrent~
+getOpenInterest(symbol:string) Promise~OpenInterestSnapshot~
+getOpenInterestHistory(symbol:string,period:string,limit?:number) Promise~OpenInterestPoint[]~
}
class BinanceRest {
+getFundingRateHistory(symbol:string,limit?:number) Promise~FundingRateRecord[]~
+getCurrentFundingRate(symbol:string) Promise~FundingCurrent~
+getOpenInterest(symbol:string) Promise~OpenInterestSnapshot~
+getOpenInterestHistory(symbol:string,period:string,limit?:number) Promise~OpenInterestPoint[]~
}
class LlmCliService {
+decide(oauthToken:string,marketContext:MarketContext) Promise~LlmDecision~
-buildUserPrompt(input:LlmDecisionInput) string
-parse(cliStdout:string,marketContext:MarketContext) LlmDecision
}
class LlmTradesService {
+signal(userId:string,dto:RequestSignalDto) Promise~LlmDecisionWithContext~
}
class LlmDecisionInput {
<<type>>
+string oauthToken
+MarketContext marketContext
}
class LlmDecisionWithContext {
<<type>>
+string signal
+string takeProfitPrice
+string stopLossPrice
+string entryPrice
+MarketContext marketContext
}
FundingSentiment --> SettlementPoint : uses
OpenInterestSentiment --> OiHistoryPoint : uses
SentimentSummary --> FundingSentiment : contains
SentimentSummary --> OpenInterestSentiment : contains
MarketContext --> EnrichedCandleRow : candles
MarketContext --> StructureSummary : structure
MarketContext --> SentimentSummary : sentiment
EnrichedCandleRow --|> IndicatorRow : extends
MarketContextBuilder --> BuildOptions : input
MarketContextBuilder --> BuildDeps : input
MarketContextBuilder --> MarketContext : output
MarketContextBuilder --> EnrichedCandleRow : builds
MarketContextBuilder --> StructureSummary : computes
MarketContextBuilder --> SentimentSummary : computes
MarketContextService --> MarketContextBuilder : calls_buildMarketContext
MarketContextService --> BinanceRest : uses
BinanceRest ..|> IExchangeRest
LlmCliService --> MarketContext : reads
LlmTradesService --> MarketContextService : uses
LlmTradesService --> LlmCliService : uses
LlmTradesService --> MarketContext : returns_in_response
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The 7d funding-rate average in
computeAvg7dis computed from whateverfetchFundingHistoryreturns (currently limited to 8 records), which contradicts the inline comment about 21 settlements over 7 days; consider either increasing the history limit or updating the semantics/comment so the LLM isn’t misled by a very short-window “7d” average. MarketContextServicedirectly instantiatesRedisandBinanceRestrather than using Nest DI, which makes configuration and testing harder; consider injecting these as providers so you can more easily swap endpoints/credentials or mock them in higher-level tests.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The 7d funding-rate average in `computeAvg7d` is computed from whatever `fetchFundingHistory` returns (currently limited to 8 records), which contradicts the inline comment about 21 settlements over 7 days; consider either increasing the history limit or updating the semantics/comment so the LLM isn’t misled by a very short-window “7d” average.
- `MarketContextService` directly instantiates `Redis` and `BinanceRest` rather than using Nest DI, which makes configuration and testing harder; consider injecting these as providers so you can more easily swap endpoints/credentials or mock them in higher-level tests.
## Individual Comments
### Comment 1
<location path="apps/api-server/src/llm-trades/market-context/build-market-context.ts" line_range="189-190" />
<code_context>
+ interval: string,
+ deps: BuildDeps,
+): Promise<OpenInterestSentiment | null> {
+ // OI history granularity scales with the user's candle interval, capped at 1h
+ // to keep the series readable (24 points = 24h window at 1h granularity).
+ const period = oiPeriodFor(interval);
+ try {
</code_context>
<issue_to_address>
**issue:** The OI period selection comment doesn’t match the actual behavior for sub-hour intervals.
Since oiPeriodFor returns '5m' for 1m/3m/5m intervals, the 24 points only span ~2h, so change24hPct is not actually a 24h change for those cases. Consider either always using a fixed '1h' period so the metric is truly 24h, or renaming/adjusting downstream usage and prompt text to reflect that this is a shorter-window OI change on low timeframes.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| // OI history granularity scales with the user's candle interval, capped at 1h | ||
| // to keep the series readable (24 points = 24h window at 1h granularity). |
There was a problem hiding this comment.
issue: The OI period selection comment doesn’t match the actual behavior for sub-hour intervals.
Since oiPeriodFor returns '5m' for 1m/3m/5m intervals, the 24 points only span ~2h, so change24hPct is not actually a 24h change for those cases. Consider either always using a fixed '1h' period so the metric is truly 24h, or renaming/adjusting downstream usage and prompt text to reflect that this is a shorter-window OI change on low timeframes.
Closes #99.
Three new prompt layers
structuresummary — swing high/low over the prompt window + ATR-derived suggested SL distance.sentimentsection — current funding rate + last 8 settlements + 7d avg, plus 24-point OI history with 24h % change.Decisions (from grilling)
프롬프트 캔들 수, range 30~120 (default 60). Backend always fetches MAX(220, prompt+50) so EMA200 is stable.sentimentsection at natural cadence (funding 8h, OI 24×1h) — not inlined per candle.null; system prompt instructs LLM to treat null as "missing, do not guess".Modules
@coin/indicatorsworkspace package — singlecomputeIndicators(candles)entry, ownsIndicatorRow/IndicatorSeriestypes in@coin/types. Wrapstechnicalindicatorsso the rest of the codebase never imports it directly.getFundingRateHistory,getCurrentFundingRate,getOpenInterest,getOpenInterestHistory.MarketContextServicein api-server with Redis caching (funding 1h, OI 60s) and a deepbuildMarketContextcore that's pure modulo deps for unit testing.LlmCliService.decidenow takesmarketContextdirectly;LlmTradesService.signalbuilds and forwards it.TRADING_SYSTEM_PROMPTupdated with full schema documentation.Tests
@coin/indicators: 5 testsbuildMarketContext: 9 tests (prompt slicing, 220-candle floor, empty error, degraded sentiment, structure derivation, OI 24h change)🤖 Generated with Claude Code
Summary by Sourcery
Introduce an LLM-ready market context layer that enriches trading prompts with technical indicators, structural summaries, and futures sentiment, and refactor LLM trade signalling to consume this unified context.
New Features:
Enhancements:
Tests: