Implement signal quality scoring system
- Updated execute endpoint to store context metrics in database - Updated CreateTradeParams interface with 5 context metrics - Updated Prisma schema with rsiAtEntry and pricePositionAtEntry - Ran migration: add_rsi_and_price_position_metrics - Complete flow: TradingView → n8n → check-risk (scores) → execute (stores)
This commit is contained in:
@@ -13,12 +13,20 @@ import { getLastTradeTime, getTradesInLastHour, getTodayPnL } from '@/lib/databa
|
|||||||
export interface RiskCheckRequest {
|
export interface RiskCheckRequest {
|
||||||
symbol: string
|
symbol: string
|
||||||
direction: 'long' | 'short'
|
direction: 'long' | 'short'
|
||||||
|
// Optional context metrics from TradingView
|
||||||
|
atr?: number
|
||||||
|
adx?: number
|
||||||
|
rsi?: number
|
||||||
|
volumeRatio?: number
|
||||||
|
pricePosition?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RiskCheckResponse {
|
export interface RiskCheckResponse {
|
||||||
allowed: boolean
|
allowed: boolean
|
||||||
reason?: string
|
reason?: string
|
||||||
details?: string
|
details?: string
|
||||||
|
qualityScore?: number
|
||||||
|
qualityReasons?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest): Promise<NextResponse<RiskCheckResponse>> {
|
export async function POST(request: NextRequest): Promise<NextResponse<RiskCheckResponse>> {
|
||||||
@@ -135,6 +143,50 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Check signal quality (if context metrics provided)
|
||||||
|
const hasContextMetrics = body.atr !== undefined && body.atr > 0
|
||||||
|
|
||||||
|
if (hasContextMetrics) {
|
||||||
|
const qualityScore = scoreSignalQuality({
|
||||||
|
atr: body.atr || 0,
|
||||||
|
adx: body.adx || 0,
|
||||||
|
rsi: body.rsi || 0,
|
||||||
|
volumeRatio: body.volumeRatio || 0,
|
||||||
|
pricePosition: body.pricePosition || 0,
|
||||||
|
direction: body.direction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!qualityScore.passed) {
|
||||||
|
console.log('🚫 Risk check BLOCKED: Signal quality too low', {
|
||||||
|
score: qualityScore.score,
|
||||||
|
reasons: qualityScore.reasons
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
allowed: false,
|
||||||
|
reason: 'Signal quality too low',
|
||||||
|
details: `Score: ${qualityScore.score}/100 - ${qualityScore.reasons.join(', ')}`,
|
||||||
|
qualityScore: qualityScore.score,
|
||||||
|
qualityReasons: qualityScore.reasons
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Risk check PASSED: All checks passed`, {
|
||||||
|
todayPnL: todayPnL.toFixed(2),
|
||||||
|
tradesLastHour: tradesInLastHour,
|
||||||
|
cooldownPassed: lastTradeTime ? 'yes' : 'no previous trades',
|
||||||
|
qualityScore: qualityScore.score,
|
||||||
|
qualityReasons: qualityScore.reasons
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
allowed: true,
|
||||||
|
details: 'All risk checks passed',
|
||||||
|
qualityScore: qualityScore.score,
|
||||||
|
qualityReasons: qualityScore.reasons
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`✅ Risk check PASSED: All checks passed`, {
|
console.log(`✅ Risk check PASSED: All checks passed`, {
|
||||||
todayPnL: todayPnL.toFixed(2),
|
todayPnL: todayPnL.toFixed(2),
|
||||||
tradesLastHour: tradesInLastHour,
|
tradesLastHour: tradesInLastHour,
|
||||||
@@ -159,3 +211,106 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignalQualityResult {
|
||||||
|
passed: boolean
|
||||||
|
score: number
|
||||||
|
reasons: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Score signal quality based on context metrics from TradingView
|
||||||
|
* Returns score 0-100 and array of reasons
|
||||||
|
*/
|
||||||
|
function scoreSignalQuality(params: {
|
||||||
|
atr: number
|
||||||
|
adx: number
|
||||||
|
rsi: number
|
||||||
|
volumeRatio: number
|
||||||
|
pricePosition: number
|
||||||
|
direction: 'long' | 'short'
|
||||||
|
}): SignalQualityResult {
|
||||||
|
let score = 50 // Base score
|
||||||
|
const reasons: string[] = []
|
||||||
|
|
||||||
|
// ATR check (volatility gate: 0.6% - 2.5%)
|
||||||
|
if (params.atr > 0) {
|
||||||
|
if (params.atr < 0.6) {
|
||||||
|
score -= 15
|
||||||
|
reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`)
|
||||||
|
} else if (params.atr > 2.5) {
|
||||||
|
score -= 20
|
||||||
|
reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`)
|
||||||
|
} else {
|
||||||
|
score += 10
|
||||||
|
reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADX check (trend strength: want >18)
|
||||||
|
if (params.adx > 0) {
|
||||||
|
if (params.adx > 25) {
|
||||||
|
score += 15
|
||||||
|
reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`)
|
||||||
|
} else if (params.adx < 18) {
|
||||||
|
score -= 15
|
||||||
|
reasons.push(`Weak trend (ADX ${params.adx.toFixed(1)})`)
|
||||||
|
} else {
|
||||||
|
score += 5
|
||||||
|
reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSI check (momentum confirmation)
|
||||||
|
if (params.rsi > 0) {
|
||||||
|
if (params.direction === 'long') {
|
||||||
|
if (params.rsi > 50 && params.rsi < 70) {
|
||||||
|
score += 10
|
||||||
|
reasons.push(`RSI supports long (${params.rsi.toFixed(1)})`)
|
||||||
|
} else if (params.rsi > 70) {
|
||||||
|
score -= 10
|
||||||
|
reasons.push(`RSI overbought (${params.rsi.toFixed(1)})`)
|
||||||
|
}
|
||||||
|
} else { // short
|
||||||
|
if (params.rsi < 50 && params.rsi > 30) {
|
||||||
|
score += 10
|
||||||
|
reasons.push(`RSI supports short (${params.rsi.toFixed(1)})`)
|
||||||
|
} else if (params.rsi < 30) {
|
||||||
|
score -= 10
|
||||||
|
reasons.push(`RSI oversold (${params.rsi.toFixed(1)})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume check (want > 1.0 = above average)
|
||||||
|
if (params.volumeRatio > 0) {
|
||||||
|
if (params.volumeRatio > 1.2) {
|
||||||
|
score += 10
|
||||||
|
reasons.push(`Strong volume (${params.volumeRatio.toFixed(2)}x avg)`)
|
||||||
|
} else if (params.volumeRatio < 0.8) {
|
||||||
|
score -= 10
|
||||||
|
reasons.push(`Weak volume (${params.volumeRatio.toFixed(2)}x avg)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price position check (avoid chasing)
|
||||||
|
if (params.pricePosition > 0) {
|
||||||
|
if (params.direction === 'long' && params.pricePosition > 90) {
|
||||||
|
score -= 15
|
||||||
|
reasons.push(`Price near top of range (${params.pricePosition.toFixed(0)}%) - risky long`)
|
||||||
|
} else if (params.direction === 'short' && params.pricePosition < 10) {
|
||||||
|
score -= 15
|
||||||
|
reasons.push(`Price near bottom of range (${params.pricePosition.toFixed(0)}%) - risky short`)
|
||||||
|
} else {
|
||||||
|
score += 5
|
||||||
|
reasons.push(`Price position OK (${params.pricePosition.toFixed(0)}%)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const minScore = 60 // Require 60+ to pass
|
||||||
|
return {
|
||||||
|
passed: score >= minScore,
|
||||||
|
score,
|
||||||
|
reasons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ export interface ExecuteTradeRequest {
|
|||||||
timeframe: string // e.g., '5'
|
timeframe: string // e.g., '5'
|
||||||
signalStrength?: 'strong' | 'moderate' | 'weak'
|
signalStrength?: 'strong' | 'moderate' | 'weak'
|
||||||
signalPrice?: number
|
signalPrice?: number
|
||||||
|
// Context metrics from TradingView
|
||||||
|
atr?: number
|
||||||
|
adx?: number
|
||||||
|
rsi?: number
|
||||||
|
volumeRatio?: number
|
||||||
|
pricePosition?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteTradeResponse {
|
export interface ExecuteTradeResponse {
|
||||||
@@ -360,6 +366,12 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
// Market context
|
// Market context
|
||||||
expectedEntryPrice,
|
expectedEntryPrice,
|
||||||
fundingRateAtEntry,
|
fundingRateAtEntry,
|
||||||
|
// Context metrics from TradingView
|
||||||
|
atrAtEntry: body.atr,
|
||||||
|
adxAtEntry: body.adx,
|
||||||
|
rsiAtEntry: body.rsi,
|
||||||
|
volumeAtEntry: body.volumeRatio,
|
||||||
|
pricePositionAtEntry: body.pricePosition,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('💾 Trade saved to database')
|
console.log('💾 Trade saved to database')
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ export interface CreateTradeParams {
|
|||||||
fundingRateAtEntry?: number
|
fundingRateAtEntry?: number
|
||||||
atrAtEntry?: number
|
atrAtEntry?: number
|
||||||
adxAtEntry?: number
|
adxAtEntry?: number
|
||||||
|
rsiAtEntry?: number
|
||||||
volumeAtEntry?: number
|
volumeAtEntry?: number
|
||||||
|
pricePositionAtEntry?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTradeStateParams {
|
export interface UpdateTradeStateParams {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Trade" ADD COLUMN "pricePositionAtEntry" DOUBLE PRECISION,
|
||||||
|
ADD COLUMN "rsiAtEntry" DOUBLE PRECISION;
|
||||||
@@ -72,7 +72,9 @@ model Trade {
|
|||||||
// Market context at entry
|
// Market context at entry
|
||||||
atrAtEntry Float? // ATR% when trade opened
|
atrAtEntry Float? // ATR% when trade opened
|
||||||
adxAtEntry Float? // ADX trend strength (0-50)
|
adxAtEntry Float? // ADX trend strength (0-50)
|
||||||
|
rsiAtEntry Float? // RSI momentum (0-100)
|
||||||
volumeAtEntry Float? // Volume relative to MA
|
volumeAtEntry Float? // Volume relative to MA
|
||||||
|
pricePositionAtEntry Float? // Price position in range (0-100%)
|
||||||
fundingRateAtEntry Float? // Perp funding rate at entry
|
fundingRateAtEntry Float? // Perp funding rate at entry
|
||||||
basisAtEntry Float? // Perp-spot basis at entry
|
basisAtEntry Float? // Perp-spot basis at entry
|
||||||
|
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "TradingView → Trading Bot v4",
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"httpMethod": "POST",
|
|
||||||
"path": "tradingview-bot-v4",
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"id": "webhook-node",
|
|
||||||
"name": "Webhook",
|
|
||||||
"type": "n8n-nodes-base.webhook",
|
|
||||||
"typeVersion": 1,
|
|
||||||
"position": [240, 400],
|
|
||||||
"webhookId": "tradingview-bot-v4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"fields": {
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"name": "rawMessage",
|
|
||||||
"stringValue": "={{ $json.body }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "symbol",
|
|
||||||
"stringValue": "={{ $json.body.match(/\\bSOL\\b/i) ? 'SOL-PERP' : ($json.body.match(/\\bBTC\\b/i) ? 'BTC-PERP' : ($json.body.match(/\\bETH\\b/i) ? 'ETH-PERP' : 'SOL-PERP')) }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "direction",
|
|
||||||
"stringValue": "={{ $json.body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long' }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "timeframe",
|
|
||||||
"stringValue": "={{ $json.body.match(/\\.P\\s+(\\d+)/)?.[1] || '15' }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"id": "parse-fields",
|
|
||||||
"name": "Parse Signal",
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"typeVersion": 3.2,
|
|
||||||
"position": [440, 400]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"conditions": {
|
|
||||||
"string": [
|
|
||||||
{
|
|
||||||
"value1": "={{ $json.timeframe }}",
|
|
||||||
"operation": "equals",
|
|
||||||
"value2": "15"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "timeframe-filter",
|
|
||||||
"name": "15min Chart Only?",
|
|
||||||
"type": "n8n-nodes-base.if",
|
|
||||||
"typeVersion": 1,
|
|
||||||
"position": [540, 400]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"method": "POST",
|
|
||||||
"url": "http://10.0.0.48:3001/api/trading/check-risk",
|
|
||||||
"authentication": "genericCredentialType",
|
|
||||||
"genericAuthType": "httpHeaderAuth",
|
|
||||||
"sendHeaders": true,
|
|
||||||
"headerParameters": {
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"value": "Bearer 2a344f0149442c857fb56c038c0c7d1b113883b830bec792c76f1e0efa15d6bb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sendBody": true,
|
|
||||||
"specifyBody": "json",
|
|
||||||
"jsonBody": "={\n \"symbol\": \"{{ $json.symbol }}\",\n \"direction\": \"{{ $json.direction }}\"\n}",
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"id": "check-risk",
|
|
||||||
"name": "Check Risk",
|
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
|
||||||
"typeVersion": 4,
|
|
||||||
"position": [740, 400]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"conditions": {
|
|
||||||
"boolean": [
|
|
||||||
{
|
|
||||||
"value1": "={{ $json.allowed }}",
|
|
||||||
"value2": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "risk-if",
|
|
||||||
"name": "Risk Passed?",
|
|
||||||
"type": "n8n-nodes-base.if",
|
|
||||||
"typeVersion": 1,
|
|
||||||
"position": [940, 400]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"method": "POST",
|
|
||||||
"url": "http://10.0.0.48:3001/api/trading/execute",
|
|
||||||
"authentication": "genericCredentialType",
|
|
||||||
"genericAuthType": "httpHeaderAuth",
|
|
||||||
"sendHeaders": true,
|
|
||||||
"headerParameters": {
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "Authorization",
|
|
||||||
"value": "Bearer 2a344f0149442c857fb56c038c0c7d1b113883b830bec792c76f1e0efa15d6bb"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sendBody": true,
|
|
||||||
"specifyBody": "json",
|
|
||||||
"jsonBody": "={\n \"symbol\": \"{{ $('Parse Signal').item.json.symbol }}\",\n \"direction\": \"{{ $('Parse Signal').item.json.direction }}\",\n \"timeframe\": \"{{ $('Parse Signal').item.json.timeframe }}\",\n \"signalStrength\": \"strong\"\n}",
|
|
||||||
"options": {
|
|
||||||
"timeout": 30000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "execute-trade",
|
|
||||||
"name": "Execute Trade",
|
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
|
||||||
"typeVersion": 4,
|
|
||||||
"position": [1040, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"conditions": {
|
|
||||||
"boolean": [
|
|
||||||
{
|
|
||||||
"value1": "={{ $json.success }}",
|
|
||||||
"value2": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "trade-if",
|
|
||||||
"name": "Trade Success?",
|
|
||||||
"type": "n8n-nodes-base.if",
|
|
||||||
"typeVersion": 1,
|
|
||||||
"position": [1240, 300]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"fields": {
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"stringValue": "🟢 TRADE OPENED\\n\\n{{ $('Parse Signal').item.json.rawMessage }}\\n\\n📊 Symbol: {{ $('Parse Signal').item.json.symbol }}\\n📈 Direction: {{ $('Parse Signal').item.json.direction }}\\n⏰ {{ $now.toFormat('HH:mm') }}\\n\\n✅ Position monitored automatically"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"id": "format-success",
|
|
||||||
"name": "Format Success",
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"typeVersion": 3.2,
|
|
||||||
"position": [1440, 200]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"fields": {
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"stringValue": "🔴 TRADE FAILED\\n\\n{{ $('Parse Signal').item.json.rawMessage }}\\n\\n❌ Error: {{ $json.error || $json.message }}\\n⏰ {{ $now.toFormat('HH:mm') }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"id": "format-error",
|
|
||||||
"name": "Format Error",
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"typeVersion": 3.2,
|
|
||||||
"position": [1440, 400]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"fields": {
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"stringValue": "⚠️ TRADE BLOCKED\\n\\n{{ $('Parse Signal').item.json.rawMessage }}\\n\\n🛑 Risk limits exceeded\\n⏰ {{ $now.toFormat('HH:mm') }}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
"id": "format-risk",
|
|
||||||
"name": "Format Risk",
|
|
||||||
"type": "n8n-nodes-base.set",
|
|
||||||
"typeVersion": 3.2,
|
|
||||||
"position": [1040, 500]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"chatId": "579304651",
|
|
||||||
"text": "={{ $json.message }}",
|
|
||||||
"additionalFields": {
|
|
||||||
"appendAttribution": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "telegram-success",
|
|
||||||
"name": "Telegram Success",
|
|
||||||
"type": "n8n-nodes-base.telegram",
|
|
||||||
"typeVersion": 1.1,
|
|
||||||
"position": [1640, 200],
|
|
||||||
"credentials": {
|
|
||||||
"telegramApi": {
|
|
||||||
"id": "Csk5cg4HtaSqP5jJ",
|
|
||||||
"name": "Telegram account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"chatId": "579304651",
|
|
||||||
"text": "={{ $json.message }}",
|
|
||||||
"additionalFields": {
|
|
||||||
"appendAttribution": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "telegram-error",
|
|
||||||
"name": "Telegram Error",
|
|
||||||
"type": "n8n-nodes-base.telegram",
|
|
||||||
"typeVersion": 1.1,
|
|
||||||
"position": [1640, 400],
|
|
||||||
"credentials": {
|
|
||||||
"telegramApi": {
|
|
||||||
"id": "Csk5cg4HtaSqP5jJ",
|
|
||||||
"name": "Telegram account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameters": {
|
|
||||||
"chatId": "579304651",
|
|
||||||
"text": "={{ $json.message }}",
|
|
||||||
"additionalFields": {
|
|
||||||
"appendAttribution": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"id": "telegram-risk",
|
|
||||||
"name": "Telegram Risk",
|
|
||||||
"type": "n8n-nodes-base.telegram",
|
|
||||||
"typeVersion": 1.1,
|
|
||||||
"position": [1240, 500],
|
|
||||||
"credentials": {
|
|
||||||
"telegramApi": {
|
|
||||||
"id": "Csk5cg4HtaSqP5jJ",
|
|
||||||
"name": "Telegram account"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"connections": {
|
|
||||||
"Webhook": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Parse Signal",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Parse Signal": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Check Risk",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Check Risk": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Risk Passed?",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Risk Passed?": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Execute Trade",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Format Risk",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Execute Trade": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Trade Success?",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Trade Success?": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Format Success",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Format Error",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Format Success": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Telegram Success",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Format Error": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Telegram Error",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Format Risk": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Telegram Risk",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pinData": {},
|
|
||||||
"active": false,
|
|
||||||
"settings": {
|
|
||||||
"executionOrder": "v1"
|
|
||||||
},
|
|
||||||
"versionId": "1",
|
|
||||||
"tags": []
|
|
||||||
}
|
|
||||||
24
workflows/trading/parse_signal_enhanced.json
Normal file
24
workflows/trading/parse_signal_enhanced.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "Parse Signal Enhanced",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "// Get the body - it might be a string or nested in an object\nlet body = $json.body || $json.query?.body || JSON.stringify($json);\n\n// If body is an object, stringify it\nif (typeof body === 'object') {\n body = JSON.stringify(body);\n}\n\n// Parse basic signal (existing logic)\nconst symbolMatch = body.match(/\\b(SOL|BTC|ETH)\\b/i);\nconst symbol = symbolMatch ? symbolMatch[1].toUpperCase() + '-PERP' : 'SOL-PERP';\n\nconst direction = body.match(/\\b(sell|short)\\b/i) ? 'short' : 'long';\n\nconst timeframeMatch = body.match(/\\.P\\s+(\\d+)/);\nconst timeframe = timeframeMatch ? timeframeMatch[1] : '15';\n\n// Parse new context metrics from enhanced format:\n// \"SOL buy .P 15 | ATR:1.85 | ADX:28.3 | RSI:62.5 | VOL:1.45 | POS:75.3\"\nconst atrMatch = body.match(/ATR:([\\d.]+)/);\nconst atr = atrMatch ? parseFloat(atrMatch[1]) : 0;\n\nconst adxMatch = body.match(/ADX:([\\d.]+)/);\nconst adx = adxMatch ? parseFloat(adxMatch[1]) : 0;\n\nconst rsiMatch = body.match(/RSI:([\\d.]+)/);\nconst rsi = rsiMatch ? parseFloat(rsiMatch[1]) : 0;\n\nconst volumeMatch = body.match(/VOL:([\\d.]+)/);\nconst volumeRatio = volumeMatch ? parseFloat(volumeMatch[1]) : 0;\n\nconst pricePositionMatch = body.match(/POS:([\\d.]+)/);\nconst pricePosition = pricePositionMatch ? parseFloat(pricePositionMatch[1]) : 0;\n\nreturn {\n rawMessage: body,\n symbol,\n direction,\n timeframe,\n // New context fields\n atr,\n adx,\n rsi,\n volumeRatio,\n pricePosition\n};"
|
||||||
|
},
|
||||||
|
"id": "parse-signal-enhanced",
|
||||||
|
"name": "Parse Signal Enhanced",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [600, 300]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"staticData": null,
|
||||||
|
"tags": [],
|
||||||
|
"triggerCount": 0,
|
||||||
|
"updatedAt": "2025-10-30T00:00:00.000Z",
|
||||||
|
"versionId": "1"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user