diff --git a/INDICATOR_V9_MA_GAP_ROADMAP.md b/INDICATOR_V9_MA_GAP_ROADMAP.md new file mode 100644 index 0000000..cefa389 --- /dev/null +++ b/INDICATOR_V9_MA_GAP_ROADMAP.md @@ -0,0 +1,332 @@ +# Indicator v9: MA Gap Quality Enhancement + +**Status:** πŸ“‹ PLANNED +**Priority:** HIGH (addresses $380 missed profit from Nov 25 blocked signal) +**Motivation:** v8 indicator catches trend changes BEFORE classic MA crossovers, but quality filter blocks these early signals + +--- + +## Problem Statement + +**Real Incident (Nov 25, 2025 21:15 UTC):** +- v8 generated LONG signal at $136.91 (quality 75, ADX 17.9) +- Signal BLOCKED by quality threshold (75 < 90 required for LONGs) +- Chart showed 50 MA converging toward 200 MA (gap β‰ˆ -1% to 0%) +- Golden cross occurred a few bars AFTER entry signal +- Price moved to $142+ = **$380 missed profit** (~4% move) + +**Key Insight:** +- v8 indicator is **BETTER** than MA crossover timing - catches moves earlier +- BUT quality filter doesn't recognize when MAs are positioned for breakout +- Need to reward MA convergence/proximity, not just crossover events + +--- + +## v9 Enhancement: MA Gap Analysis + +### Core Concept + +Instead of detecting exact crossover moment (lagging), measure **MA gap percentage**: +- **Tight gap** (0-2%) = Strong trend with momentum or imminent crossover +- **Converging gap** (-2% to 0%) = Potential golden cross brewing +- **Wide gap** (>2%) = Established trend, less explosive but stable + +### TradingView Indicator Changes + +**Add after context metrics calculation (~line 221):** + +```pinescript +// ═══════════════════════════════════════════════════════════ +// 🎯 MA GAP ANALYSIS (v9 - for quality scoring) +// ═══════════════════════════════════════════════════════════ + +// Calculate 50 and 200 period moving averages +ma50 = ta.sma(calcC, 50) +ma200 = ta.sma(calcC, 200) + +// Calculate MA gap as percentage (negative = 50 below 200) +maGap = ((ma50 - ma200) / ma200) * 100 + +// Detect convergence (MAs getting closer = potential crossover) +maConverging = math.abs(maGap) < 2.0 // Within 2% = tight squeeze + +// Current alignment +maAlignmentBullish = ma50 > ma200 + +// Optional: Plot MAs on chart for visual confirmation +plot(ma50, title="MA 50", color=color.yellow, linewidth=1) +plot(ma200, title="MA 200", color=color.red, linewidth=1) +``` + +**Update alert messages (~lines 257-258):** + +```pinescript +longAlertMsg = baseCurrency + " buy " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:v9" + +shortAlertMsg = baseCurrency + " sell " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:v9" +``` + +--- + +## Backend Quality Scoring Changes + +### Update scoreSignalQuality() function + +**File:** `lib/trading/signal-quality.ts` + +**Add parameters:** +```typescript +export async function scoreSignalQuality(params: { + // ... existing params ... + maGap?: number // NEW: % gap between 50 and 200 MA + maAlignmentBullish?: boolean // NEW: is 50 above 200? +}): Promise { +``` + +**Add scoring logic (after existing metrics):** + +```typescript +// ═══════════════════════════════════════════════════════════ +// MA Gap Analysis (v9 - Nov 26, 2025) +// ═══════════════════════════════════════════════════════════ + +if (params.maGap !== undefined) { + if (params.direction === 'long') { + // LONG scenarios + if (params.maGap >= 0 && params.maGap < 2.0) { + // 50 MA above 200 MA, tight gap (0-2%) + // = Bullish trend with momentum OR fresh golden cross + score += 15 + reasons.push(`🎯 MA bullish + tight gap (${params.maGap.toFixed(2)}%) = strong momentum (+15 pts)`) + + } else if (params.maGap < 0 && params.maGap > -2.0) { + // 50 MA below 200 MA but converging (-2% to 0%) + // = Potential golden cross brewing (early detection like Nov 25 signal!) + score += 12 + reasons.push(`🌟 MA converging (${params.maGap.toFixed(2)}%) = golden cross potential (+12 pts)`) + + } else if (params.maGap >= 2.0 && params.maGap < 5.0) { + // 50 MA well above 200 MA (2-5%) + // = Established bullish trend, stable but less explosive + score += 8 + reasons.push(`πŸ“ˆ MA strong bullish trend (${params.maGap.toFixed(2)}%) (+8 pts)`) + + } else if (params.maGap >= 5.0) { + // 50 MA far above 200 MA (>5%) + // = Very extended, potential exhaustion + score += 5 + reasons.push(`⚠️ MA extended bullish (${params.maGap.toFixed(2)}%) = overbought risk (+5 pts)`) + + } else if (params.maGap <= -2.0) { + // 50 MA significantly below 200 MA + // = Bearish trend, LONG signal is counter-trend (risky) + score -= 10 + reasons.push(`❌ MA bearish divergence (${params.maGap.toFixed(2)}%) = counter-trend risk (-10 pts)`) + } + + } else if (params.direction === 'short') { + // SHORT scenarios (inverse of LONG logic) + if (params.maGap <= 0 && params.maGap > -2.0) { + // 50 MA below 200 MA, tight gap (-2% to 0%) + // = Bearish trend with momentum OR fresh death cross + score += 15 + reasons.push(`🎯 MA bearish + tight gap (${params.maGap.toFixed(2)}%) = strong momentum (+15 pts)`) + + } else if (params.maGap > 0 && params.maGap < 2.0) { + // 50 MA above 200 MA but converging (0% to 2%) + // = Potential death cross brewing + score += 12 + reasons.push(`🌟 MA converging (${params.maGap.toFixed(2)}%) = death cross potential (+12 pts)`) + + } else if (params.maGap <= -2.0 && params.maGap > -5.0) { + // 50 MA well below 200 MA (-5% to -2%) + // = Established bearish trend + score += 8 + reasons.push(`πŸ“‰ MA strong bearish trend (${params.maGap.toFixed(2)}%) (+8 pts)`) + + } else if (params.maGap <= -5.0) { + // 50 MA far below 200 MA (<-5%) + // = Very extended, potential bounce risk + score += 5 + reasons.push(`⚠️ MA extended bearish (${params.maGap.toFixed(2)}%) = oversold risk (+5 pts)`) + + } else if (params.maGap >= 2.0) { + // 50 MA significantly above 200 MA + // = Bullish trend, SHORT signal is counter-trend (risky) + score -= 10 + reasons.push(`❌ MA bullish divergence (${params.maGap.toFixed(2)}%) = counter-trend risk (-10 pts)`) + } + } +} +``` + +--- + +## Expected Impact + +### Nov 25 21:15 Signal Reanalysis + +**Original (v8):** +- Quality: 75 (blocked) +- ADX: 17.9 (weak) +- MA Gap: β‰ˆ -1.0% (50 below 200, converging) + +**With v9 MA Gap Enhancement:** +- Base quality: 75 +- MA converging bonus: +12 +- **New quality: 87** (closer but still blocked) + +**Note:** Would still need ADX improvement OR slightly lower threshold (88-89?) to catch this specific signal. But the +12 points get us much closer and would catch signals with ADX 18-20. + +### Alternative Scenarios + +**Scenario A: MA Gap -0.5% (very tight convergence)** +- Quality 75 + 12 = **87** (close) + +**Scenario B: MA Gap +0.5% (just crossed, tight)** +- Quality 75 + 15 = **90** βœ… **PASSES!** + +**Scenario C: MA Gap +1.8% (recent golden cross, momentum strong)** +- Quality 75 + 15 = **90** βœ… **PASSES!** + +--- + +## Implementation Checklist + +### Phase 1: TradingView Indicator +- [ ] Add MA50 and MA200 calculations +- [ ] Calculate maGap percentage +- [ ] Add maGap to alert message payload +- [ ] Optional: Plot MA lines on chart +- [ ] Update indicatorVer from "v8" to "v9" +- [ ] Test on SOL-PERP 5min chart + +### Phase 2: Backend Integration +- [ ] Update TypeScript interfaces for maGap parameter +- [ ] Modify scoreSignalQuality() with MA gap logic +- [ ] Update check-risk endpoint to accept maGap +- [ ] Update execute endpoint to accept maGap +- [ ] Add maGap to database fields (Trade table, BlockedSignal table) + +### Phase 3: Testing & Validation +- [ ] Deploy v9 indicator to TradingView +- [ ] Trigger test signals manually +- [ ] Verify maGap calculation matches chart visual +- [ ] Check quality scores increase appropriately +- [ ] Monitor first 20-30 signals for validation + +### Phase 4: Data Collection +- [ ] Collect 50+ v9 signals +- [ ] Compare v8 vs v9 win rates +- [ ] Analyze: Did MA gap bonus catch missed winners? +- [ ] SQL queries to validate improvement +- [ ] Adjust bonus points if needed (12/15 β†’ 10/12 or 15/18) + +--- + +## Success Metrics + +**Target Improvements:** +1. **Catch signals like Nov 25 21:15:** Quality 75 + MA converging β†’ 87-90 range +2. **Reduce false negatives:** Fewer blocked signals that would have been winners +3. **Maintain safety:** Don't add too many low-quality signals +4. **Win rate:** v9 should maintain or improve v8's 57.1% WR + +**Validation Queries:** + +```sql +-- Compare v9 MA gap bonus impact +SELECT + CASE + WHEN "signalQualityScore" >= 90 THEN 'Passed' + WHEN "signalQualityScore" >= 80 THEN 'Close (80-89)' + ELSE 'Blocked (<80)' + END as category, + COUNT(*) as signals, + AVG("scoreBreakdown"->>'maGap')::numeric as avg_ma_gap +FROM "BlockedSignal" +WHERE "indicatorVersion" = 'v9' + AND "scoreBreakdown"->>'maGap' IS NOT NULL +GROUP BY category; + +-- Find signals that would pass with v9 but were blocked in v8 +SELECT + TO_CHAR("createdAt", 'MM-DD HH24:MI') as time, + symbol, direction, + "signalQualityScore" as original_score, + -- Simulate v9 score (add 12 for converging, 15 for tight) + CASE + WHEN ("scoreBreakdown"->>'maGap')::numeric >= 0 AND ("scoreBreakdown"->>'maGap')::numeric < 2.0 + THEN "signalQualityScore" + 15 + WHEN ("scoreBreakdown"->>'maGap')::numeric < 0 AND ("scoreBreakdown"->>'maGap')::numeric > -2.0 + THEN "signalQualityScore" + 12 + ELSE "signalQualityScore" + END as v9_score, + "blockReason" +FROM "BlockedSignal" +WHERE "signalQualityScore" < 90 -- Was blocked + AND "indicatorVersion" = 'v8' +ORDER BY "createdAt" DESC +LIMIT 20; +``` + +--- + +## Risk Mitigation + +### Potential Issues + +1. **MA calculation lag:** 200-period MA requires significant history + - **Mitigation:** TradingView has full history, no issue + +2. **Whipsaw during sideways markets:** MAs converge often in chop + - **Mitigation:** ADX filter still applies (weak ADX = less bonus effect) + +3. **Over-optimization on single signal:** Nov 25 may be outlier + - **Mitigation:** Collect 50+ v9 signals before final judgment + +4. **Bonus points too generous:** Could inflate scores artificially + - **Mitigation:** Start conservative (12/15), adjust based on data + +### Rollback Plan + +If v9 performs worse than v8: +1. Revert TradingView indicator to v8 +2. Keep backend code but disable MA gap bonus +3. Analyze what went wrong (false positives? whipsaw signals?) +4. Redesign MA gap logic with tighter conditions + +--- + +## Timeline + +**Estimated Implementation Time:** +- TradingView changes: 30 minutes +- Backend integration: 1 hour +- Testing & deployment: 30 minutes +- **Total: ~2 hours** + +**Data Collection:** +- Minimum 50 signals: 2-3 weeks (at ~3-5 signals/day) +- Comparative analysis: 1 week after 50 signals + +**Decision Point:** +- After 50 v9 signals: Keep, adjust, or revert based on performance data + +--- + +## Notes + +- This enhancement preserves v8's early detection advantage +- Adds context awareness of MA positioning +- Rewards both imminent crossovers (converging) AND fresh crossovers (tight gap) +- Balances explosive potential (tight gaps) with trend stability (wider gaps) +- Counter-trend penalties prevent chasing wrong direction + +**Key Insight:** v8 catches momentum shifts BEFORE visible MA crossovers. v9 validates those shifts by checking if MA structure supports the move. + +--- + +**Created:** Nov 26, 2025 +**Motivation:** $380 missed profit from Nov 25 21:15 blocked signal +**Expected Impact:** Catch 15-25% more profitable signals while maintaining quality standards diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index bb1db9d..d419cd1 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -94,6 +94,7 @@ async function shouldAllowScaling( rsi: newSignal.rsi || 50, volumeRatio: newSignal.volumeRatio || 1, pricePosition: newSignal.pricePosition, + maGap: newSignal.maGap, // V9: MA gap convergence scoring direction: newSignal.direction, symbol: newSignal.symbol, currentPrice: newSignal.currentPrice, @@ -373,6 +374,7 @@ export async function POST(request: NextRequest): Promise5678) +**External Domain:** https://flow.egonetix.de (currently DNS unavailable) +**API Key:** `n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1` + +**Environment Variable (.env):** +```bash +N8N_API_KEY=n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1 +N8N_API_URL=http://localhost:8098/api/v1 +``` + +--- + +## Common API Operations + +### 1. List All Workflows + +```bash +curl -X GET "http://localhost:8098/api/v1/workflows" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + -H "Accept: application/json" +``` + +**Response Fields:** +- `id` - Workflow ID (use for updates/deletes) +- `name` - Workflow name +- `active` - true/false (is workflow enabled?) +- `nodes` - Array of workflow nodes +- `connections` - Node connections map + +**Current Workflows (as of Nov 26, 2025):** +- `gUDqTiHyHSfRUXv6` - **Money Machine** (Active: true) - Main trading workflow +- `Zk4gbBzjxVppHiCB` - nextcloud deck tf bank (Active: true) +- `l5Bnf1Nh3C2GDcpv` - nextcloud deck gebΓΌhrenfrei mastercard (Active: true) + +### 2. Get Specific Workflow + +```bash +# Get Money Machine workflow details +curl -X GET "http://localhost:8098/api/v1/workflows/gUDqTiHyHSfRUXv6" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + -H "Accept: application/json" +``` + +**Money Machine Workflow Nodes:** +- **Parse Signal Enhanced** - Extracts metrics from TradingView alerts +- **Check Risk1** - Validates signal quality via `/api/trading/check-risk` +- **Execute Trade1** - Opens position via `/api/trading/execute` +- **Trade Success?** - Validation/branching logic + +### 3. Update Workflow + +```bash +curl -X PATCH "http://localhost:8098/api/v1/workflows/{workflow_id}" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Updated Workflow Name", + "active": true, + "nodes": [...], + "connections": {...} + }' +``` + +### 4. Activate/Deactivate Workflow + +```bash +# Activate Money Machine workflow +curl -X PATCH "http://localhost:8098/api/v1/workflows/gUDqTiHyHSfRUXv6" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + -H "Content-Type: application/json" \ + -d '{"active": true}' + +# Deactivate (use for maintenance or testing) +curl -X PATCH "http://localhost:8098/api/v1/workflows/gUDqTiHyHSfRUXv6" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + -H "Content-Type: application/json" \ + -d '{"active": false}' +``` + +### 5. Execute Workflow + +```bash +curl -X POST "https://flow.egonetix.de/api/v1/workflows/{workflow_id}/executions" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + -H "Content-Type: application/json" \ + -d '{ + "data": { + "symbol": "SOL-PERP", + "direction": "long", + "atr": 0.45 + } + }' +``` + +### 6. List Executions + +```bash +curl -X GET "https://flow.egonetix.de/api/v1/executions?workflowId={workflow_id}&limit=10" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + -H "Accept: application/json" +``` + +--- + +## Trading Bot Workflows + +### Key Workflows to Manage + +1. **Parse Signal Enhanced** + - Extracts metrics from TradingView alerts + - Parses: ATR, ADX, RSI, volumeRatio, pricePosition, timeframe, indicator version + - **Future:** Will include maGap for v9 indicator + +2. **Check Risk** + - Calls `/api/trading/check-risk` endpoint + - Validates signal quality score + - Checks duplicate signals, cooldown periods + - Blocks if quality < threshold (LONG: 90, SHORT: 95) + +3. **Execute Trade** + - Calls `/api/trading/execute` endpoint + - Opens position on Drift Protocol + - Places TP/SL orders + - Adds to Position Manager monitoring + +### Updating Workflows for v9 Enhancement + +When implementing v9 MA gap enhancement: + +**Step 1: Update Parse Signal Enhanced node** +```json +{ + "nodes": [ + { + "name": "Parse Signal Enhanced", + "parameters": { + "jsCode": "// Add MA gap parsing\nconst maGapMatch = message.match(/MAGAP:([\\d.-]+)/);\nconst maGap = maGapMatch ? parseFloat(maGapMatch[1]) : undefined;\n\nreturn { maGap };" + } + } + ] +} +``` + +**Step 2: Update HTTP Request nodes** +Add `maGap` to request body: +```json +{ + "body": { + "symbol": "={{$json.symbol}}", + "direction": "={{$json.direction}}", + "atr": "={{$json.atr}}", + "adx": "={{$json.adx}}", + "rsi": "={{$json.rsi}}", + "volumeRatio": "={{$json.volumeRatio}}", + "pricePosition": "={{$json.pricePosition}}", + "maGap": "={{$json.maGap}}", + "timeframe": "={{$json.timeframe}}" + } +} +``` + +--- + +## Workflow IDs Reference + +**Trading Workflow:** +- **ID:** `gUDqTiHyHSfRUXv6` +- **Name:** Money Machine +- **Status:** Active +- **Nodes:** + - Parse Signal Enhanced (extracts TradingView metrics) + - Check Risk1 (validates quality score, duplicates, cooldowns) + - Execute Trade1 (opens position on Drift) + - Trade Success? (validation branching) + +**Quick Commands:** +```bash +# Get Money Machine workflow +curl -s http://localhost:8098/api/v1/workflows/gUDqTiHyHSfRUXv6 \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" + +# List all workflows +curl -s http://localhost:8098/api/v1/workflows \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" \ + | jq -r '.data[] | "\(.id) | \(.name) | Active: \(.active)"' +``` + +--- + +## API Response Examples + +### Successful Workflow List +```json +{ + "data": [ + { + "id": "12345", + "name": "Trading Signal Complete Workflow", + "active": true, + "createdAt": "2025-11-15T12:00:00.000Z", + "updatedAt": "2025-11-20T15:30:00.000Z", + "nodes": [...], + "connections": {...} + } + ] +} +``` + +### Error Responses + +**401 Unauthorized:** +```json +{ + "code": 401, + "message": "Unauthorized" +} +``` +β†’ Check API key is correct + +**404 Not Found:** +```json +{ + "code": 404, + "message": "Workflow not found" +} +``` +β†’ Check workflow ID + +**429 Rate Limited:** +```json +{ + "code": 429, + "message": "Too many requests" +} +``` +β†’ Wait before retrying + +--- + +## Troubleshooting + +### API Connection Issues + +**DNS Resolution Failed:** +```bash +# Test DNS +ping flow.egonetix.de + +# If fails, check: +# 1. n8n instance is running +# 2. Domain DNS is configured +# 3. Server firewall allows access +``` + +**SSL Certificate Issues:** +```bash +# Test with --insecure (dev only!) +curl --insecure -X GET "https://flow.egonetix.de/api/v1/workflows" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" +``` + +**401 Unauthorized:** +- Verify API key is correct (no typos) +- Check API key hasn't been revoked in n8n settings +- Ensure API access is enabled in n8n instance + +--- + +## Security Best Practices + +1. **Store API key in .env file** (never commit to git) +2. **Use environment variables** in scripts: + ```bash + export N8N_API_KEY="n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" + curl -H "X-N8N-API-KEY: $N8N_API_KEY" ... + ``` +3. **Rotate API keys periodically** (quarterly recommended) +4. **Monitor API usage** in n8n admin panel +5. **Restrict API key permissions** if possible (read vs write) + +--- + +## Automated Workflow Management Scripts + +### Check Workflow Status +```bash +#!/bin/bash +# check-n8n-workflows.sh + +API_KEY="n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" +BASE_URL="https://flow.egonetix.de/api/v1" + +echo "πŸ” Checking n8n workflows..." +curl -s -X GET "$BASE_URL/workflows" \ + -H "X-N8N-API-KEY: $API_KEY" \ + | jq -r '.data[] | "[\(if .active then "βœ…" else "❌" end)] \(.name) (ID: \(.id))"' +``` + +### Deploy Workflow Update +```bash +#!/bin/bash +# deploy-workflow-update.sh + +WORKFLOW_ID=$1 +WORKFLOW_FILE=$2 + +if [ -z "$WORKFLOW_ID" ] || [ -z "$WORKFLOW_FILE" ]; then + echo "Usage: ./deploy-workflow-update.sh " + exit 1 +fi + +API_KEY="n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" +BASE_URL="https://flow.egonetix.de/api/v1" + +echo "πŸ“€ Deploying workflow update..." +curl -X PATCH "$BASE_URL/workflows/$WORKFLOW_ID" \ + -H "X-N8N-API-KEY: $API_KEY" \ + -H "Content-Type: application/json" \ + -d @"$WORKFLOW_FILE" \ + | jq '.' +``` + +--- + +## Next Steps After API Test + +Once n8n instance is reachable: + +1. **Test API connection:** + ```bash + curl -X GET "https://flow.egonetix.de/api/v1/workflows" \ + -H "X-N8N-API-KEY: n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1" + ``` + +2. **Document workflow IDs** in this file + +3. **Update .env file** with API key: + ```bash + sed -i 's/N8N_API_KEY=.*/N8N_API_KEY=n8n_api_42f1838c1e2de90cadcb669f78083de92697a85322c0b6009ad2e55760db992ab0bf61515a3cf0e1/' /home/icke/traderv4/.env + ``` + +4. **Backup current workflows:** + ```bash + mkdir -p /home/icke/traderv4/workflows/n8n/backups + # Export each workflow via API + ``` + +5. **Test workflow update** with non-critical workflow first + +--- + +**Status:** βœ… API key verified and working (Nov 26, 2025) +**Instance:** http://localhost:8098 (Docker container n8n) +**Main Workflow:** Money Machine (ID: gUDqTiHyHSfRUXv6) - Active +**Last Test:** Nov 26, 2025 - Successfully listed workflows and retrieved Money Machine details diff --git a/lib/trading/signal-quality.ts b/lib/trading/signal-quality.ts index 20107d8..296fcf9 100644 --- a/lib/trading/signal-quality.ts +++ b/lib/trading/signal-quality.ts @@ -55,6 +55,7 @@ export async function scoreSignalQuality(params: { timeframe?: string // "5" = 5min, "15" = 15min, "60" = 1H, "D" = daily minScore?: number // Configurable minimum score threshold skipFrequencyCheck?: boolean // For testing or when frequency check not needed + maGap?: number // V9: MA gap percentage (MA50-MA200)/MA200*100 }): Promise { let score = 50 // Base score const reasons: string[] = [] @@ -274,6 +275,57 @@ export async function scoreSignalQuality(params: { } } + // V9: MA Gap Analysis (Nov 26, 2025) + // MA convergence/divergence indicates momentum building or fading + // Helps catch early trend signals when MAs align with direction + if (params.maGap !== undefined && params.maGap !== null) { + if (params.direction === 'long') { + if (params.maGap >= 0 && params.maGap < 2.0) { + // Tight bullish convergence (MA50 above MA200, close together) + score += 15 + reasons.push(`βœ… Tight bullish MA convergence (${params.maGap.toFixed(2)}% gap) (+15 pts)`) + } else if (params.maGap < 0 && params.maGap > -2.0) { + // MAs converging from below (MA50 approaching MA200) + score += 12 + reasons.push(`βœ… MAs converging bullish (${params.maGap.toFixed(2)}% gap) (+12 pts)`) + } else if (params.maGap < -2.0 && params.maGap > -5.0) { + // Early momentum building + score += 8 + reasons.push(`βœ… Early bullish momentum (${params.maGap.toFixed(2)}% gap) (+8 pts)`) + } else if (params.maGap >= 2.0) { + // Wide gap = momentum already extended + score += 5 + reasons.push(`⚠️ Extended bullish gap (${params.maGap.toFixed(2)}%) (+5 pts)`) + } else if (params.maGap <= -5.0) { + // Very bearish MA structure for long + score -= 5 + reasons.push(`⚠️ Bearish MA structure for long (${params.maGap.toFixed(2)}%) (-5 pts)`) + } + } else if (params.direction === 'short') { + if (params.maGap <= 0 && params.maGap > -2.0) { + // Tight bearish convergence (MA50 below MA200, close together) + score += 15 + reasons.push(`βœ… Tight bearish MA convergence (${params.maGap.toFixed(2)}% gap) (+15 pts)`) + } else if (params.maGap > 0 && params.maGap < 2.0) { + // MAs converging from above (MA50 approaching MA200) + score += 12 + reasons.push(`βœ… MAs converging bearish (${params.maGap.toFixed(2)}% gap) (+12 pts)`) + } else if (params.maGap > 2.0 && params.maGap < 5.0) { + // Early momentum building + score += 8 + reasons.push(`βœ… Early bearish momentum (${params.maGap.toFixed(2)}% gap) (+8 pts)`) + } else if (params.maGap <= -2.0) { + // Wide gap = momentum already extended + score += 5 + reasons.push(`⚠️ Extended bearish gap (${params.maGap.toFixed(2)}%) (+5 pts)`) + } else if (params.maGap >= 5.0) { + // Very bullish MA structure for short + score -= 5 + reasons.push(`⚠️ Bullish MA structure for short (${params.maGap.toFixed(2)}%) (-5 pts)`) + } + } + } + // Direction-specific threshold support (Nov 23, 2025) // Use provided minScore, or fall back to 60 if not specified const minScore = params.minScore || 60 diff --git a/workflows/trading/moneyline_v8_comparisson.pinescript b/workflows/trading/moneyline_v8_comparisson.pinescript new file mode 100644 index 0000000..303d3c5 --- /dev/null +++ b/workflows/trading/moneyline_v8_comparisson.pinescript @@ -0,0 +1,268 @@ +//@version=6 +indicator("Bullmania Money Line v8 Sticky Trend", shorttitle="ML v8", overlay=true) + +// Calculation source (Chart vs Heikin Ashi) +srcMode = input.string("Chart", "Calculation source", options=["Chart","Heikin Ashi"], tooltip="Use regular chart candles or Heikin Ashi for the line calculation.") + +// Parameter Mode +paramMode = input.string("Profiles by timeframe", "Parameter Mode", options=["Single", "Profiles by timeframe"], tooltip="Choose whether to use one global set of parameters or timeframe-specific profiles.") + +// Single (global) parameters +atrPeriodSingle = input.int(10, "ATR Period (Single mode)", minval=1, group="Single Mode") +multiplierSingle = input.float(3.0, "Multiplier (Single mode)", minval=0.1, step=0.1, group="Single Mode") + +// Profile override when using profiles +profileOverride = input.string("Auto", "Profile Override", options=["Auto", "Minutes", "Hours", "Daily", "Weekly/Monthly"], tooltip="When in 'Profiles by timeframe' mode, choose a fixed profile or let it auto-detect from the chart timeframe.", group="Profiles") + +// Timeframe profile parameters +// Minutes (<= 59m) +atr_m = input.int(12, "ATR Period (Minutes)", minval=1, group="Profiles β€” Minutes") +mult_m = input.float(3.8, "Multiplier (Minutes)", minval=0.1, step=0.1, group="Profiles β€” Minutes", tooltip="V8: Increased from 3.3 for stickier trend") + +// Hours (>=1h and <1d) +atr_h = input.int(10, "ATR Period (Hours)", minval=1, group="Profiles β€” Hours") +mult_h = input.float(3.5, "Multiplier (Hours)", minval=0.1, step=0.1, group="Profiles β€” Hours", tooltip="V8: Increased from 3.0 for stickier trend") + +// Daily (>=1d and <1w) +atr_d = input.int(10, "ATR Period (Daily)", minval=1, group="Profiles β€” Daily") +mult_d = input.float(3.2, "Multiplier (Daily)", minval=0.1, step=0.1, group="Profiles β€” Daily", tooltip="V8: Increased from 2.8 for stickier trend") + +// Weekly/Monthly (>=1w) +atr_w = input.int(7, "ATR Period (Weekly/Monthly)", minval=1, group="Profiles β€” Weekly/Monthly") +mult_w = input.float(3.0, "Multiplier (Weekly/Monthly)", minval=0.1, step=0.1, group="Profiles β€” Weekly/Monthly", tooltip="V8: Increased from 2.5 for stickier trend") + +// Optional MACD confirmation +useMacd = input.bool(false, "Use MACD confirmation", inline="macd") +macdSrc = input.source(close, "MACD Source", inline="macd") +macdFastLen = input.int(12, "Fast", minval=1, inline="macdLens") +macdSlowLen = input.int(26, "Slow", minval=1, inline="macdLens") +macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens") + +// Signal timing (ALWAYS applies to all signals) +groupTiming = "Signal Timing" +confirmBars = input.int(2, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V8: Wait X bars after flip to confirm trend change. Filters rapid flip-flops.") +flipThreshold = input.float(0.8, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V8: Require price to move this % beyond line before flip. Increased to 0.8% to filter small bounces.") + +// Entry filters (optional) +groupFilters = "Entry filters" +useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="V8: Enabled by default. Close must be beyond the Money Line by buffer amount to avoid wick flips.") +entryBufferATR = input.float(0.20, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="V8: Increased to 0.20 ATR (from 0.15) to reduce flip-flops.") +useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="V8: Enabled by default to reduce choppy trades.") +adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters) +adxMin = input.int(18, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="V8: Increased from 14 to 18 for stronger trend requirement.") + +// NEW v6 FILTERS +groupV6Filters = "v6 Quality Filters" +usePricePosition = input.bool(true, "Use price position filter", group=groupV6Filters, tooltip="Prevent chasing extremes - don't buy at top of range or sell at bottom.") +longPosMax = input.float(85, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't buy if price is above this % of 100-bar range (prevents chasing highs).") +shortPosMin = input.float(15, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't sell if price is below this % of 100-bar range (prevents chasing lows).") + +useVolumeFilter = input.bool(true, "Use volume filter", group=groupV6Filters, tooltip="Filter signals with extreme volume (too low = dead, too high = climax).") +volMin = input.float(0.7, "Volume min ratio", minval=0.1, step=0.1, group=groupV6Filters, tooltip="Minimum volume relative to 20-bar MA.") +volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group=groupV6Filters, tooltip="Maximum volume relative to 20-bar MA.") + +useRsiFilter = input.bool(true, "Use RSI momentum filter", group=groupV6Filters, tooltip="Ensure momentum confirms direction.") +rsiLongMin = input.float(35, "RSI long minimum", minval=0, maxval=100, group=groupV6Filters) +rsiLongMax = input.float(70, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters) + +// Determine effective parameters based on selected mode/profile +var string activeProfile = "" +resSec = timeframe.in_seconds(timeframe.period) +isMinutes = resSec < 3600 +isHours = resSec >= 3600 and resSec < 86400 +isDaily = resSec >= 86400 and resSec < 604800 +isWeeklyOrMore = resSec >= 604800 + +// Resolve profile bucket +string profileBucket = "Single" +if paramMode == "Single" + profileBucket := "Single" +else + if profileOverride == "Minutes" + profileBucket := "Minutes" + else if profileOverride == "Hours" + profileBucket := "Hours" + else if profileOverride == "Daily" + profileBucket := "Daily" + else if profileOverride == "Weekly/Monthly" + profileBucket := "Weekly/Monthly" + else + profileBucket := isMinutes ? "Minutes" : isHours ? "Hours" : isDaily ? "Daily" : "Weekly/Monthly" + +atrPeriod = profileBucket == "Single" ? atrPeriodSingle : profileBucket == "Minutes" ? atr_m : profileBucket == "Hours" ? atr_h : profileBucket == "Daily" ? atr_d : atr_w +multiplier = profileBucket == "Single" ? multiplierSingle : profileBucket == "Minutes" ? mult_m : profileBucket == "Hours" ? mult_h : profileBucket == "Daily" ? mult_d : mult_w +activeProfile := profileBucket + +// Core Money Line logic (with selectable source) +// Build selected source OHLC +// Optimized: Calculate Heikin Ashi directly instead of using request.security() +haC = srcMode == "Heikin Ashi" ? (open + high + low + close) / 4 : close +haO = srcMode == "Heikin Ashi" ? (nz(haC[1]) + nz(open[1])) / 2 : open +haH = srcMode == "Heikin Ashi" ? math.max(high, math.max(haO, haC)) : high +haL = srcMode == "Heikin Ashi" ? math.min(low, math.min(haO, haC)) : low +calcH = haH +calcL = haL +calcC = haC + +// ATR on selected source +tr = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atr = ta.rma(tr, atrPeriod) +src = (calcH + calcL) / 2 + +up = src - (multiplier * atr) +dn = src + (multiplier * atr) + +var float up1 = na +var float dn1 = na + +up1 := nz(up1[1], up) +dn1 := nz(dn1[1], dn) + +up1 := calcC[1] > up1 ? math.max(up, up1) : up +dn1 := calcC[1] < dn1 ? math.min(dn, dn1) : dn + +var int trend = 1 +var float tsl = na + +tsl := nz(tsl[1], up1) + +// V8: Apply flip threshold - require price to move X% beyond line before flip +thresholdAmount = tsl * (flipThreshold / 100) + +// Track consecutive bars in potential new direction (anti-whipsaw) +var int bullMomentumBars = 0 +var int bearMomentumBars = 0 + +if trend == 1 + tsl := math.max(up1, tsl) + // Count consecutive bearish bars + if calcC < (tsl - thresholdAmount) + bearMomentumBars := bearMomentumBars + 1 + bullMomentumBars := 0 + else + bearMomentumBars := 0 + // Flip only after X consecutive bars below threshold + trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1 +else + tsl := math.min(dn1, tsl) + // Count consecutive bullish bars + if calcC > (tsl + thresholdAmount) + bullMomentumBars := bullMomentumBars + 1 + bearMomentumBars := 0 + else + bullMomentumBars := 0 + // Flip only after X consecutive bars above threshold + trend := bullMomentumBars >= (confirmBars + 1) ? 1 : -1 + +supertrend = tsl + +// Plot the Money Line +upTrend = trend == 1 ? supertrend : na +downTrend = trend == -1 ? supertrend : na + +plot(upTrend, "Up Trend", color=color.new(color.green, 0), style=plot.style_linebr, linewidth=2) +plot(downTrend, "Down Trend", color=color.new(color.red, 0), style=plot.style_linebr, linewidth=2) + +// Show active profile on chart as a label (optimized - only on confirmed bar) +showProfileLabel = input.bool(true, "Show active profile label", group="Profiles") +var label profLbl = na +if barstate.islast and barstate.isconfirmed and showProfileLabel + label.delete(profLbl) + profLbl := label.new(bar_index, close, text="Profile: " + activeProfile + " | ATR=" + str.tostring(atrPeriod) + " Mult=" + str.tostring(multiplier), yloc=yloc.price, style=label.style_label_upper_left, textcolor=color.white, color=color.new(color.blue, 20)) + +// MACD confirmation logic +[macdLine, macdSignal, macdHist] = ta.macd(macdSrc, macdFastLen, macdSlowLen, macdSigLen) +longOk = not useMacd or (macdLine > macdSignal) +shortOk = not useMacd or (macdLine < macdSignal) + +// Plot buy/sell signals (gated by optional MACD) +buyFlip = trend == 1 and trend[1] == -1 +sellFlip = trend == -1 and trend[1] == 1 + +// ADX computation (always calculate for context, but only filter if enabled) +upMove = calcH - calcH[1] +downMove = calcL[1] - calcL +plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0 +minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0 +trADX = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atrADX = ta.rma(trADX, adxLen) +plusDMSmooth = ta.rma(plusDM, adxLen) +minusDMSmooth = ta.rma(minusDM, adxLen) +plusDI = atrADX == 0.0 ? 0.0 : 100.0 * plusDMSmooth / atrADX +minusDI = atrADX == 0.0 ? 0.0 : 100.0 * minusDMSmooth / atrADX +dx = (plusDI + minusDI == 0.0) ? 0.0 : 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI) +adxVal = ta.rma(dx, adxLen) +adxOk = not useAdx or (adxVal > adxMin) + +// Entry buffer gates relative to current Money Line +longBufferOk = not useEntryBuffer or (calcC > supertrend + entryBufferATR * atr) +shortBufferOk = not useEntryBuffer or (calcC < supertrend - entryBufferATR * atr) + +// Confirmation bars after flip +buyReady = ta.barssince(buyFlip) == confirmBars +sellReady = ta.barssince(sellFlip) == confirmBars + +// === CONTEXT METRICS FOR SIGNAL QUALITY === +// Calculate ATR as percentage of price +atrPercent = (atr / calcC) * 100 + +// Calculate RSI +rsi14 = ta.rsi(calcC, 14) + +// Volume ratio (current volume vs 20-bar MA) +volMA20 = ta.sma(volume, 20) +volumeRatio = volume / volMA20 + +// v6 IMPROVEMENT: Price position in 100-bar range (was 20-bar in v5) +highest100 = ta.highest(calcH, 100) // Changed from 20 to 100 +lowest100 = ta.lowest(calcL, 100) // Changed from 20 to 100 +priceRange = highest100 - lowest100 +pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100 + +// v6 NEW FILTERS +// Price position filter - prevent chasing extremes +longPositionOk = not usePricePosition or (pricePosition < longPosMax) +shortPositionOk = not usePricePosition or (pricePosition > shortPosMin) + +// Volume filter - avoid dead or overheated moves +volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax) + +// RSI momentum filter +rsiLongOk = not useRsiFilter or (rsi14 >= rsiLongMin and rsi14 <= rsiLongMax) +rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax) + +// V8: STICKY TREND SIGNALS - High accuracy with flip-flop protection +// Signal fires on line color changes ONLY when price breaches threshold +// Protection: 0.5% flip threshold + 0.20 ATR buffer + ADX 18+ + stickier multipliers +// Result: Clean trend signals without noise +finalLongSignal = buyReady // 🟒 Signal on red β†’ green flip (with threshold) +finalShortSignal = sellReady // πŸ”΄ Signal on green β†’ red flip (with threshold) + +plotshape(finalLongSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small) +plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small) + +// Extract base currency from ticker (e.g., "ETHUSD" -> "ETH", "SOLUSD" -> "SOL") +baseCurrency = str.replace(syminfo.ticker, "USD", "") +baseCurrency := str.replace(baseCurrency, "USDT", "") +baseCurrency := str.replace(baseCurrency, "PERP", "") + +// Indicator version for tracking in database +indicatorVer = "v8" + +// Build enhanced alert messages with context (timeframe.period is dynamic) +longAlertMsg = baseCurrency + " buy " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | IND:" + indicatorVer + +shortAlertMsg = baseCurrency + " sell " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | IND:" + indicatorVer + +// Fire alerts with dynamic messages (use alert() not alertcondition() for dynamic content) +if finalLongSignal + alert(longAlertMsg, alert.freq_once_per_bar_close) + +if finalShortSignal + alert(shortAlertMsg, alert.freq_once_per_bar_close) + +// Fill area between price and Money Line +fill(plot(close, display=display.none), plot(upTrend, display=display.none), color=color.new(color.green, 90)) +fill(plot(close, display=display.none), plot(downTrend, display=display.none), color=color.new(color.red, 90)) \ No newline at end of file diff --git a/workflows/trading/moneyline_v8_sticky_trend.pinescript b/workflows/trading/moneyline_v8_sticky_trend.pinescript index 7c5c82b..6ffd02e 100644 --- a/workflows/trading/moneyline_v8_sticky_trend.pinescript +++ b/workflows/trading/moneyline_v8_sticky_trend.pinescript @@ -41,7 +41,7 @@ macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens") // Signal timing (ALWAYS applies to all signals) groupTiming = "Signal Timing" confirmBars = input.int(2, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V8: Wait X bars after flip to confirm trend change. Filters rapid flip-flops.") -flipThreshold = input.float(0.6, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V8: Require price to move this % beyond line before flip. Set to 0.6% to filter small bounces while catching real reversals.") +flipThreshold = input.float(0.8, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V8: Require price to move this % beyond line before flip. Increased to 0.8% to filter small bounces.") // Entry filters (optional) groupFilters = "Entry filters" @@ -235,10 +235,10 @@ rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax) // V8: STICKY TREND SIGNALS - High accuracy with flip-flop protection // Signal fires on line color changes ONLY when price breaches threshold -// Protection: 0.5% flip threshold + 0.20 ATR buffer + ADX 18+ + stickier multipliers +// Protection: 0.6% flip threshold + 0.20 ATR buffer + ADX 18+ + stickier multipliers // Result: Clean trend signals without noise -finalLongSignal = buyReady // 🟒 Signal on red β†’ green flip (with threshold) -finalShortSignal = sellReady // πŸ”΄ Signal on green β†’ red flip (with threshold) +finalLongSignal = buyReady and longOk and adxOk and longBufferOk and longPositionOk and volumeOk and rsiLongOk +finalShortSignal = sellReady and shortOk and adxOk and shortBufferOk and shortPositionOk and volumeOk and rsiShortOk plotshape(finalLongSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small) plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small) diff --git a/workflows/trading/moneyline_v9_ma_gap.pinescript b/workflows/trading/moneyline_v9_ma_gap.pinescript new file mode 100644 index 0000000..35e95b9 --- /dev/null +++ b/workflows/trading/moneyline_v9_ma_gap.pinescript @@ -0,0 +1,292 @@ +//@version=6 +indicator("Bullmania Money Line v9 MA Gap", shorttitle="ML v9", overlay=true) + +// Calculation source (Chart vs Heikin Ashi) +srcMode = input.string("Chart", "Calculation source", options=["Chart","Heikin Ashi"], tooltip="Use regular chart candles or Heikin Ashi for the line calculation.") + +// Parameter Mode +paramMode = input.string("Profiles by timeframe", "Parameter Mode", options=["Single", "Profiles by timeframe"], tooltip="Choose whether to use one global set of parameters or timeframe-specific profiles.") + +// Single (global) parameters +atrPeriodSingle = input.int(10, "ATR Period (Single mode)", minval=1, group="Single Mode") +multiplierSingle = input.float(3.0, "Multiplier (Single mode)", minval=0.1, step=0.1, group="Single Mode") + +// Profile override when using profiles +profileOverride = input.string("Auto", "Profile Override", options=["Auto", "Minutes", "Hours", "Daily", "Weekly/Monthly"], tooltip="When in 'Profiles by timeframe' mode, choose a fixed profile or let it auto-detect from the chart timeframe.", group="Profiles") + +// Timeframe profile parameters +// Minutes (<= 59m) +atr_m = input.int(12, "ATR Period (Minutes)", minval=1, group="Profiles β€” Minutes") +mult_m = input.float(3.8, "Multiplier (Minutes)", minval=0.1, step=0.1, group="Profiles β€” Minutes", tooltip="V8: Increased from 3.3 for stickier trend") + +// Hours (>=1h and <1d) +atr_h = input.int(10, "ATR Period (Hours)", minval=1, group="Profiles β€” Hours") +mult_h = input.float(3.5, "Multiplier (Hours)", minval=0.1, step=0.1, group="Profiles β€” Hours", tooltip="V8: Increased from 3.0 for stickier trend") + +// Daily (>=1d and <1w) +atr_d = input.int(10, "ATR Period (Daily)", minval=1, group="Profiles β€” Daily") +mult_d = input.float(3.2, "Multiplier (Daily)", minval=0.1, step=0.1, group="Profiles β€” Daily", tooltip="V8: Increased from 2.8 for stickier trend") + +// Weekly/Monthly (>=1w) +atr_w = input.int(7, "ATR Period (Weekly/Monthly)", minval=1, group="Profiles β€” Weekly/Monthly") +mult_w = input.float(3.0, "Multiplier (Weekly/Monthly)", minval=0.1, step=0.1, group="Profiles β€” Weekly/Monthly", tooltip="V8: Increased from 2.5 for stickier trend") + +// Optional MACD confirmation +useMacd = input.bool(false, "Use MACD confirmation", inline="macd") +macdSrc = input.source(close, "MACD Source", inline="macd") +macdFastLen = input.int(12, "Fast", minval=1, inline="macdLens") +macdSlowLen = input.int(26, "Slow", minval=1, inline="macdLens") +macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens") + +// Signal timing (ALWAYS applies to all signals) +groupTiming = "Signal Timing" +confirmBars = input.int(0, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V9: Set to 0 for immediate signals on flip. Increase to wait X bars for confirmation.") +flipThreshold = input.float(0.6, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V9: Require price to move this % beyond line before flip. 0.6% filters small bounces while catching real reversals.") + +// Entry filters (optional) +groupFilters = "Entry filters" +useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="V8: Enabled by default. Close must be beyond the Money Line by buffer amount to avoid wick flips.") +entryBufferATR = input.float(0.20, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="V8: Increased to 0.20 ATR (from 0.15) to reduce flip-flops.") +useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="V8: Enabled by default to reduce choppy trades.") +adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters) +adxMin = input.int(18, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="V8: Increased from 14 to 18 for stronger trend requirement.") + +// NEW v6 FILTERS +groupV6Filters = "v6 Quality Filters" +usePricePosition = input.bool(true, "Use price position filter", group=groupV6Filters, tooltip="Prevent chasing extremes - don't buy at top of range or sell at bottom.") +longPosMax = input.float(85, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't buy if price is above this % of 100-bar range (prevents chasing highs).") +shortPosMin = input.float(15, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't sell if price is below this % of 100-bar range (prevents chasing lows).") + +useVolumeFilter = input.bool(true, "Use volume filter", group=groupV6Filters, tooltip="Filter signals with extreme volume (too low = dead, too high = climax).") +volMin = input.float(0.7, "Volume min ratio", minval=0.1, step=0.1, group=groupV6Filters, tooltip="Minimum volume relative to 20-bar MA.") +volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group=groupV6Filters, tooltip="Maximum volume relative to 20-bar MA.") + +useRsiFilter = input.bool(true, "Use RSI momentum filter", group=groupV6Filters, tooltip="Ensure momentum confirms direction.") +rsiLongMin = input.float(35, "RSI long minimum", minval=0, maxval=100, group=groupV6Filters) +rsiLongMax = input.float(70, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters) + +// V9 NEW: MA GAP VISUALIZATION OPTIONS +groupV9MA = "v9 MA Gap Options" +showMAs = input.bool(true, "Show 50 and 200 MAs on chart", group=groupV9MA, tooltip="Display the moving averages for visual reference.") +ma50Color = input.color(color.new(color.yellow, 0), "MA 50 Color", group=groupV9MA) +ma200Color = input.color(color.new(color.orange, 0), "MA 200 Color", group=groupV9MA) + +// Determine effective parameters based on selected mode/profile +var string activeProfile = "" +resSec = timeframe.in_seconds(timeframe.period) +isMinutes = resSec < 3600 +isHours = resSec >= 3600 and resSec < 86400 +isDaily = resSec >= 86400 and resSec < 604800 +isWeeklyOrMore = resSec >= 604800 + +// Resolve profile bucket +string profileBucket = "Single" +if paramMode == "Single" + profileBucket := "Single" +else + if profileOverride == "Minutes" + profileBucket := "Minutes" + else if profileOverride == "Hours" + profileBucket := "Hours" + else if profileOverride == "Daily" + profileBucket := "Daily" + else if profileOverride == "Weekly/Monthly" + profileBucket := "Weekly/Monthly" + else + profileBucket := isMinutes ? "Minutes" : isHours ? "Hours" : isDaily ? "Daily" : "Weekly/Monthly" + +atrPeriod = profileBucket == "Single" ? atrPeriodSingle : profileBucket == "Minutes" ? atr_m : profileBucket == "Hours" ? atr_h : profileBucket == "Daily" ? atr_d : atr_w +multiplier = profileBucket == "Single" ? multiplierSingle : profileBucket == "Minutes" ? mult_m : profileBucket == "Hours" ? mult_h : profileBucket == "Daily" ? mult_d : mult_w +activeProfile := profileBucket + +// Core Money Line logic (with selectable source) +// Build selected source OHLC +// Optimized: Calculate Heikin Ashi directly instead of using request.security() +haC = srcMode == "Heikin Ashi" ? (open + high + low + close) / 4 : close +haO = srcMode == "Heikin Ashi" ? (nz(haC[1]) + nz(open[1])) / 2 : open +haH = srcMode == "Heikin Ashi" ? math.max(high, math.max(haO, haC)) : high +haL = srcMode == "Heikin Ashi" ? math.min(low, math.min(haO, haC)) : low +calcH = haH +calcL = haL +calcC = haC + +// ATR on selected source +tr = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atr = ta.rma(tr, atrPeriod) +src = (calcH + calcL) / 2 + +up = src - (multiplier * atr) +dn = src + (multiplier * atr) + +var float up1 = na +var float dn1 = na + +up1 := nz(up1[1], up) +dn1 := nz(dn1[1], dn) + +up1 := calcC[1] > up1 ? math.max(up, up1) : up +dn1 := calcC[1] < dn1 ? math.min(dn, dn1) : dn + +var int trend = 1 +var float tsl = na + +tsl := nz(tsl[1], up1) + +// V8: Apply flip threshold - require price to move X% beyond line before flip +thresholdAmount = tsl * (flipThreshold / 100) + +// Track consecutive bars in potential new direction (anti-whipsaw) +var int bullMomentumBars = 0 +var int bearMomentumBars = 0 + +if trend == 1 + tsl := math.max(up1, tsl) + // Count consecutive bearish bars + if calcC < (tsl - thresholdAmount) + bearMomentumBars := bearMomentumBars + 1 + bullMomentumBars := 0 + else + bearMomentumBars := 0 + // Flip only after X consecutive bars below threshold + trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1 +else + tsl := math.min(dn1, tsl) + // Count consecutive bullish bars + if calcC > (tsl + thresholdAmount) + bullMomentumBars := bullMomentumBars + 1 + bearMomentumBars := 0 + else + bullMomentumBars := 0 + // Flip only after X consecutive bars above threshold + trend := bullMomentumBars >= (confirmBars + 1) ? 1 : -1 + +supertrend = tsl + +// Plot the Money Line +upTrend = trend == 1 ? supertrend : na +downTrend = trend == -1 ? supertrend : na + +plot(upTrend, "Up Trend", color=color.new(color.green, 0), style=plot.style_linebr, linewidth=2) +plot(downTrend, "Down Trend", color=color.new(color.red, 0), style=plot.style_linebr, linewidth=2) + +// Show active profile on chart as a label (optimized - only on confirmed bar) +showProfileLabel = input.bool(true, "Show active profile label", group="Profiles") +var label profLbl = na +if barstate.islast and barstate.isconfirmed and showProfileLabel + label.delete(profLbl) + profLbl := label.new(bar_index, close, text="Profile: " + activeProfile + " | ATR=" + str.tostring(atrPeriod) + " Mult=" + str.tostring(multiplier), yloc=yloc.price, style=label.style_label_upper_left, textcolor=color.white, color=color.new(color.blue, 20)) + +// MACD confirmation logic +[macdLine, macdSignal, macdHist] = ta.macd(macdSrc, macdFastLen, macdSlowLen, macdSigLen) +longOk = not useMacd or (macdLine > macdSignal) +shortOk = not useMacd or (macdLine < macdSignal) + +// Plot buy/sell signals (gated by optional MACD) +buyFlip = trend == 1 and trend[1] == -1 +sellFlip = trend == -1 and trend[1] == 1 + +// ADX computation (always calculate for context, but only filter if enabled) +upMove = calcH - calcH[1] +downMove = calcL[1] - calcL +plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0 +minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0 +trADX = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atrADX = ta.rma(trADX, adxLen) +plusDMSmooth = ta.rma(plusDM, adxLen) +minusDMSmooth = ta.rma(minusDM, adxLen) +plusDI = atrADX == 0.0 ? 0.0 : 100.0 * plusDMSmooth / atrADX +minusDI = atrADX == 0.0 ? 0.0 : 100.0 * minusDMSmooth / atrADX +dx = (plusDI + minusDI == 0.0) ? 0.0 : 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI) +adxVal = ta.rma(dx, adxLen) +adxOk = not useAdx or (adxVal > adxMin) + +// Entry buffer gates relative to current Money Line +longBufferOk = not useEntryBuffer or (calcC > supertrend + entryBufferATR * atr) +shortBufferOk = not useEntryBuffer or (calcC < supertrend - entryBufferATR * atr) + +// Confirmation bars after flip +buyReady = ta.barssince(buyFlip) == confirmBars +sellReady = ta.barssince(sellFlip) == confirmBars + +// === CONTEXT METRICS FOR SIGNAL QUALITY === +// Calculate ATR as percentage of price +atrPercent = (atr / calcC) * 100 + +// Calculate RSI +rsi14 = ta.rsi(calcC, 14) + +// Volume ratio (current volume vs 20-bar MA) +volMA20 = ta.sma(volume, 20) +volumeRatio = volume / volMA20 + +// v6 IMPROVEMENT: Price position in 100-bar range (was 20-bar in v5) +highest100 = ta.highest(calcH, 100) // Changed from 20 to 100 +lowest100 = ta.lowest(calcL, 100) // Changed from 20 to 100 +priceRange = highest100 - lowest100 +pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100 + +// === V9 NEW: MA GAP ANALYSIS === +// Calculate 50 and 200 period moving averages on CLOSE (not Heikin Ashi) +// Use standard close for MA calculations to match traditional analysis +ma50 = ta.sma(close, 50) +ma200 = ta.sma(close, 200) + +// Calculate MA gap as percentage +// Positive gap = bullish (50 MA above 200 MA) +// Negative gap = bearish (50 MA below 200 MA) +// Values near 0 = convergence (potential crossover brewing) +maGap = ma200 == 0 ? 0.0 : ((ma50 - ma200) / ma200) * 100 + +// Plot MAs if enabled (for visual reference) - disabled by default for clean chart +// plot(showMAs ? ma50 : na, title="MA 50", color=ma50Color, linewidth=1) +// plot(showMAs ? ma200 : na, title="MA 200", color=ma200Color, linewidth=2) + +// v6 NEW FILTERS +// Price position filter - prevent chasing extremes +longPositionOk = not usePricePosition or (pricePosition < longPosMax) +shortPositionOk = not usePricePosition or (pricePosition > shortPosMin) + +// Volume filter - avoid dead or overheated moves +volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax) + +// RSI momentum filter +rsiLongOk = not useRsiFilter or (rsi14 >= rsiLongMin and rsi14 <= rsiLongMax) +rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax) + +// V9: STICKY TREND SIGNALS with MA Gap awareness +// Signal fires on line color changes ONLY when price breaches threshold +// Protection: 0.6% flip threshold + 0.20 ATR buffer + ADX 18+ + stickier multipliers +// NEW: MA gap data helps backend validate trend structure alignment +// Result: Clean trend signals without noise + MA structure confirmation +finalLongSignal = buyReady // 🟒 Signal on red β†’ green flip (with threshold) +finalShortSignal = sellReady // πŸ”΄ Signal on green β†’ red flip (with threshold) + +plotshape(finalLongSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small) +plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small) + +// Extract base currency from ticker (e.g., "ETHUSD" -> "ETH", "SOLUSD" -> "SOL") +baseCurrency = str.replace(syminfo.ticker, "USD", "") +baseCurrency := str.replace(baseCurrency, "USDT", "") +baseCurrency := str.replace(baseCurrency, "PERP", "") + +// Indicator version for tracking in database +indicatorVer = "v9" + +// Build enhanced alert messages with context (timeframe.period is dynamic) +// V9 NEW: Added MAGAP field for MA gap percentage +longAlertMsg = baseCurrency + " buy " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer + +shortAlertMsg = baseCurrency + " sell " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer + +// Fire alerts with dynamic messages (use alert() not alertcondition() for dynamic content) +if finalLongSignal + alert(longAlertMsg, alert.freq_once_per_bar_close) + +if finalShortSignal + alert(shortAlertMsg, alert.freq_once_per_bar_close) + +// Fill area between price and Money Line +fill(plot(close, display=display.none), plot(upTrend, display=display.none), color=color.new(color.green, 90)) +fill(plot(close, display=display.none), plot(downTrend, display=display.none), color=color.new(color.red, 90)) diff --git a/workflows/trading/moneyline_v9_ma_gap_clean.pinescript b/workflows/trading/moneyline_v9_ma_gap_clean.pinescript new file mode 100644 index 0000000..1a22121 --- /dev/null +++ b/workflows/trading/moneyline_v9_ma_gap_clean.pinescript @@ -0,0 +1,273 @@ +//@version=6 +indicator("Bullmania Money Line v9 Clean", shorttitle="ML v9C", overlay=true) + +// Calculation source (Chart vs Heikin Ashi) +srcMode = input.string("Chart", "Calculation source", options=["Chart","Heikin Ashi"], tooltip="Use regular chart candles or Heikin Ashi for the line calculation.") + +// Parameter Mode +paramMode = input.string("Profiles by timeframe", "Parameter Mode", options=["Single", "Profiles by timeframe"], tooltip="Choose whether to use one global set of parameters or timeframe-specific profiles.") + +// Single (global) parameters +atrPeriodSingle = input.int(10, "ATR Period (Single mode)", minval=1, group="Single Mode") +multiplierSingle = input.float(3.0, "Multiplier (Single mode)", minval=0.1, step=0.1, group="Single Mode") + +// Profile override when using profiles +profileOverride = input.string("Auto", "Profile Override", options=["Auto", "Minutes", "Hours", "Daily", "Weekly/Monthly"], tooltip="When in 'Profiles by timeframe' mode, choose a fixed profile or let it auto-detect from the chart timeframe.", group="Profiles") + +// Timeframe profile parameters +// Minutes (<= 59m) +atr_m = input.int(12, "ATR Period (Minutes)", minval=1, group="Profiles β€” Minutes") +mult_m = input.float(3.8, "Multiplier (Minutes)", minval=0.1, step=0.1, group="Profiles β€” Minutes", tooltip="V8: Increased from 3.3 for stickier trend") + +// Hours (>=1h and <1d) +atr_h = input.int(10, "ATR Period (Hours)", minval=1, group="Profiles β€” Hours") +mult_h = input.float(3.5, "Multiplier (Hours)", minval=0.1, step=0.1, group="Profiles β€” Hours", tooltip="V8: Increased from 3.0 for stickier trend") + +// Daily (>=1d and <1w) +atr_d = input.int(10, "ATR Period (Daily)", minval=1, group="Profiles β€” Daily") +mult_d = input.float(3.2, "Multiplier (Daily)", minval=0.1, step=0.1, group="Profiles β€” Daily", tooltip="V8: Increased from 2.8 for stickier trend") + +// Weekly/Monthly (>=1w) +atr_w = input.int(7, "ATR Period (Weekly/Monthly)", minval=1, group="Profiles β€” Weekly/Monthly") +mult_w = input.float(3.0, "Multiplier (Weekly/Monthly)", minval=0.1, step=0.1, group="Profiles β€” Weekly/Monthly", tooltip="V8: Increased from 2.5 for stickier trend") + +// Optional MACD confirmation +useMacd = input.bool(false, "Use MACD confirmation", inline="macd") +macdSrc = input.source(close, "MACD Source", inline="macd") +macdFastLen = input.int(12, "Fast", minval=1, inline="macdLens") +macdSlowLen = input.int(26, "Slow", minval=1, inline="macdLens") +macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens") + +// Signal timing (ALWAYS applies to all signals) +groupTiming = "Signal Timing" +confirmBars = input.int(2, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V8: Wait X bars after flip to confirm trend change. Filters rapid flip-flops.") +flipThreshold = input.float(0.8, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V8: Require price to move this % beyond line before flip. Increased to 0.8% to filter small bounces.") + +// Entry filters (optional) +groupFilters = "Entry filters" +useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="V8: Enabled by default. Close must be beyond the Money Line by buffer amount to avoid wick flips.") +entryBufferATR = input.float(0.20, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="V8: Increased to 0.20 ATR (from 0.15) to reduce flip-flops.") +useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="V8: Enabled by default to reduce choppy trades.") +adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters) +adxMin = input.int(18, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="V8: Increased from 14 to 18 for stronger trend requirement.") + +// NEW v6 FILTERS +groupV6Filters = "v6 Quality Filters" +usePricePosition = input.bool(true, "Use price position filter", group=groupV6Filters, tooltip="Prevent chasing extremes - don't buy at top of range or sell at bottom.") +longPosMax = input.float(85, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't buy if price is above this % of 100-bar range (prevents chasing highs).") +shortPosMin = input.float(15, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't sell if price is below this % of 100-bar range (prevents chasing lows).") + +useVolumeFilter = input.bool(true, "Use volume filter", group=groupV6Filters, tooltip="Filter signals with extreme volume (too low = dead, too high = climax).") +volMin = input.float(0.7, "Volume min ratio", minval=0.1, step=0.1, group=groupV6Filters, tooltip="Minimum volume relative to 20-bar MA.") +volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group=groupV6Filters, tooltip="Maximum volume relative to 20-bar MA.") + +useRsiFilter = input.bool(true, "Use RSI momentum filter", group=groupV6Filters, tooltip="Ensure momentum confirms direction.") +rsiLongMin = input.float(35, "RSI long minimum", minval=0, maxval=100, group=groupV6Filters) +rsiLongMax = input.float(70, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters) + +// Determine effective parameters based on selected mode/profile +var string activeProfile = "" +resSec = timeframe.in_seconds(timeframe.period) +isMinutes = resSec < 3600 +isHours = resSec >= 3600 and resSec < 86400 +isDaily = resSec >= 86400 and resSec < 604800 +isWeeklyOrMore = resSec >= 604800 + +// Resolve profile bucket +string profileBucket = "Single" +if paramMode == "Single" + profileBucket := "Single" +else + if profileOverride == "Minutes" + profileBucket := "Minutes" + else if profileOverride == "Hours" + profileBucket := "Hours" + else if profileOverride == "Daily" + profileBucket := "Daily" + else if profileOverride == "Weekly/Monthly" + profileBucket := "Weekly/Monthly" + else + profileBucket := isMinutes ? "Minutes" : isHours ? "Hours" : isDaily ? "Daily" : "Weekly/Monthly" + +atrPeriod = profileBucket == "Single" ? atrPeriodSingle : profileBucket == "Minutes" ? atr_m : profileBucket == "Hours" ? atr_h : profileBucket == "Daily" ? atr_d : atr_w +multiplier = profileBucket == "Single" ? multiplierSingle : profileBucket == "Minutes" ? mult_m : profileBucket == "Hours" ? mult_h : profileBucket == "Daily" ? mult_d : mult_w +activeProfile := profileBucket + +// Core Money Line logic (with selectable source) +// Build selected source OHLC +// Optimized: Calculate Heikin Ashi directly instead of using request.security() +haC = srcMode == "Heikin Ashi" ? (open + high + low + close) / 4 : close +haO = srcMode == "Heikin Ashi" ? (nz(haC[1]) + nz(open[1])) / 2 : open +haH = srcMode == "Heikin Ashi" ? math.max(high, math.max(haO, haC)) : high +haL = srcMode == "Heikin Ashi" ? math.min(low, math.min(haO, haC)) : low +calcH = haH +calcL = haL +calcC = haC + +// ATR on selected source +tr = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atr = ta.rma(tr, atrPeriod) +src = (calcH + calcL) / 2 + +up = src - (multiplier * atr) +dn = src + (multiplier * atr) + +var float up1 = na +var float dn1 = na + +up1 := nz(up1[1], up) +dn1 := nz(dn1[1], dn) + +up1 := calcC[1] > up1 ? math.max(up, up1) : up +dn1 := calcC[1] < dn1 ? math.min(dn, dn1) : dn + +var int trend = 1 +var float tsl = na + +tsl := nz(tsl[1], up1) + +// V8: Apply flip threshold - require price to move X% beyond line before flip +thresholdAmount = tsl * (flipThreshold / 100) + +// Track consecutive bars in potential new direction (anti-whipsaw) +var int bullMomentumBars = 0 +var int bearMomentumBars = 0 + +if trend == 1 + tsl := math.max(up1, tsl) + // Count consecutive bearish bars + if calcC < (tsl - thresholdAmount) + bearMomentumBars := bearMomentumBars + 1 + bullMomentumBars := 0 + else + bearMomentumBars := 0 + // Flip only after X consecutive bars below threshold + trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1 +else + tsl := math.min(dn1, tsl) + // Count consecutive bullish bars + if calcC > (tsl + thresholdAmount) + bullMomentumBars := bullMomentumBars + 1 + bearMomentumBars := 0 + else + bullMomentumBars := 0 + // Flip only after X consecutive bars above threshold + trend := bullMomentumBars >= (confirmBars + 1) ? 1 : -1 + +supertrend = tsl + +// Plot the Money Line +upTrend = trend == 1 ? supertrend : na +downTrend = trend == -1 ? supertrend : na + +plot(upTrend, "Up Trend", color=color.new(color.green, 0), style=plot.style_linebr, linewidth=2) +plot(downTrend, "Down Trend", color=color.new(color.red, 0), style=plot.style_linebr, linewidth=2) + +// Show active profile on chart as a label (optimized - only on confirmed bar) +showProfileLabel = input.bool(true, "Show active profile label", group="Profiles") +var label profLbl = na +if barstate.islast and barstate.isconfirmed and showProfileLabel + label.delete(profLbl) + profLbl := label.new(bar_index, close, text="Profile: " + activeProfile + " | ATR=" + str.tostring(atrPeriod) + " Mult=" + str.tostring(multiplier), yloc=yloc.price, style=label.style_label_upper_left, textcolor=color.white, color=color.new(color.blue, 20)) + +// MACD confirmation logic +[macdLine, macdSignal, macdHist] = ta.macd(macdSrc, macdFastLen, macdSlowLen, macdSigLen) +longOk = not useMacd or (macdLine > macdSignal) +shortOk = not useMacd or (macdLine < macdSignal) + +// Plot buy/sell signals (gated by optional MACD) +buyFlip = trend == 1 and trend[1] == -1 +sellFlip = trend == -1 and trend[1] == 1 + +// ADX computation (always calculate for context, but only filter if enabled) +upMove = calcH - calcH[1] +downMove = calcL[1] - calcL +plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0 +minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0 +trADX = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atrADX = ta.rma(trADX, adxLen) +plusDMSmooth = ta.rma(plusDM, adxLen) +minusDMSmooth = ta.rma(minusDM, adxLen) +plusDI = atrADX == 0.0 ? 0.0 : 100.0 * plusDMSmooth / atrADX +minusDI = atrADX == 0.0 ? 0.0 : 100.0 * minusDMSmooth / atrADX +dx = (plusDI + minusDI == 0.0) ? 0.0 : 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI) +adxVal = ta.rma(dx, adxLen) +adxOk = not useAdx or (adxVal > adxMin) + +// Entry buffer gates relative to current Money Line +longBufferOk = not useEntryBuffer or (calcC > supertrend + entryBufferATR * atr) +shortBufferOk = not useEntryBuffer or (calcC < supertrend - entryBufferATR * atr) + +// Confirmation bars after flip +buyReady = ta.barssince(buyFlip) == confirmBars +sellReady = ta.barssince(sellFlip) == confirmBars + +// === CONTEXT METRICS FOR SIGNAL QUALITY === +// Calculate ATR as percentage of price +atrPercent = (atr / calcC) * 100 + +// Calculate RSI +rsi14 = ta.rsi(calcC, 14) + +// Volume ratio (current volume vs 20-bar MA) +volMA20 = ta.sma(volume, 20) +volumeRatio = volume / volMA20 + +// v6 IMPROVEMENT: Price position in 100-bar range (was 20-bar in v5) +highest100 = ta.highest(calcH, 100) // Changed from 20 to 100 +lowest100 = ta.lowest(calcL, 100) // Changed from 20 to 100 +priceRange = highest100 - lowest100 +pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100 + +// V9: MA Gap Analysis (uses standard close, not Heikin Ashi) +ma50 = ta.sma(close, 50) +ma200 = ta.sma(close, 200) +maGap = ma200 == 0 ? 0.0 : ((ma50 - ma200) / ma200) * 100 + +// v6 NEW FILTERS +// Price position filter - prevent chasing extremes +longPositionOk = not usePricePosition or (pricePosition < longPosMax) +shortPositionOk = not usePricePosition or (pricePosition > shortPosMin) + +// Volume filter - avoid dead or overheated moves +volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax) + +// RSI momentum filter +rsiLongOk = not useRsiFilter or (rsi14 >= rsiLongMin and rsi14 <= rsiLongMax) +rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax) + +// V9: STICKY TREND SIGNALS - High accuracy with flip-flop protection +// Signal fires on line color changes ONLY when price breaches threshold +// Protection: 0.6% flip threshold + 0.20 ATR buffer + ADX 18+ + stickier multipliers +// Result: Clean trend signals without noise +finalLongSignal = buyReady and longOk and adxOk and longBufferOk and longPositionOk and volumeOk and rsiLongOk +finalShortSignal = sellReady and shortOk and adxOk and shortBufferOk and shortPositionOk and volumeOk and rsiShortOk + +plotshape(finalLongSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small) +plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small) + +// Extract base currency from ticker (e.g., "ETHUSD" -> "ETH", "SOLUSD" -> "SOL") +baseCurrency = str.replace(syminfo.ticker, "USD", "") +baseCurrency := str.replace(baseCurrency, "USDT", "") +baseCurrency := str.replace(baseCurrency, "PERP", "") + +// Indicator version for tracking in database +indicatorVer = "v9" + +// Build enhanced alert messages with context (timeframe.period is dynamic) +longAlertMsg = baseCurrency + " buy " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer + +shortAlertMsg = baseCurrency + " sell " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer + +// Fire alerts with dynamic messages (use alert() not alertcondition() for dynamic content) +if finalLongSignal + alert(longAlertMsg, alert.freq_once_per_bar_close) + +if finalShortSignal + alert(shortAlertMsg, alert.freq_once_per_bar_close) + +// Fill area between price and Money Line +fill(plot(close, display=display.none), plot(upTrend, display=display.none), color=color.new(color.green, 90)) +fill(plot(close, display=display.none), plot(downTrend, display=display.none), color=color.new(color.red, 90)) \ No newline at end of file diff --git a/workflows/trading/moneyline_v9_test.pinescript b/workflows/trading/moneyline_v9_test.pinescript new file mode 100644 index 0000000..18f41e2 --- /dev/null +++ b/workflows/trading/moneyline_v9_test.pinescript @@ -0,0 +1,268 @@ +//@version=6 +indicator("Bullmania Money Line v9 TEST", shorttitle="ML v9T", overlay=true) + +// Calculation source (Chart vs Heikin Ashi) +srcMode = input.string("Chart", "Calculation source", options=["Chart","Heikin Ashi"], tooltip="Use regular chart candles or Heikin Ashi for the line calculation.") + +// Parameter Mode +paramMode = input.string("Profiles by timeframe", "Parameter Mode", options=["Single", "Profiles by timeframe"], tooltip="Choose whether to use one global set of parameters or timeframe-specific profiles.") + +// Single (global) parameters +atrPeriodSingle = input.int(10, "ATR Period (Single mode)", minval=1, group="Single Mode") +multiplierSingle = input.float(3.0, "Multiplier (Single mode)", minval=0.1, step=0.1, group="Single Mode") + +// Profile override when using profiles +profileOverride = input.string("Auto", "Profile Override", options=["Auto", "Minutes", "Hours", "Daily", "Weekly/Monthly"], tooltip="When in 'Profiles by timeframe' mode, choose a fixed profile or let it auto-detect from the chart timeframe.", group="Profiles") + +// Timeframe profile parameters +// Minutes (<= 59m) +atr_m = input.int(12, "ATR Period (Minutes)", minval=1, group="Profiles β€” Minutes") +mult_m = input.float(3.8, "Multiplier (Minutes)", minval=0.1, step=0.1, group="Profiles β€” Minutes", tooltip="V8: Increased from 3.3 for stickier trend") + +// Hours (>=1h and <1d) +atr_h = input.int(10, "ATR Period (Hours)", minval=1, group="Profiles β€” Hours") +mult_h = input.float(3.5, "Multiplier (Hours)", minval=0.1, step=0.1, group="Profiles β€” Hours", tooltip="V8: Increased from 3.0 for stickier trend") + +// Daily (>=1d and <1w) +atr_d = input.int(10, "ATR Period (Daily)", minval=1, group="Profiles β€” Daily") +mult_d = input.float(3.2, "Multiplier (Daily)", minval=0.1, step=0.1, group="Profiles β€” Daily", tooltip="V8: Increased from 2.8 for stickier trend") + +// Weekly/Monthly (>=1w) +atr_w = input.int(7, "ATR Period (Weekly/Monthly)", minval=1, group="Profiles β€” Weekly/Monthly") +mult_w = input.float(3.0, "Multiplier (Weekly/Monthly)", minval=0.1, step=0.1, group="Profiles β€” Weekly/Monthly", tooltip="V8: Increased from 2.5 for stickier trend") + +// Optional MACD confirmation +useMacd = input.bool(false, "Use MACD confirmation", inline="macd") +macdSrc = input.source(close, "MACD Source", inline="macd") +macdFastLen = input.int(12, "Fast", minval=1, inline="macdLens") +macdSlowLen = input.int(26, "Slow", minval=1, inline="macdLens") +macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens") + +// Signal timing (ALWAYS applies to all signals) +groupTiming = "Signal Timing" +confirmBars = input.int(2, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V8: Wait X bars after flip to confirm trend change. Filters rapid flip-flops.") +flipThreshold = input.float(0.8, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V8: Require price to move this % beyond line before flip. Increased to 0.8% to filter small bounces.") + +// Entry filters (optional) +groupFilters = "Entry filters" +useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="V8: Enabled by default. Close must be beyond the Money Line by buffer amount to avoid wick flips.") +entryBufferATR = input.float(0.20, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="V8: Increased to 0.20 ATR (from 0.15) to reduce flip-flops.") +useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="V8: Enabled by default to reduce choppy trades.") +adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters) +adxMin = input.int(18, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="V8: Increased from 14 to 18 for stronger trend requirement.") + +// NEW v6 FILTERS +groupV6Filters = "v6 Quality Filters" +usePricePosition = input.bool(true, "Use price position filter", group=groupV6Filters, tooltip="Prevent chasing extremes - don't buy at top of range or sell at bottom.") +longPosMax = input.float(85, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't buy if price is above this % of 100-bar range (prevents chasing highs).") +shortPosMin = input.float(15, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't sell if price is below this % of 100-bar range (prevents chasing lows).") + +useVolumeFilter = input.bool(true, "Use volume filter", group=groupV6Filters, tooltip="Filter signals with extreme volume (too low = dead, too high = climax).") +volMin = input.float(0.7, "Volume min ratio", minval=0.1, step=0.1, group=groupV6Filters, tooltip="Minimum volume relative to 20-bar MA.") +volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group=groupV6Filters, tooltip="Maximum volume relative to 20-bar MA.") + +useRsiFilter = input.bool(true, "Use RSI momentum filter", group=groupV6Filters, tooltip="Ensure momentum confirms direction.") +rsiLongMin = input.float(35, "RSI long minimum", minval=0, maxval=100, group=groupV6Filters) +rsiLongMax = input.float(70, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters) + +// Determine effective parameters based on selected mode/profile +var string activeProfile = "" +resSec = timeframe.in_seconds(timeframe.period) +isMinutes = resSec < 3600 +isHours = resSec >= 3600 and resSec < 86400 +isDaily = resSec >= 86400 and resSec < 604800 +isWeeklyOrMore = resSec >= 604800 + +// Resolve profile bucket +string profileBucket = "Single" +if paramMode == "Single" + profileBucket := "Single" +else + if profileOverride == "Minutes" + profileBucket := "Minutes" + else if profileOverride == "Hours" + profileBucket := "Hours" + else if profileOverride == "Daily" + profileBucket := "Daily" + else if profileOverride == "Weekly/Monthly" + profileBucket := "Weekly/Monthly" + else + profileBucket := isMinutes ? "Minutes" : isHours ? "Hours" : isDaily ? "Daily" : "Weekly/Monthly" + +atrPeriod = profileBucket == "Single" ? atrPeriodSingle : profileBucket == "Minutes" ? atr_m : profileBucket == "Hours" ? atr_h : profileBucket == "Daily" ? atr_d : atr_w +multiplier = profileBucket == "Single" ? multiplierSingle : profileBucket == "Minutes" ? mult_m : profileBucket == "Hours" ? mult_h : profileBucket == "Daily" ? mult_d : mult_w +activeProfile := profileBucket + +// Core Money Line logic (with selectable source) +// Build selected source OHLC +// Optimized: Calculate Heikin Ashi directly instead of using request.security() +haC = srcMode == "Heikin Ashi" ? (open + high + low + close) / 4 : close +haO = srcMode == "Heikin Ashi" ? (nz(haC[1]) + nz(open[1])) / 2 : open +haH = srcMode == "Heikin Ashi" ? math.max(high, math.max(haO, haC)) : high +haL = srcMode == "Heikin Ashi" ? math.min(low, math.min(haO, haC)) : low +calcH = haH +calcL = haL +calcC = haC + +// ATR on selected source +tr = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atr = ta.rma(tr, atrPeriod) +src = (calcH + calcL) / 2 + +up = src - (multiplier * atr) +dn = src + (multiplier * atr) + +var float up1 = na +var float dn1 = na + +up1 := nz(up1[1], up) +dn1 := nz(dn1[1], dn) + +up1 := calcC[1] > up1 ? math.max(up, up1) : up +dn1 := calcC[1] < dn1 ? math.min(dn, dn1) : dn + +var int trend = 1 +var float tsl = na + +tsl := nz(tsl[1], up1) + +// V8: Apply flip threshold - require price to move X% beyond line before flip +thresholdAmount = tsl * (flipThreshold / 100) + +// Track consecutive bars in potential new direction (anti-whipsaw) +var int bullMomentumBars = 0 +var int bearMomentumBars = 0 + +if trend == 1 + tsl := math.max(up1, tsl) + // Count consecutive bearish bars + if calcC < (tsl - thresholdAmount) + bearMomentumBars := bearMomentumBars + 1 + bullMomentumBars := 0 + else + bearMomentumBars := 0 + // Flip only after X consecutive bars below threshold + trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1 +else + tsl := math.min(dn1, tsl) + // Count consecutive bullish bars + if calcC > (tsl + thresholdAmount) + bullMomentumBars := bullMomentumBars + 1 + bearMomentumBars := 0 + else + bullMomentumBars := 0 + // Flip only after X consecutive bars above threshold + trend := bullMomentumBars >= (confirmBars + 1) ? 1 : -1 + +supertrend = tsl + +// Plot the Money Line +upTrend = trend == 1 ? supertrend : na +downTrend = trend == -1 ? supertrend : na + +plot(upTrend, "Up Trend", color=color.new(color.green, 0), style=plot.style_linebr, linewidth=2) +plot(downTrend, "Down Trend", color=color.new(color.red, 0), style=plot.style_linebr, linewidth=2) + +// Show active profile on chart as a label (optimized - only on confirmed bar) +showProfileLabel = input.bool(true, "Show active profile label", group="Profiles") +var label profLbl = na +if barstate.islast and barstate.isconfirmed and showProfileLabel + label.delete(profLbl) + profLbl := label.new(bar_index, close, text="Profile: " + activeProfile + " | ATR=" + str.tostring(atrPeriod) + " Mult=" + str.tostring(multiplier), yloc=yloc.price, style=label.style_label_upper_left, textcolor=color.white, color=color.new(color.blue, 20)) + +// MACD confirmation logic +[macdLine, macdSignal, macdHist] = ta.macd(macdSrc, macdFastLen, macdSlowLen, macdSigLen) +longOk = not useMacd or (macdLine > macdSignal) +shortOk = not useMacd or (macdLine < macdSignal) + +// Plot buy/sell signals (gated by optional MACD) +buyFlip = trend == 1 and trend[1] == -1 +sellFlip = trend == -1 and trend[1] == 1 + +// ADX computation (always calculate for context, but only filter if enabled) +upMove = calcH - calcH[1] +downMove = calcL[1] - calcL +plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0 +minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0 +trADX = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1]))) +atrADX = ta.rma(trADX, adxLen) +plusDMSmooth = ta.rma(plusDM, adxLen) +minusDMSmooth = ta.rma(minusDM, adxLen) +plusDI = atrADX == 0.0 ? 0.0 : 100.0 * plusDMSmooth / atrADX +minusDI = atrADX == 0.0 ? 0.0 : 100.0 * minusDMSmooth / atrADX +dx = (plusDI + minusDI == 0.0) ? 0.0 : 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI) +adxVal = ta.rma(dx, adxLen) +adxOk = not useAdx or (adxVal > adxMin) + +// Entry buffer gates relative to current Money Line +longBufferOk = not useEntryBuffer or (calcC > supertrend + entryBufferATR * atr) +shortBufferOk = not useEntryBuffer or (calcC < supertrend - entryBufferATR * atr) + +// Confirmation bars after flip +buyReady = ta.barssince(buyFlip) == confirmBars +sellReady = ta.barssince(sellFlip) == confirmBars + +// === CONTEXT METRICS FOR SIGNAL QUALITY === +// Calculate ATR as percentage of price +atrPercent = (atr / calcC) * 100 + +// Calculate RSI +rsi14 = ta.rsi(calcC, 14) + +// Volume ratio (current volume vs 20-bar MA) +volMA20 = ta.sma(volume, 20) +volumeRatio = volume / volMA20 + +// v6 IMPROVEMENT: Price position in 100-bar range (was 20-bar in v5) +highest100 = ta.highest(calcH, 100) // Changed from 20 to 100 +lowest100 = ta.lowest(calcL, 100) // Changed from 20 to 100 +priceRange = highest100 - lowest100 +pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100 + +// v6 NEW FILTERS +// Price position filter - prevent chasing extremes +longPositionOk = not usePricePosition or (pricePosition < longPosMax) +shortPositionOk = not usePricePosition or (pricePosition > shortPosMin) + +// Volume filter - avoid dead or overheated moves +volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax) + +// RSI momentum filter +rsiLongOk = not useRsiFilter or (rsi14 >= rsiLongMin and rsi14 <= rsiLongMax) +rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax) + +// V8: STICKY TREND SIGNALS - High accuracy with flip-flop protection +// Signal fires on line color changes ONLY when price breaches threshold +// Protection: 0.6% flip threshold + 0.20 ATR buffer + ADX 18+ + stickier multipliers +// Result: Clean trend signals without noise +finalLongSignal = buyReady and longOk and adxOk and longBufferOk and longPositionOk and volumeOk and rsiLongOk +finalShortSignal = sellReady and shortOk and adxOk and shortBufferOk and shortPositionOk and volumeOk and rsiShortOk + +plotshape(finalLongSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small) +plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small) + +// Extract base currency from ticker (e.g., "ETHUSD" -> "ETH", "SOLUSD" -> "SOL") +baseCurrency = str.replace(syminfo.ticker, "USD", "") +baseCurrency := str.replace(baseCurrency, "USDT", "") +baseCurrency := str.replace(baseCurrency, "PERP", "") + +// Indicator version for tracking in database +indicatorVer = "v8" + +// Build enhanced alert messages with context (timeframe.period is dynamic) +longAlertMsg = baseCurrency + " buy " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | IND:" + indicatorVer + +shortAlertMsg = baseCurrency + " sell " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | IND:" + indicatorVer + +// Fire alerts with dynamic messages (use alert() not alertcondition() for dynamic content) +if finalLongSignal + alert(longAlertMsg, alert.freq_once_per_bar_close) + +if finalShortSignal + alert(shortAlertMsg, alert.freq_once_per_bar_close) + +// Fill area between price and Money Line +fill(plot(close, display=display.none), plot(upTrend, display=display.none), color=color.new(color.green, 90)) +fill(plot(close, display=display.none), plot(downTrend, display=display.none), color=color.new(color.red, 90)) \ No newline at end of file diff --git a/workflows/trading/parse_signal_enhanced.json b/workflows/trading/parse_signal_enhanced.json index 712d699..818c008 100644 --- a/workflows/trading/parse_signal_enhanced.json +++ b/workflows/trading/parse_signal_enhanced.json @@ -3,7 +3,7 @@ "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\n// Enhanced timeframe extraction supporting multiple formats:\n// - \"buy 5\" β†’ \"5\"\n// - \"buy 15\" β†’ \"15\"\n// - \"buy 60\" or \"buy 1h\" β†’ \"60\"\n// - \"buy 240\" or \"buy 4h\" β†’ \"240\"\n// - \"buy D\" or \"buy 1d\" β†’ \"D\"\n// - \"buy W\" β†’ \"W\"\nconst timeframeMatch = body.match(/\\b(buy|sell)\\s+(\\d+|D|W|M|1h|4h|1d)\\b/i);\nlet timeframe = '5'; // Default to 5min\n\nif (timeframeMatch) {\n const tf = timeframeMatch[2];\n // Convert hour/day notation to minutes\n if (tf === '1h' || tf === '60') {\n timeframe = '60';\n } else if (tf === '4h' || tf === '240') {\n timeframe = '240';\n } else if (tf === '1d' || tf.toUpperCase() === 'D') {\n timeframe = 'D';\n } else if (tf.toUpperCase() === 'W') {\n timeframe = 'W';\n } else if (tf.toUpperCase() === 'M') {\n timeframe = 'M';\n } else {\n timeframe = tf;\n }\n}\n\n// Parse new context metrics from enhanced format:\n// \"SOLT.P buy 15 | ATR:0.65 | ADX:14.3 | RSI:51.3 | VOL:0.87 | POS:59.3 | IND:v8\"\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\n// Parse indicator version (optional, backward compatible)\nconst indicatorVersionMatch = body.match(/IND:(v\\d+)/i);\nconst indicatorVersion = indicatorVersionMatch ? indicatorVersionMatch[1] : 'v5';\n\nreturn {\n rawMessage: body,\n symbol,\n direction,\n timeframe,\n // Context fields\n atr,\n adx,\n rsi,\n volumeRatio,\n pricePosition,\n // Version tracking (defaults to v5 for backward compatibility)\n indicatorVersion\n};" + "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\n// Enhanced timeframe extraction supporting multiple formats:\n// - \"buy 5\" β†’ \"5\"\n// - \"buy 15\" β†’ \"15\"\n// - \"buy 60\" or \"buy 1h\" β†’ \"60\"\n// - \"buy 240\" or \"buy 4h\" β†’ \"240\"\n// - \"buy D\" or \"buy 1d\" β†’ \"D\"\n// - \"buy W\" β†’ \"W\"\nconst timeframeMatch = body.match(/\\b(buy|sell)\\s+(\\d+|D|W|M|1h|4h|1d)\\b/i);\nlet timeframe = '5'; // Default to 5min\n\nif (timeframeMatch) {\n const tf = timeframeMatch[2];\n // Convert hour/day notation to minutes\n if (tf === '1h' || tf === '60') {\n timeframe = '60';\n } else if (tf === '4h' || tf === '240') {\n timeframe = '240';\n } else if (tf === '1d' || tf.toUpperCase() === 'D') {\n timeframe = 'D';\n } else if (tf.toUpperCase() === 'W') {\n timeframe = 'W';\n } else if (tf.toUpperCase() === 'M') {\n timeframe = 'M';\n } else {\n timeframe = tf;\n }\n}\n\n// Parse new context metrics from enhanced format:\n// \"SOLT.P buy 15 | ATR:0.65 | ADX:14.3 | RSI:51.3 | VOL:0.87 | POS:59.3 | MAGAP:-1.23 | IND:v9\"\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\n// V9: Parse MA gap (optional, backward compatible with v8)\nconst maGapMatch = body.match(/MAGAP:([-\\d.]+)/);\nconst maGap = maGapMatch ? parseFloat(maGapMatch[1]) : undefined;\n\n// Parse indicator version (optional, backward compatible)\nconst indicatorVersionMatch = body.match(/IND:(v\\d+)/i);\nconst indicatorVersion = indicatorVersionMatch ? indicatorVersionMatch[1] : 'v5';\n\nreturn {\n rawMessage: body,\n symbol,\n direction,\n timeframe,\n // Context fields\n atr,\n adx,\n rsi,\n volumeRatio,\n pricePosition,\n maGap, // V9 NEW\n // Version tracking (defaults to v5 for backward compatibility)\n indicatorVersion\n};" }, "id": "parse-signal-enhanced", "name": "Parse Signal Enhanced",