feat(v9): Complete MA gap backend integration
Integrated MA gap analysis into signal quality evaluation pipeline: BACKEND SCORING (lib/trading/signal-quality.ts): - Added maGap?: number parameter to scoreSignalQuality interface - Implemented convergence/divergence scoring logic: * LONG: +15pts tight bullish (0-2%), +12pts converging (-2-0%), +8pts early momentum (-5--2%) * SHORT: +15pts tight bearish (-2-0%), +12pts converging (0-2%), +8pts early momentum (2-5%) * Penalties: -5pts for misaligned MA structure (>5% wrong direction) N8N PARSER (workflows/trading/parse_signal_enhanced.json): - Added MAGAP:([-\d.]+) regex pattern for negative number support - Extracts maGap from TradingView v9 alert messages - Returns maGap in parsed output (backward compatible with v8) - Updated comment to show v9 format API ENDPOINTS: - app/api/trading/check-risk/route.ts: Pass maGap to scoreSignalQuality (2 calls) - app/api/trading/execute/route.ts: Pass maGap to scoreSignalQuality (2 calls) FULL PIPELINE NOW COMPLETE: 1. TradingView v9 → Generates signal with MAGAP field 2. n8n webhook → Extracts maGap from alert message 3. Backend scoring → Evaluates MA gap convergence (+8 to +15 pts) 4. Quality threshold → Borderline signals (75-85) can reach 91+ 5. Execute decision → Only signals scoring ≥91 are executed MOTIVATION: Helps borderline quality signals reach execution threshold without overriding safety rules. Addresses Nov 25 missed opportunity where good signal had MA convergence but borderline quality score. TESTING REQUIRED: - Verify n8n parses MAGAP correctly from v9 alerts - Confirm backend receives maGap parameter - Validate MA gap scoring applied to quality calculation - Monitor first 10-20 v9 signals for scoring accuracy
This commit is contained in:
332
INDICATOR_V9_MA_GAP_ROADMAP.md
Normal file
332
INDICATOR_V9_MA_GAP_ROADMAP.md
Normal file
@@ -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<SignalQualityResult> {
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
@@ -94,6 +94,7 @@ async function shouldAllowScaling(
|
|||||||
rsi: newSignal.rsi || 50,
|
rsi: newSignal.rsi || 50,
|
||||||
volumeRatio: newSignal.volumeRatio || 1,
|
volumeRatio: newSignal.volumeRatio || 1,
|
||||||
pricePosition: newSignal.pricePosition,
|
pricePosition: newSignal.pricePosition,
|
||||||
|
maGap: newSignal.maGap, // V9: MA gap convergence scoring
|
||||||
direction: newSignal.direction,
|
direction: newSignal.direction,
|
||||||
symbol: newSignal.symbol,
|
symbol: newSignal.symbol,
|
||||||
currentPrice: newSignal.currentPrice,
|
currentPrice: newSignal.currentPrice,
|
||||||
@@ -373,6 +374,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
rsi: body.rsi || 0,
|
rsi: body.rsi || 0,
|
||||||
volumeRatio: body.volumeRatio || 0,
|
volumeRatio: body.volumeRatio || 0,
|
||||||
pricePosition: body.pricePosition || 0,
|
pricePosition: body.pricePosition || 0,
|
||||||
|
maGap: body.maGap, // V9: MA gap convergence scoring
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
symbol: body.symbol,
|
symbol: body.symbol,
|
||||||
currentPrice: currentPrice,
|
currentPrice: currentPrice,
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
rsi: body.rsi || 0,
|
rsi: body.rsi || 0,
|
||||||
volumeRatio: body.volumeRatio || 0,
|
volumeRatio: body.volumeRatio || 0,
|
||||||
pricePosition: body.pricePosition || 0,
|
pricePosition: body.pricePosition || 0,
|
||||||
|
maGap: body.maGap, // V9: MA gap convergence scoring
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
currentPrice: body.signalPrice || 0,
|
currentPrice: body.signalPrice || 0,
|
||||||
@@ -487,6 +488,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
rsi: body.rsi || 0,
|
rsi: body.rsi || 0,
|
||||||
volumeRatio: body.volumeRatio || 0,
|
volumeRatio: body.volumeRatio || 0,
|
||||||
pricePosition: body.pricePosition || 0,
|
pricePosition: body.pricePosition || 0,
|
||||||
|
maGap: body.maGap, // V9: MA gap convergence scoring
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
currentPrice: openResult.fillPrice,
|
currentPrice: openResult.fillPrice,
|
||||||
|
|||||||
364
docs/N8N_API_MANAGEMENT.md
Normal file
364
docs/N8N_API_MANAGEMENT.md
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# n8n API Management Guide
|
||||||
|
|
||||||
|
**Created:** Nov 26, 2025
|
||||||
|
**Purpose:** Document n8n API access for workflow automation and management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Credentials
|
||||||
|
|
||||||
|
**n8n Instance:** http://localhost:8098 (Docker container, port mapped 0.0.0.0:8098->5678)
|
||||||
|
**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 <workflow_id> <workflow.json>"
|
||||||
|
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
|
||||||
@@ -55,6 +55,7 @@ export async function scoreSignalQuality(params: {
|
|||||||
timeframe?: string // "5" = 5min, "15" = 15min, "60" = 1H, "D" = daily
|
timeframe?: string // "5" = 5min, "15" = 15min, "60" = 1H, "D" = daily
|
||||||
minScore?: number // Configurable minimum score threshold
|
minScore?: number // Configurable minimum score threshold
|
||||||
skipFrequencyCheck?: boolean // For testing or when frequency check not needed
|
skipFrequencyCheck?: boolean // For testing or when frequency check not needed
|
||||||
|
maGap?: number // V9: MA gap percentage (MA50-MA200)/MA200*100
|
||||||
}): Promise<SignalQualityResult> {
|
}): Promise<SignalQualityResult> {
|
||||||
let score = 50 // Base score
|
let score = 50 // Base score
|
||||||
const reasons: string[] = []
|
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)
|
// Direction-specific threshold support (Nov 23, 2025)
|
||||||
// Use provided minScore, or fall back to 60 if not specified
|
// Use provided minScore, or fall back to 60 if not specified
|
||||||
const minScore = params.minScore || 60
|
const minScore = params.minScore || 60
|
||||||
|
|||||||
268
workflows/trading/moneyline_v8_comparisson.pinescript
Normal file
268
workflows/trading/moneyline_v8_comparisson.pinescript
Normal file
@@ -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))
|
||||||
@@ -41,7 +41,7 @@ macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens")
|
|||||||
// Signal timing (ALWAYS applies to all signals)
|
// Signal timing (ALWAYS applies to all signals)
|
||||||
groupTiming = "Signal Timing"
|
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.")
|
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)
|
// Entry filters (optional)
|
||||||
groupFilters = "Entry filters"
|
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
|
// V8: STICKY TREND SIGNALS - High accuracy with flip-flop protection
|
||||||
// Signal fires on line color changes ONLY when price breaches threshold
|
// 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
|
// Result: Clean trend signals without noise
|
||||||
finalLongSignal = buyReady // 🟢 Signal on red → green flip (with threshold)
|
finalLongSignal = buyReady and longOk and adxOk and longBufferOk and longPositionOk and volumeOk and rsiLongOk
|
||||||
finalShortSignal = sellReady // 🔴 Signal on green → red flip (with threshold)
|
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(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)
|
plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small)
|
||||||
|
|||||||
292
workflows/trading/moneyline_v9_ma_gap.pinescript
Normal file
292
workflows/trading/moneyline_v9_ma_gap.pinescript
Normal file
@@ -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))
|
||||||
273
workflows/trading/moneyline_v9_ma_gap_clean.pinescript
Normal file
273
workflows/trading/moneyline_v9_ma_gap_clean.pinescript
Normal file
@@ -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))
|
||||||
268
workflows/trading/moneyline_v9_test.pinescript
Normal file
268
workflows/trading/moneyline_v9_test.pinescript
Normal file
@@ -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))
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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",
|
"id": "parse-signal-enhanced",
|
||||||
"name": "Parse Signal Enhanced",
|
"name": "Parse Signal Enhanced",
|
||||||
|
|||||||
Reference in New Issue
Block a user