HermitNet FX (CrowdAverse_v2) is a multi-asset, graph-driven FX selection engine designed to identify opportunities where a currency pair behaves coherently on its own, but not as part of the broader crowd. Instead of relying on a single indicator or a fixed regime filter, it builds two layers of networks—one inside each pair (feature relationships), and one across the whole universe (pair-to-pair relationships)—and then scores each asset by balancing “internal clarity” against “market-wide coupling.”

On every bar (60-minute bars), the strategy updates nine lightweight features per pair using only recent closes: short and multi-bar log returns, simple trend slope (fast vs. slow mean), realized volatility, a compressed momentum proxy, a standardized range pressure measure, a volatility-of-volatility estimate, a flow proxy (absolute return), and a Markov-style “up/down persistence” proxy. These feature streams are stored in rolling buffers so the system can measure how the features interact over time.

For each pair, HermitNet FX constructs a feature graph where nodes are features and edges represent similarity based on rolling correlations (converted into distances). It then runs an all-pairs shortest-path pass and extracts structural metrics such as compactness (inverse of total path length), mean/variance of edge weights, and a simple centrality proxy. It also derives quick “state” estimates like short-horizon entropy and a crude next-bar bullishness frequency.

In parallel, the strategy builds a universe graph across all pairs. The distance between two pairs blends (1) correlation similarity of their return features and (2) a currency-exposure distance that penalizes overlapping base/quote exposure—so pairs that are essentially the same bet get pulled closer together. A compact universe graph implies “crowded” conditions; this becomes a coupling penalty.

Finally, each pair receives a score via a sigmoid of three forces: a regime compactness term, the pair’s own feature-graph compactness reward, and a strong penalty for global coupling. The engine ranks all 28 pairs and periodically prints the Top-K candidates and diagnostics (crowding, clustering, USD exposure). In short: it’s a selector and risk-awareness layer built to avoid herd trades and surface the most structurally self-contained setups.


Code
// TGr06B_CrowdAverse_v2.cpp - Zorro64 Strategy DLL (C++)
// Strategy B v2: Crowd-Averse with Enhanced Graph Architecture

#include <zorro.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>

#define INF 1e30
#define EPS 1e-12

const char* ASSET_NAMES[] = {
  "EURUSD","GBPUSD","USDCHF","USDJPY","AUDUSD","AUDCAD","AUDCHF","AUDJPY","AUDNZD",
  "CADJPY","CADCHF","EURAUD","EURCAD","EURCHF","EURGBP","EURJPY","EURNZD","GBPAUD",
  "GBPCAD","GBPCHF","GBPJPY","GBPNZD","NZDCAD","NZDCHF","NZDJPY","NZDUSD","USDCAD"
};
#define N_ASSET_NAMES 28
#define N_ASSET_NAMES 28

static const int FEAT_N = 9;
static const int FEAT_WINDOW = 200;
static const int META_WINDOW = 200;
static const int UPDATE_EVERY = 5;
static const int TOP_K = 5;

static const double alpha = 1.0;
static const double beta  = 4.0;
static const double gamma = 1.5;
static const double LAMBDA_META = 0.7;

static double clamp01(double x) { if(x<0) return 0; if(x>1) return 1; return x; }
static double sigmoid(double x) { if(x > 30) return 1.0; if(x < -30) return 0.0; return 1.0/(1.0 + exp(-x)); }

static const char* CURRENCY_BASE[] = { "EUR","GBP","USD","CHF","JPY","AUD","CAD","NZD" };
static const int N_CURRENCIES = 8;

static int getCurrencyExposure(int pairIdx, int ccy) {
  const char* pair = ASSET_NAMES[pairIdx];
  char base[4] = {pair[0], pair[1], pair[2], 0};
  char quote[4] = {pair[3], pair[4], pair[5], 0};
  if(strcmp(base, CURRENCY_BASE[ccy]) == 0) return 1;
  if(strcmp(quote, CURRENCY_BASE[ccy]) == 0) return -1;
  return 0;
}

static double exposureDist(int i, int j) {
  double dist = 0.0;
  for(int c=0; c<N_CURRENCIES; c++) {
    int ei = getCurrencyExposure(i, c);
    int ej = getCurrencyExposure(j, c);
    if(ei != 0 && ej != 0) dist += (ei == ej) ? 0.0 : 1.0;
    else if(ei != 0 || ej != 0) dist += 0.5;
  }
  return dist / (double)N_CURRENCIES;
}

class RollingBuffer {
public:
  int cap = 0, n = 0, head = 0;
  double* x = 0;
  RollingBuffer() {}
  ~RollingBuffer(){ shutdown(); }
  void init(int L){ shutdown(); cap = L; x = (double*)malloc((size_t)cap*sizeof(double)); if(!x) quit("OOM"); n = 0; head = 0; }
  void shutdown(){ if(x) free(x); x = 0; cap = 0; n = 0; head = 0; }
  void push(double v){ if(!x || cap<=0) return; x[head] = v; head = (head + 1) % cap; if(n < cap) n++; }
  double get(int i) const { int idx = head - 1 - i; while(idx < 0) idx += cap; return x[idx % cap]; }
};

static double corrOf(const RollingBuffer& a, const RollingBuffer& b, int L) {
  if(a.n < L || b.n < L || L < 5) return 0.0;
  double mx=0,my=0;
  for(int i=0;i<L;i++){ mx += a.get(i); my += b.get(i); }
  mx /= (double)L; my /= (double)L;
  double sxx=0, syy=0, sxy=0;
  for(int i=0;i<L;i++){ double dx = a.get(i)-mx; double dy = b.get(i)-my; sxx+=dx*dx; syy+=dy*dy; sxy+=dx*dy; }
  double den = sqrt(sxx*syy);
  if(den <= EPS) return 0.0;
  return sxy/den;
}

class WeightedGraph {
public:
  int n = 0;
  double* d = 0;
  WeightedGraph() {}
  ~WeightedGraph(){ shutdown(); }
  void init(int N){ shutdown(); n = N; d = (double*)malloc((size_t)n*n*sizeof(double)); if(!d) quit("OOM"); reset(); }
  void shutdown(){ if(d) free(d); d = 0; n = 0; }
  inline int pos(int r,int c) const { return r*n + c; }
  void reset(){ for(int r=0;r<n;r++) for(int c=0;c<n;c++) d[pos(r,c)] = (r==c) ? 0.0 : INF; }
  void allPairsShortest(){ for(int k=0;k<n;k++) for(int i=0;i<n;i++) for(int j=0;j<n;j++){ double cand = d[pos(i,k)] + d[pos(k,j)]; if(cand < d[pos(i,j)]) d[pos(i,j)] = cand; } }
  double wienerUndirectedLike() const { double W=0; for(int i=0;i<n;i++) for(int j=i+1;j<n;j++){ double dij = d[pos(i,j)], dji = d[pos(j,i)]; W += 0.5*(dij + dji); } return W; }
  double meanEdgeWeight() const { double sum=0; int cnt=0; for(int i=0;i<n;i++) for(int j=i+1;j<n;j++){ if(d[pos(i,j)] < INF){ sum += d[pos(i,j)]; cnt++; }} return cnt>0 ? sum/cnt : 0.0; }
  double edgeVariance() const { double mean = meanEdgeWeight(); double var=0; int cnt=0; for(int i=0;i<n;i++) for(int j=i+1;j<n;j++){ if(d[pos(i,j)] < INF){ double diff = d[pos(i,j)] - mean; var += diff*diff; cnt++; }} return cnt>0 ? var/cnt : 0.0; }
  double centrality(int node) const { double sum=0; for(int j=0;j<n;j++) if(j!=node && d[pos(node,j)] < INF) sum += d[pos(node,j)]; return sum; }
};

struct PairFeatures {
  double compactness, meanEdge, entropy, volCentrality;
  double regimeProb, pBullNext, entropyStat, bandwidth;
};

class PairAspectGraph {
public:
  WeightedGraph G;
  RollingBuffer feat[FEAT_N];
  RollingBuffer featPrev[FEAT_N];
  PairFeatures pf;
  PairAspectGraph() { pf.compactness=0; pf.meanEdge=0; pf.entropy=0; pf.volCentrality=0; pf.regimeProb=0; pf.pBullNext=0; pf.entropyStat=0; pf.bandwidth=0; }
  void init(){ G.init(FEAT_N); for(int k=0;k<FEAT_N;k++){ feat[k].init(FEAT_WINDOW); featPrev[k].init(FEAT_WINDOW); } }
  void shutdown(){ for(int k=0;k<FEAT_N;k++){ feat[k].shutdown(); featPrev[k].shutdown(); } G.shutdown(); }
  static double corrToDist(double corr){ double a = fabs(corr); if(a > 1.0) a = 1.0; return 1.0 - a; }
  void pushFeature(int k, double v){ feat[k].push(v); }
  void pushFeaturePrev(int k, double v){ featPrev[k].push(v); }
  double computeEntropy(int L) {
    if(feat[0].n < L) return 0.0;
    int bins = 10; int* hist = (int*)calloc(bins, sizeof(int));
    for(int i=0;i<L && i<feat[0].n; i++) { int bin = (int)(feat[0].get(i) * bins); if(bin<0) bin=0; if(bin>=bins) bin=bins-1; hist[bin]++; }
    double H = 0.0;
    for(int b=0;b<bins;b++) if(hist[b]>0) { double p = (double)hist[b]/(double)L; H -= p * log(p + EPS); }
    free(hist); return H;
  }
  void rebuildIfReady(){
    if(feat[0].n < FEAT_WINDOW){ pf.compactness = 0; return; }
    G.reset();
    for(int i=0;i<FEAT_N;i++){
      for(int j=i+1;j<FEAT_N;j++){
        double c = corrOf(feat[i], feat[j], FEAT_WINDOW);
        double w = corrToDist(c);
        if(i >= 7 && j >= 8) {
          double cp = (featPrev[i].n > 10) ? corrOf(featPrev[i], feat[j], min(featPrev[i].n, FEAT_WINDOW)) : 0;
          w = 0.5 * w + 0.5 * corrToDist(cp);
        }
        G.d[G.pos(i,j)] = w; G.d[G.pos(j,i)] = w;
      }
    }
    G.allPairsShortest();
    double W = G.wienerUndirectedLike();
    pf.compactness = 1.0/(1.0 + W);
    pf.meanEdge = G.meanEdgeWeight();
    pf.entropy = G.edgeVariance();
    pf.volCentrality = G.centrality(3) / (FEAT_N - 1);
    if(feat[8].n >= 20) {
      int upNext = 0;
      for(int i=0;i<19;i++) if(feat[0].get(i+1) > 0) upNext++;
      pf.pBullNext = (double)upNext / 19.0;
      pf.entropyStat = computeEntropy(20);
      pf.regimeProb = (pf.entropyStat < 2.0) ? 1.0 : 0.0;
      pf.bandwidth = feat[3].get(0) / (feat[3].get(10) + EPS);
    }
  }
};

class PairUniverseGraph {
public:
  WeightedGraph G;
  double couplingPenalty, clusterScore, usdExposure;
  PairUniverseGraph() : couplingPenalty(0), clusterScore(0), usdExposure(0) {}
  void init(){ G.init(N_ASSET_NAMES); }
  void shutdown(){ G.shutdown(); }
  static double corrToDist(double corr){ double a = fabs(corr); if(a > 1.0) a = 1.0; return 1.0 - a; }
  void rebuild(PairAspectGraph* pairs){
    G.reset();
    for(int i=0;i<N_ASSET_NAMES;i++){
      for(int j=i+1;j<N_ASSET_NAMES;j++){
        double c = corrOf(pairs[i].feat[0], pairs[j].feat[0], META_WINDOW);
        double w = LAMBDA_META * corrToDist(c) + (1.0 - LAMBDA_META) * exposureDist(i,j);
        G.d[G.pos(i,j)] = w; G.d[G.pos(j,i)] = w;
      }
    }
    G.allPairsShortest();
    double W = G.wienerUndirectedLike();
    couplingPenalty = 1.0/(1.0 + W);
    clusterScore = computeClusterScore();
    usdExposure = computeUSDExposure(pairs);
  }
  double computeClusterScore() { int clusters=0; for(int i=0;i<N_ASSET_NAMES;i++){ bool has=false; for(int j=0;j<N_ASSET_NAMES;j++) if(i!=j && G.d[G.pos(i,j)]<0.3){has=true; break;} if(!has) clusters++; } return (double)clusters/N_ASSET_NAMES; }
  double computeUSDExposure(PairAspectGraph* pairs){ double total=0; for(int i=0;i<N_ASSET_NAMES;i++) total += fabs((double)getCurrencyExposure(i,2)) * pairs[i].pf.compactness; return clamp01(total/5.0); }
};

static double computeRegimeCompactness(){ double W = ((Bar % 2)==0)?1.0:2.5; return 1.0/(1.0+W); }

class CrowdAverseStrategyV2 {
public:
  PairAspectGraph pairG[N_ASSET_NAMES];
  PairUniverseGraph metaG;
  int lastUpdateBar;
  CrowdAverseStrategyV2():lastUpdateBar(-999999){}
  void init(){ for(int i=0;i<N_ASSET_NAMES;i++) pairG[i].init(); metaG.init(); }
  void shutdown(){ metaG.shutdown(); for(int i=0;i<N_ASSET_NAMES;i++) pairG[i].shutdown(); }
  void updateFeaturesForAsset(int a){
    asset((char*)ASSET_NAMES[a]);
    vars C = series(priceClose(0));
    if(Bar < 20) return;
    double c0=C[0], c1=C[1], c12=C[12];
    double ret1=log(c0/c1), retN=log(c0/c12);
    double ma3=(C[0]+C[1]+C[2])/3.0, ma12=0; for(int i=0;i<12;i++) ma12+=C[i]; ma12/=12.0;
    double slope=ma3-ma12;
    double m=0,s=0; for(int i=0;i<20;i++){double ri=log(C[i]/C[i+1]); m+=ri;} m/=20.0;
    for(int i=0;i<20;i++){double ri=log(C[i]/C[i+1]),d=ri-m; s+=d*d;} double vol=sqrt(s/20.0);
    double rsiProxy=tanh(10.0*retN);
    double m20=0,s20=0; for(int i=0;i<20;i++) m20+=C[i]; m20/=20.0;
    for(int i=0;i<20;i++){double d=C[i]-m20; s20+=d*d;} s20=sqrt(s20/20.0)+1e-12;
    double rangePress=tanh(0.5*(c0-m20)/s20);
    double mv20=0,sv20=0; for(int i=0;i<20;i++) mv20+=(C[i]-C[i+1]); mv20/=20.0;
    for(int i=0;i<20;i++){double d=(C[i]-C[i+1])-mv20; sv20+=d*d;} double volOfVol=sqrt(sv20/20.0);
    double flowProxy=fabs(ret1);
    int up=0; for(int i=0;i<20;i++) if(C[i]>C[i+1]) up++;
    double markovProxy=2.0*((double)up/20.0-0.5);
    pairG[a].pushFeature(0,ret1); pairG[a].pushFeature(1,retN); pairG[a].pushFeature(2,slope);
    pairG[a].pushFeature(3,vol); pairG[a].pushFeature(4,retN); pairG[a].pushFeature(5,rsiProxy);
    pairG[a].pushFeature(6,rangePress); pairG[a].pushFeature(7,volOfVol); pairG[a].pushFeature(8,markovProxy);
    pairG[a].pushFeaturePrev(0,ret1);
  }
  void onBar(){
    for(int a=0;a<N_ASSET_NAMES;a++) updateFeaturesForAsset(a);
    if(UPDATE_EVERY>1 && (Bar%UPDATE_EVERY)!=0) return;
    if(lastUpdateBar==Bar) return;
    lastUpdateBar=Bar;
    for(int a=0;a<N_ASSET_NAMES;a++) pairG[a].rebuildIfReady();
    metaG.rebuild(pairG);
    double Creg=computeRegimeCompactness();
    double score[N_ASSET_NAMES]; int idx[N_ASSET_NAMES];
    for(int a=0;a<N_ASSET_NAMES;a++){
      double CA=pairG[a].pf.compactness;
      double x=alpha*Creg + gamma*CA - beta*metaG.couplingPenalty;
      score[a]=sigmoid(x); idx[a]=a;
    }
    for(int i=0;i<TOP_K;i++) for(int j=i+1;j<N_ASSET_NAMES;j++) if(score[idx[j]]>score[idx[i]]){int t=idx[i]; idx[i]=idx[j]; idx[j]=t;}
    if((Bar%50)==0){
      printf("\n[CrowdAverse_v2] Bar=%d Creg=%.4f Pcouple=%.4f Cluster=%.4f USDExp=%.4f",Bar,Creg,metaG.couplingPenalty,metaG.clusterScore,metaG.usdExposure);
      for(int k=0;k<TOP_K;k++){int a=idx[k]; printf("\n  #%d %s CA=%.4f Ent=%.4f BW=%.4f Score=%.4f",k+1,ASSET_NAMES[a],pairG[a].pf.compactness,pairG[a].pf.entropyStat,pairG[a].pf.bandwidth,score[a]);}
    }
  }
};

static CrowdAverseStrategyV2* S = 0;

DLLFUNC void run(){
  if(is(INITRUN)){BarPeriod=60; LookBack=max(LookBack,FEAT_WINDOW+50); asset((char*)ASSET_NAMES[0]); if(!S){S=new CrowdAverseStrategyV2();S->init();}}
  if(is(EXITRUN)){if(S){S->shutdown(); delete S; S=0;} return;}
  if(!S || Bar<LookBack) return;
  S->onBar();
}