# How the Activation Scores Strategy Works

A visual guide to the strategy architecture, signal logic, and optimization pipeline.

---

## 1. The Big Picture

The strategy lives in two places simultaneously: **TradingView** (Pine Script) for live
charting and execution, and **Python + GPU** for backtesting and optimization. They stay
in sync by sharing a common CSV export of pre-calculated indicator values.

```mermaid
graph TD
    TV["📊 TradingView Chart\nBTCUSD · 1D / 6H / 8H"]
    EXP["📁 data/COINBASE_BTCUSD-1D.csv\n~4000 bars × 40 columns\nOHLCV + pre-calculated indicators"]
    PY["🐍 Python Strategy\nstrategy_activation_scores.py"]
    GPU["⚡ GPU Optimizer\noptimize_strategy.py\n50M parameter sets / run"]
    WIN["🏆 Winner\nresults/optimization_winner_*.csv\nCheatsheet + Pine snippet"]
    PINE["🌲 Pine Script\nstrategy_activation_scores.pine\nUpdated input() defaults"]

    TV -->|"Export chart data\n(Save chart data button)"| EXP
    EXP -->|"Read pre-calculated\nindicator columns"| PY
    PY -->|"Batch backtest\nN=50M samples"| GPU
    GPU -->|"CPU verification\nof top 100 results"| WIN
    WIN -->|"Paste weights +\nthresholds into TV"| PINE
    PINE -->|"Live strategy\nexecutes on new bars"| TV

    style TV fill:#1a3a5c,color:#fff
    style GPU fill:#2d5a1b,color:#fff
    style WIN fill:#5a3a00,color:#fff
    style PINE fill:#1a3a5c,color:#fff
```

**Key insight:** TradingView does all the hard indicator math (Stochastic RSI, MACD,
M2/M3 money supply, divergence oscillators). Python reads those pre-calculated values
from the CSV — it never re-implements the indicator logic. This ensures exact parity
between the Python backtest and what TradingView shows on the chart.

---

## 2. The Activation Score — How 25 Signals Become One Number

Every bar, 25 indicator signals are each normalised to a **[-1, +1]** scale and then
combined into a single **Activation Score** via a weighted dot-product.

```mermaid
graph LR
    subgraph TECH["⚡ Technical Indicators"]
        T1["Stochastic RSI\n(stoch−50)/50"]
        T2["MACD Prediction\nsign"]
        T3["RSI / OSC\n(rsi−50)/50"]
        T4["MACD Bullish Flip\n1 or 0"]
        T5["Stoch Peaking\n−1 or 0"]
        T6["Stoch Bottoming\n1 or 0"]
        T7["Stoch Divergence\n−1/0/+1"]
        T8["VWAP Divergence\nclipped ±1"]
        T9["RSI Divergence\nclipped ±1"]
    end

    subgraph MONEY["💰 Money Supply  M2 / M3"]
        M1["M3 Momentum\nsign"]
        M2["M2 Tiny Momentum\nsign (ε threshold)"]
        M3["M3 Divergence Osc\n−1/0/+1"]
        M4["M2 Divergence Osc\n−1/0/+1"]
        M5["M2 No-Offset Div\n−1/0/+1"]
    end

    subgraph CANDLE["🕯️ Candlestick Patterns"]
        C1["Bearish Engulfing\n0→1 confidence"]
        C2["Bullish Hammer\n0→1 confidence"]
        C3["Bullish Engulfing\n0→1 confidence"]
        C4["Shooting Star\n0→1 confidence"]
    end

    subgraph MACRO["🌍 Macro / Cross-Asset"]
        X1["BTC/SPX Correlation\n30-bar rolling"]
        X2["DXY ROC\n−1/0/+1 inverted"]
        X3["VIX Percentile\n0→1 inverted"]
        X4["BTC Dominance ROC\n−1/0/+1"]
        X5["US 10Y Yield ROC\n−1/0/+1 inverted"]
        X6["SPY vs 200 EMA\n−1 or +1"]
        X7["Gold ROC Percentile\n0→1"]
    end

    DOT["Σ  feature_i × weight_i\n(weighted dot-product)"]
    NORM["÷  Σ|weight_i|  ×  1000\n(normalise to fixed scale)"]
    SCORE["🎯 Activation Score\nrange ≈ −1000 to +1000\n0 = neutral\n+1000 = max bullish alignment"]

    TECH --> DOT
    MONEY --> DOT
    CANDLE --> DOT
    MACRO --> DOT
    DOT --> NORM
    NORM --> SCORE
```

**Why normalise?** Without normalisation, doubling all weights doubles all scores and the
entry threshold of 400 would need to become 800 to produce identical signals. By dividing
by `Σ|weights|`, the score always means the same thing regardless of weight magnitudes:
a score of 400 always means 40% of maximum possible bullish alignment.

---

## 3. Entry & Exit Signal Logic

The strategy uses a **crossunder** pattern — it enters when the Activation Score
transitions from above to below the entry threshold. This captures the moment when
bullish conditions peak and begin to soften (buying the transition, not the bottom).

```mermaid
graph TD
    SCORE["Activation Score\n(current bar)"]

    SCORE --> ECROSS{"Previous bar ≥ Entry Threshold\nCurrent bar  < Entry Threshold?\n(score just crossed under)"}

    ECROSS -->|"No"| HOLD["No signal\nMaintain current state"]
    ECROSS -->|"Yes — standard mode"| ENTRY["🟢 Entry Signal\nOpen long position"]
    ECROSS -->|"Yes — confirmation mode"| CONF{"Score rising this bar?\nNo exit signal last bar?"}
    CONF -->|"Yes"| ENTRY
    CONF -->|"No"| HOLD

    ENTRY --> INTRADE["In Trade\nHold long, earn daily returns\nminus 0.5% commission"]

    INTRADE --> XCROSS{"2 bars ago ≥ Exit Threshold\nPrevious bar  < Exit Threshold?\n(delayed crossunder)"}

    XCROSS -->|"No"| TRAIL{"Trailing stop\nenabled?"}
    XCROSS -->|"Yes — confirmation mode"| XCONF{"Current score\n< Confirmation Threshold?"}
    XCROSS -->|"Yes — stoch-peak mode"| XPEAK{"Stochastic\ncurrently peaking?"}

    XCONF -->|"Yes"| EXIT["🔴 Exit Signal\nClose long position"]
    XCONF -->|"No"| TRAIL
    XPEAK -->|"Yes"| EXIT
    XPEAK -->|"No"| TRAIL

    TRAIL -->|"Yes — stop hit"| EXIT
    TRAIL -->|"No"| INTRADE

    EXIT -->|"0.5% commission\napplied on exit bar"| FLAT["Flat\nWait for next entry"]
    FLAT --> SCORE

    style ENTRY fill:#1a5c2d,color:#fff
    style EXIT fill:#5c1a1a,color:#fff
    style INTRADE fill:#1a3a5c,color:#fff
```

**Three optimisable parameters control signal sensitivity:**

| Parameter | Typical range | Effect |
|---|---|---|
| Entry threshold | 0 – 700 | Higher = fewer, higher-conviction entries |
| Exit threshold | −200 – 600 | Lower = exits sooner on any weakness |
| Exit confirmation threshold | −300 – 400 | Lower = requires deeper score drop before exiting |

---

## 4. The GPU Optimization Loop

Finding good weights manually across 25 dimensions is intractable. The optimizer uses a
GPU to evaluate **50 million random parameter combinations** per iteration.

```mermaid
graph TD
    PARAMS["📋 Parameter Search Space\nparams_COINBASE_BTCUSD_1D.json\n~25 weights + 3 thresholds + flags\n~10^30 possible combinations"]

    SAMPLE["🎲 Random Sampler\nDraw N = 50M parameter sets\nfrom the defined ranges"]

    BATCH["📦 Batch Assembly\nWeights matrix  N × 25\nThresholds matrix  N × 4\nConfigs matrix  N × 2"]

    PREP["🔧 _prepare_features\nCPU: pre-normalise all 25\nindicator columns to ±1\nUpload once to GPU memory"]

    subgraph KERNEL["⚡ GPU Kernel  _backtest_kernel\nOne CUDA thread per parameter set"]
        K1["Compute max_score\n= Σ|weights|"]
        K2["For each bar t = 1..T:\n  score = dot(features, weights) / max_score × 1000\n  check crossunder entry/exit\n  update equity curve"]
        K3["Compute final metrics:\nP&L%, Max Drawdown%\nSharpe, Sortino, trades"]
    end

    TOP["📊 Heap: Top 1000 results\n(by Calmar Ratio)"]

    VERIFY["🔍 CPU Verification\nRe-run top 100 with full\nPython strategy logic\n(commission, exact parity)"]

    WINNER["🏆 Winner\noptimization_winner_*.csv\nCheatsheet  +  Pine snippet"]

    LOOP{"More iterations\n(MAX = 3)?"}

    PARAMS --> SAMPLE
    SAMPLE --> BATCH
    PREP --> KERNEL
    BATCH --> KERNEL
    K1 --> K2 --> K3
    KERNEL --> TOP
    TOP --> VERIFY
    VERIFY --> WINNER
    WINNER --> LOOP
    LOOP -->|"Yes\n(sleep 5s)"| SAMPLE
    LOOP -->|"No"| DONE["✅ Done\nFinal winner in results/"]

    style KERNEL fill:#2d3a1a,color:#fff
    style WINNER fill:#5a3a00,color:#fff
```

**Why GPU?** Each parameter set is an independent backtest — no shared state between
threads. This is a textbook embarrassingly parallel problem. On a consumer RTX GPU
(WSL2) the optimizer evaluates ~610,000 parameter sets per second, completing 50M
samples in ~80 seconds. The equivalent CPU loop would take ~14 hours.

**Why CPU verification?** The GPU kernel trades some precision for speed (integer
commission approximation, no score-start window). The top-100 GPU candidates are
re-run through the full Python strategy to get exact metrics before recording a winner.

---

## 5. Score Normalisation — Why Thresholds Are Stable Across Weight Sets

```mermaid
graph LR
    W["Example weight set A\nstoch=10, macd=50, osc=−5\n...  Σ|w| = 350"]
    W2["Example weight set B\nstoch=20, macd=100, osc=−10\n...  Σ|w| = 700\n(all weights × 2)"]

    RAW["Raw dot-product\nset A: score = 175\nset B: score = 350"]

    NORM["After normalisation\nset A: 175/350 × 1000 = 500\nset B: 350/700 × 1000 = 500\n✅ Identical normalised score"]

    ENTRY["Entry threshold = 400\nmeans the same thing\nfor BOTH weight sets:\n40% of max bullish"]

    W --> RAW
    W2 --> RAW
    RAW --> NORM
    NORM --> ENTRY
```

Before normalisation was added, entry threshold 106 might mean "tight" with one weight
set and "very loose" with another. The optimizer had to learn the coupling from scratch
on every run. With normalisation, the optimizer can directly learn "entry at ~40% of max
bullish works well" and that knowledge transfers across all weight magnitudes.

---

## 6. Full End-to-End Workflow

```mermaid
sequenceDiagram
    participant TV as TradingView
    participant CSV as data/CSV
    participant OPT as Optimizer (GPU)
    participant VER as CPU Verifier
    participant RES as results/

    Note over TV: Add/tune indicators on chart
    TV->>CSV: Export chart data (Save chart data)
    Note over CSV: ~40 columns: OHLCV +<br/>stoch, macd, m2, m3, rsi,<br/>divergence oscs, macro signals...

    Note over OPT: auto_optimize_loop.py --hours 6
    OPT->>CSV: Load indicator columns
    OPT->>OPT: Random sample 50M weight+threshold sets
    OPT->>OPT: GPU kernel: backtest all 50M in ~80s
    OPT->>VER: Send top 1000 candidates

    VER->>CSV: Re-run top 100 with Python strategy
    VER->>RES: Save winner CSV + cheatsheet + pine snippet

    Note over RES: Calmar ~X.XX | Sortino ~X.XX<br/>~40 trades | 2019→2025

    RES->>TV: Paste weights + thresholds into<br/>TradingView strategy inputs

    Note over TV: Live strategy now uses<br/>GPU-optimized parameters

    TV->>CSV: Re-export if new indicators added
    Note over OPT: Repeat with updated CSV
```

---

## 7. Technology Stack at a Glance

| Layer | Technology | Role |
|---|---|---|
| **Indicator calculation** | Pine Script v6 (TradingView) | Stochastic RSI, MACD, M2/M3 money supply, VWAP divergence, RSI divergence, candlestick patterns |
| **Data bridge** | CSV export (TradingView → disk) | ~4,000 bars × 40 pre-calculated columns |
| **Strategy logic** | Python + NumPy + Pandas | Weighted dot-product, score normalisation, crossunder signal logic |
| **JIT compilation** | Numba `@njit` | Candlestick scoring, position calculation — compiled to machine code |
| **GPU acceleration** | Numba CUDA | 50M parallel backtests per run; ~610k samples/s on consumer RTX GPU |
| **Metric optimisation** | Calmar Ratio (primary) | Annualised return ÷ max drawdown — penalises large drawdowns heavily |
| **Macro signals** | Global M2/M3 money supply, DXY, VIX, BTC dominance, SPY, US10Y, Gold | 7 cross-asset regime indicators alongside technical signals |
| **Search strategy** | Random search over 25 dimensions | ~10³⁰ possible combinations; random sampling is more efficient than grid for high-dimensional spaces |

---

## 8. What Gets Optimised

The optimizer searches over **three categories** of parameters simultaneously:

```mermaid
graph TD
    subgraph WEIGHTS["🎛️ 25 Feature Weights\n(what each signal contributes to the score)"]
        W1["Technical: stoch, macd_pred, osc\nmacd_bullish, rsid_osc\nstoch_div, vwap_div\nstoch_peaking, stoch_bottoming"]
        W2["Money Supply: m3_momentum, m2_tiny\nm3_div_osc, m2_div_osc, m2_div_noOffset"]
        W3["Candlestick: bearish_engulfing\nbullish_hammer, bullish_engulfing\nshooting_star"]
        W4["Macro: btc_spx_corr, dxy, vix\nbtc_dominance, us10y, spy, gold"]
    end

    subgraph THRESH["📏 3 Thresholds\n(when to enter and exit)"]
        T1["Entry threshold\n(score level that triggers long entry)"]
        T2["Exit threshold\n(score level that triggers exit)"]
        T3["Exit confirmation threshold\n(secondary filter to prevent premature exits)"]
    end

    subgraph FLAGS["🔀 3 Mode Flags"]
        F1["Use entry confirmation\n(1-bar vs 2-bar crossunder)"]
        F2["Use exit confirmation\n(score drop vs stoch peak)"]
        F3["Trailing stop %\n(0 = disabled)"]
    end

    WEIGHTS --> OBJ["🎯 Maximise Calmar Ratio\nAnnualised Return ÷ |Max Drawdown|\nover 2019→2025 scoring window\n(minimum 30 trades required)"]
    THRESH --> OBJ
    FLAGS --> OBJ
```

The **Calmar Ratio** rewards strategies that compound well without deep drawdowns.
A 50% drawdown requires a subsequent 100% gain just to break even — Calmar captures this
asymmetry directly. Strategies with high Sortino but catastrophic single drawdowns
(which a Sharpe-optimised strategy might accept) are explicitly penalised.
