feat: Add signalSource field to identify manual vs TradingView trades

- Set signalSource='manual' for Telegram trades, 'tradingview' for TradingView
- Updated analytics queries to exclude manual trades from indicator analysis
- getTradingStats() filters manual trades (TradingView performance only)
- Version comparison endpoint filters manual trades
- Created comprehensive filtering guide: docs/MANUAL_TRADE_FILTERING.md
- Ensures clean data for indicator optimization without contamination
This commit is contained in:
mindesbunister
2025-11-14 22:55:14 +01:00
parent 3f6fee7e1a
commit 25776413d0
4 changed files with 238 additions and 0 deletions

View File

@@ -56,6 +56,7 @@ export async function GET() {
WHERE "exitReason" IS NOT NULL
AND "exitReason" NOT LIKE '%CLEANUP%'
AND "isTestTrade" = false
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
GROUP BY "indicatorVersion"
ORDER BY version DESC
`
@@ -78,6 +79,7 @@ export async function GET() {
WHERE "exitReason" IS NOT NULL
AND "exitReason" NOT LIKE '%CLEANUP%'
AND "isTestTrade" = false
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
AND "pricePositionAtEntry" < 15
GROUP BY "indicatorVersion"
ORDER BY version DESC

View File

@@ -650,6 +650,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
hardStopOrderTx: config.useDualStops ? exitOrderSignatures[3] : undefined,
softStopPrice,
hardStopPrice,
signalSource: body.timeframe === 'manual' ? 'manual' : 'tradingview', // Identify manual Telegram trades
signalStrength: body.signalStrength,
timeframe: body.timeframe,
// Context metrics from TradingView

View File

@@ -0,0 +1,231 @@
# Manual Trade Filtering Guide
**Date:** November 14, 2025
**Purpose:** Ensure TradingView indicator analysis excludes manual Telegram trades
## Overview
Manual trades entered via Telegram bot use preset metrics and should **NOT** be included in TradingView indicator performance analysis. Including them would contaminate the data and make indicator optimization impossible.
## Database Fields
Trades from different sources are identified by:
```sql
signalSource VARCHAR -- 'tradingview', 'manual', or NULL (old trades)
timeframe VARCHAR -- 'manual' for Telegram trades, '5'/'15'/'60' for TradingView
isTestTrade BOOLEAN -- Test trades from settings UI
```
## Filtering Pattern
**ALWAYS use this WHERE clause for indicator analysis:**
```sql
WHERE
"isTestTrade" = false -- Exclude test trades
AND ("signalSource" IS NULL OR "signalSource" != 'manual') -- Exclude manual trades
AND "exitReason" IS NOT NULL -- Closed trades only
AND "exitReason" NOT LIKE '%CLEANUP%' -- Exclude cleanup trades
```
## Common Analysis Queries (CORRECT)
### Win Rate by Indicator Version
```sql
SELECT
COALESCE("indicatorVersion", 'unknown') as version,
COUNT(*) as total_trades,
SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) as win_rate,
ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl,
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl
FROM "Trade"
WHERE
"isTestTrade" = false
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
AND "exitReason" IS NOT NULL
AND "exitReason" NOT LIKE '%CLEANUP%'
GROUP BY "indicatorVersion"
ORDER BY version DESC;
```
### Signal Quality Score Distribution (TradingView Only)
```sql
SELECT
CASE
WHEN "signalQualityScore" >= 70 THEN '70-79'
WHEN "signalQualityScore" >= 65 THEN '65-69'
WHEN "signalQualityScore" >= 60 THEN '60-64'
ELSE '<60'
END as score_tier,
COUNT(*) as trades,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) as win_rate,
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl
FROM "Trade"
WHERE
"isTestTrade" = false
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
AND "exitReason" IS NOT NULL
AND "signalQualityScore" IS NOT NULL
GROUP BY score_tier
ORDER BY MIN("signalQualityScore") DESC;
```
### Extreme Position Analysis (TradingView Only)
```sql
-- Trades at range extremes (< 15% price position)
SELECT
"direction",
COUNT(*) as trades,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) as win_rate,
ROUND(AVG("adxAtEntry")::numeric, 1) as avg_adx,
ROUND(AVG("pricePositionAtEntry")::numeric, 1) as avg_price_pos,
ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl
FROM "Trade"
WHERE
"isTestTrade" = false
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
AND "exitReason" IS NOT NULL
AND "pricePositionAtEntry" < 15
GROUP BY "direction";
```
### Timeframe Performance (TradingView Only)
```sql
SELECT
"timeframe",
COUNT(*) as trades,
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*), 1) as win_rate,
ROUND(AVG("signalQualityScore")::numeric, 1) as avg_score,
ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl
FROM "Trade"
WHERE
"isTestTrade" = false
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
AND "exitReason" IS NOT NULL
AND "timeframe" IS NOT NULL
GROUP BY "timeframe"
ORDER BY timeframe;
```
## Queries INCLUDING Manual Trades
Sometimes you DO want to see manual trades (e.g., overall account performance):
```sql
-- ALL trades (manual + TradingView)
SELECT
"signalSource",
COUNT(*) as trades,
ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl
FROM "Trade"
WHERE
"isTestTrade" = false
AND "exitReason" IS NOT NULL
GROUP BY "signalSource"
ORDER BY "signalSource";
```
Output:
```
signalSource | trades | total_pnl
-------------|--------|----------
tradingview | 150 | +45.23
manual | 11 | -12.56
(null) | 10 | +8.90 (old trades before signalSource added)
```
## Application Code
### TypeScript/Prisma Queries
```typescript
// TradingView indicator analysis only
const trades = await prisma.trade.findMany({
where: {
isTestTrade: false,
OR: [
{ signalSource: null }, // Old trades
{ signalSource: { not: 'manual' } }, // Exclude Telegram
],
exitReason: { not: null },
NOT: {
exitReason: { contains: 'CLEANUP' },
},
},
})
// All real trades (including manual)
const allTrades = await prisma.trade.findMany({
where: {
isTestTrade: false,
exitReason: { not: null },
},
})
```
## Migration for Old Trades
Old trades don't have `signalSource` set (NULL). The filter `OR signalSource IS NULL` includes them in TradingView analysis, which is correct because they came from TradingView before the field existed.
To backfill old trades:
```sql
-- Set signalSource for old trades (if timeframe is known)
UPDATE "Trade"
SET "signalSource" = CASE
WHEN "timeframe" = 'manual' THEN 'manual'
WHEN "timeframe" IS NOT NULL AND "timeframe" != 'manual' THEN 'tradingview'
ELSE 'tradingview' -- Default assumption for old trades
END
WHERE "signalSource" IS NULL;
```
**Note:** Only run this if you want to clean up old data. Current filter handles NULL correctly.
## Implementation Status
**Updated files (Nov 14, 2025):**
-`app/api/trading/execute/route.ts` - Sets `signalSource` on new trades
-`app/api/analytics/version-comparison/route.ts` - Filters manual trades in SQL
-`lib/database/views.ts` - Filters manual trades in `getTradingStats()`
**Future updates needed:**
- Analytics dashboard frontend (if it queries trades directly)
- Any custom SQL analysis scripts in `scripts/` directory
- Performance analysis queries in optimization roadmaps
## Testing
Verify filtering works:
```sql
-- Should return 0 (no manual trades in TradingView analysis)
SELECT COUNT(*)
FROM "Trade"
WHERE
"isTestTrade" = false
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
AND "exitReason" IS NOT NULL
AND "signalSource" = 'manual';
```
Expected result: `0` (manual trades correctly excluded)
## Summary
**Rule of thumb:**
- **Indicator optimization:** Exclude manual trades (use filter above)
- **Account performance:** Include all real trades (exclude only `isTestTrade`)
- **Position sizing analysis:** Can include manual trades (they use same TP/SL system)
**When in doubt:** Use the standard filter shown at the top of this document.
---
*This filtering ensures clean data for TradingView indicator analysis while preserving manual trades for account-level reporting.*

View File

@@ -143,6 +143,10 @@ export async function getTradingStats(days: number = 30) {
createdAt: { gte: since },
status: 'closed',
isTestTrade: false, // Real trades only
OR: [
{ signalSource: null }, // Old trades without signalSource
{ signalSource: { not: 'manual' } }, // Exclude manual Telegram trades
],
},
})