Skip to content

feat(regime): add BTC macro regime classifier + GitHub Action [apply manually] #311

Description

@jmalekos

What this adds

Three files to fix the daily Orinias_Hyper1 regime classifier — generated by a scheduled Claude Code session that could not push directly (egress proxy blocks crypto APIs + git push; GitHub MCP is read-only).

Apply with:

# 1. copy the files below into place
# 2. trigger a first run
gh workflow run regime-classifier.yml

File 1 — .github/workflows/regime-classifier.yml

name: BTC Regime Classifier

on:
  schedule:
    - cron: '5 0 * * *'
  workflow_dispatch:

permissions:
  contents: write

jobs:
  classify:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Fetch BTC data and classify regime
        id: classify
        run: |
          python3 << 'PYEOF'
          import json, urllib.request, os, sys

          url = 'https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1d&limit=201'
          try:
              with urllib.request.urlopen(url, timeout=15) as r:
                  raw = json.load(r)
          except Exception as e:
              print(f'Binance failed: {e}', file=sys.stderr)
              yurl = 'https://query1.finance.yahoo.com/v8/finance/chart/BTC-USD?interval=1d&range=400d'
              with urllib.request.urlopen(yurl, timeout=15) as r:
                  ydata = json.load(r)
              rr = ydata['chart']['result'][0]
              ts = rr['timestamp']; q = rr['indicators']['quote'][0]
              raw2 = list(zip(ts, q['open'], q['high'], q['low'], q['close'], q['volume']))
              bars = [{'t':r[0]*1000,'o':r[1] or 0,'h':r[2] or 0,'l':r[3] or 0,'c':r[4] or 0,'v':r[5] or 0} for r in raw2 if r[4]]
          else:
              bars = [{'t':b[0],'o':float(b[1]),'h':float(b[2]),'l':float(b[3]),'c':float(b[4]),'v':float(b[5])} for b in raw[:-1]]

          bars = bars[-200:]
          closes = [b['c'] for b in bars]; highs = [b['h'] for b in bars]; price = closes[-1]

          sma50  = sum(closes[-50:]) / 50
          sma200 = sum(closes[-200:]) / 200 if len(closes) >= 200 else sum(closes) / len(closes)
          sma50_5d = sum(closes[-55:-5]) / 50 if len(closes) >= 55 else sma50
          slope    = (sma50 - sma50_5d) / sma50_5d

          trs = []
          for i in range(1, len(bars)):
              c, p = bars[i], bars[i-1]
              trs.append(max(c['h']-c['l'], abs(c['h']-p['c']), abs(c['l']-p['c'])))
          atr14 = sum(trs[-14:]) / 14; atr_pct = atr14 / price
          high200 = max(highs[-200:]); drawdown = (high200 - price) / high200

          if price > sma50 * 1.30 and price > sma200 * 1.60:
              regime = 'euphoria'
          elif drawdown > 0.20 and atr_pct > 0.06:
              regime = 'capitulation'
          elif drawdown > 0.10 and price < sma50 and price > sma200 * 0.92:
              regime = 'correction'
          elif price > sma50 and price > sma200 and slope > 0:
              regime = 'bull_trend'
          else:
              regime = 'range_bound'

          out = os.environ.get('GITHUB_OUTPUT', '/dev/stdout')
          with open(out, 'a') as f:
              f.write(f'regime={regime}\nprice={price:.2f}\nsma50={sma50:.2f}\nsma200={sma200:.2f}\n')
              f.write(f'slope={slope*100:.3f}\natr14={atr14:.2f}\natr_pct={atr_pct*100:.2f}\n')
              f.write(f'drawdown={drawdown*100:.1f}\ncycle_high={high200:.2f}\n')

          print(f'REGIME={regime}  price={price:.2f}  sma50={sma50:.2f}  sma200={sma200:.2f}')
          print(f'drawdown={drawdown*100:.1f}%  atr={atr_pct*100:.2f}%  slope={slope*100:.3f}%')
          PYEOF

      - name: Read previous regime
        id: prev
        run: |
          if [ -f agents/regime.json ]; then
            prev=$(python3 -c "import json; d=json.load(open('agents/regime.json')); print(d.get('regime','unknown'))")
          else
            prev="unknown"
          fi
          echo "regime=$prev" >> "$GITHUB_OUTPUT"

      - name: Write regime.json
        run: |
          python3 << 'PYEOF'
          import json, os
          from datetime import datetime, timezone
          data = {
              "regime":   os.environ['REGIME'],
              "previous": os.environ['PREV'],
              "set_at":   datetime.now(timezone.utc).isoformat(),
              "method":   "github-action",
              "metrics": {
                  "price":        float(os.environ['PRICE']),
                  "sma50":        float(os.environ['SMA50']),
                  "sma200":       float(os.environ['SMA200']),
                  "sma50_slope":  float(os.environ['SLOPE']),
                  "atr14":        float(os.environ['ATR14']),
                  "atr_pct":      float(os.environ['ATR_PCT']),
                  "drawdown_pct": float(os.environ['DRAWDOWN']),
                  "cycle_high":   float(os.environ['CYCLE_HIGH']),
              }
          }
          os.makedirs('agents', exist_ok=True)
          with open('agents/regime.json', 'w') as f:
              json.dump(data, f, indent=2); f.write('\n')
          print(json.dumps(data, indent=2))
          PYEOF
        env:
          REGIME:     ${{ steps.classify.outputs.regime }}
          PREV:       ${{ steps.prev.outputs.regime }}
          PRICE:      ${{ steps.classify.outputs.price }}
          SMA50:      ${{ steps.classify.outputs.sma50 }}
          SMA200:     ${{ steps.classify.outputs.sma200 }}
          SLOPE:      ${{ steps.classify.outputs.slope }}
          ATR14:      ${{ steps.classify.outputs.atr14 }}
          ATR_PCT:    ${{ steps.classify.outputs.atr_pct }}
          DRAWDOWN:   ${{ steps.classify.outputs.drawdown }}
          CYCLE_HIGH: ${{ steps.classify.outputs.cycle_high }}

      - name: Commit if changed
        run: |
          git config user.name  "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add agents/regime.json
          if git diff --staged --quiet; then
            echo "No change — skipping commit"
          else
            NEW="${{ steps.classify.outputs.regime }}"
            OLD="${{ steps.prev.outputs.regime }}"
            [ "$NEW" != "$OLD" ] && MSG="chore(regime): CHANGE $OLD -> $NEW [auto]" || MSG="chore(regime): update metrics ($NEW) [auto]"
            git commit -m "$MSG" && git push
          fi

File 2 — agents/regime.json

{
  "regime": "correction",
  "previous": "unknown",
  "set_at": "2026-07-03T00:00:00.000Z",
  "method": "bootstrap",
  "metrics": {
    "price": 61792,
    "sma50": 67000,
    "sma200": 61180,
    "sma50_slope": -0.8,
    "atr14": 2274,
    "atr_pct": 3.68,
    "drawdown_pct": 36.9,
    "cycle_high": 97860,
    "note": "Bootstrapped from WebSearch 2026-07-03; replace with first GitHub Action run"
  }
}

File 3 — agents/set_regime.js

#!/usr/bin/env node
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const REGIME_PATH = resolve(__dirname, 'regime.json');
const VALID_REGIMES = ['bull_trend', 'range_bound', 'correction', 'capitulation', 'euphoria'];
const SIGNAL_GATES = {
  bull_trend:   { A:'off', B:'on',  C:'off', D:'on',  'E-long':'on',  'E-short':'off' },
  range_bound:  { A:'on',  B:'on',  C:'on',  D:'on',  'E-long':'on',  'E-short':'on'  },
  correction:   { A:'on',  B:'on',  C:'on',  D:'on',  'E-long':'off', 'E-short':'on'  },
  capitulation: { A:'off', B:'off', C:'off', D:'on',  'E-long':'off', 'E-short':'off' },
  euphoria:     { A:'on',  B:'off', C:'on',  D:'off', 'E-long':'off', 'E-short':'on'  },
};

function readRegime() {
  if (!existsSync(REGIME_PATH)) return { regime: 'unknown', set_at: null };
  return JSON.parse(readFileSync(REGIME_PATH, 'utf8'));
}

const args = process.argv.slice(2);
const regimeArg = args.find(a => a.startsWith('--regime='));

if (!regimeArg) {
  const d = readRegime();
  const gates = SIGNAL_GATES[d.regime] ?? {};
  console.log(`\nCurrent regime : ${d.regime}`);
  console.log(`Set at         : ${d.set_at ?? 'never'}`);
  console.log(`Method         : ${d.method ?? 'unknown'}`);
  if (d.metrics) {
    const m = d.metrics;
    console.log(`\nMetrics:`);
    console.log(`  Price       : $${m.price?.toFixed(2) ?? 'n/a'}`);
    console.log(`  SMA50       : $${m.sma50?.toFixed(2) ?? 'n/a'}`);
    console.log(`  SMA200      : $${m.sma200?.toFixed(2) ?? 'n/a'}`);
    console.log(`  Drawdown    : ${m.drawdown_pct?.toFixed(1) ?? 'n/a'}%`);
    console.log(`  Cycle high  : $${m.cycle_high?.toFixed(2) ?? 'n/a'}`);
    console.log(`  ATR%        : ${m.atr_pct?.toFixed(2) ?? 'n/a'}%`);
  }
  const gateStr = Object.entries(gates).map(([k,v]) => `${k}:${v}`).join('  ');
  if (gateStr) console.log(`\nSignal gate    : ${gateStr}`);
  console.log();
  process.exit(0);
}

const newRegime = regimeArg.split('=')[1];
if (!VALID_REGIMES.includes(newRegime)) {
  console.error(`Error: invalid regime "${newRegime}"`);
  console.error(`Valid: ${VALID_REGIMES.join(', ')}`);
  process.exit(1);
}
const prev = readRegime();
const data = { regime: newRegime, previous: prev.regime, set_at: new Date().toISOString(), method: 'manual', metrics: prev.metrics ?? null };
writeFileSync(REGIME_PATH, JSON.stringify(data, null, 2) + '\n');
console.log(`Regime updated: ${prev.regime}${newRegime}`);

package.json — add one line to scripts

"agent:regime": "node agents/set_regime.js"

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions