CrowdAverse is a multi-asset selection strategy designed to avoid crowded, highly synchronized markets and instead focus attention on currency pairs whose recent behavior looks more distinctive and internally consistent. It runs as a 64-bit Zorro Strategy DLL and evaluates a fixed universe of 28 major FX pairs on an hourly bar schedule (BarPeriod = 60). Rather than generating direct trade entries in this snippet, it produces a ranked short list (TOP_K = 5) of the most “attractive” pairs according to a crowd-avoidance score.

The strategy builds two layers of graph-based structure. First, each individual pair maintains a rolling feature history (window = 200) across nine engineered signals: short and medium log returns, moving-average slope, realized volatility, momentum, an RSI-like proxy (tanh-scaled return), Bollinger-style position and range pressure, a simple “flow” proxy (absolute 1-bar return), and a Markov-style bias derived from the fraction of up-closes. These features are continuously pushed into rolling buffers, then periodically (every 5 bars) used to compute a pair-specific “aspect graph.” In that graph, edges connect features based on correlation-derived distance (1 ? |corr|). After running an all-pairs shortest-path routine, the graph’s total distance (Wiener-like sum) is inverted into a compactness score: the tighter and more coherent the feature relationships, the higher the compactness.

Second, the strategy builds a meta “pair universe” graph across all pairs, again using correlation-derived distances (here using one key feature series as the coupling proxy). This produces a global coupling penalty that rises when the overall universe becomes tightly coupled (i.e., many pairs move together). That’s the core “crowd-averse” lever: the strategy heavily penalizes selection during periods of widespread synchronization.

Finally, a regime term (a simple placeholder in this version) is combined with pair compactness and the coupling penalty through weighted coefficients (alpha = 1.0, gamma = 1.5, beta = 4.0). The result is passed through a sigmoid to map it into a stable 0–1 score, and the top-ranked pairs are logged periodically. In short: prefer coherent individual opportunities, but step away when the whole market starts marching in lockstep.

Code
// TGr06B_CrowdAverse.cpp - Zorro64 Strategy DLL (C++)
// Strategy B: Crowd-Averse
// Heavily penalizes crowded pairs via high coupling weight.

#include <zorro.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.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

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;

// Crowd-averse weights: high coupling penalty
static const double alpha = 1.0;
static const double beta  = 4.0;
static const double gamma = 1.5;

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));
}

class RollingBuffer {
public:
  int cap = 0;
  int n = 0;
  int 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: RollingBuffer");
    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*(size_t)n*sizeof(double));
    if(!d) quit("OOM: WeightedGraph");
    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)];
        double dji = d[pos(j,i)];
        W += 0.5*(dij + dji);
      }
    return W;
  }
};

class PairAspectGraph {
public:
  WeightedGraph G;
  RollingBuffer feat[FEAT_N];
  double compactness = 0.0;

  PairAspectGraph() {}

  void init(){
    G.init(FEAT_N);
    for(int k=0;k<FEAT_N;k++) feat[k].init(FEAT_WINDOW);
  }

  void shutdown(){
    for(int k=0;k<FEAT_N;k++) feat[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 rebuildIfReady(){
    if(feat[0].n < FEAT_WINDOW) { compactness = 0.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);
        G.d[G.pos(i,j)] = w;
        G.d[G.pos(j,i)] = w;
      }
    }
    G.allPairsShortest();
    double W = G.wienerUndirectedLike();
    compactness = 1.0/(1.0 + W);
  }
};

class PairUniverseGraph {
public:
  WeightedGraph G;
  double couplingPenalty = 0;

  PairUniverseGraph(){}
  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 = corrToDist(c);
        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);
  }
};

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

class CrowdAverseStrategy {
public:
  PairAspectGraph pairG[N_ASSET_NAMES];
  PairUniverseGraph metaG;
  int 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];
    double c1 = C[1];
    double c12 = C[12];

    double ret1 = log(c0/c1);
    double retN = log(c0/c12);
    double ma3 = (C[0]+C[1]+C[2]) / 3.0;
    double 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]);
      double d = ri - m;
      s += d*d;
    }
    double vol = sqrt(s/20.0);

    double mom = retN;
    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 bollPos = (c0 - m20)/s20;
    double rangePress = tanh(0.5*bollPos);
    double flowProxy = fabs(ret1);

    int up=0;
    for(int i=0;i<20;i++){
      if(C[i] > C[i+1]) up++;
    }
    double pBull = (double)up/20.0;
    double markovProxy = 2.0*(pBull - 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, mom);
    pairG[a].pushFeature(5, rsiProxy);
    pairG[a].pushFeature(6, rangePress);
    pairG[a].pushFeature(7, flowProxy);
    pairG[a].pushFeature(8, markovProxy);
  }

  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 Pcouple = metaG.couplingPenalty;

    double score[N_ASSET_NAMES];
    int idx[N_ASSET_NAMES];
    for(int a=0;a<N_ASSET_NAMES;a++){
      double CA = pairG[a].compactness;
      double x = alpha*Creg + gamma*CA - beta*Pcouple;
      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] Bar=%d Creg=%.4f Pcouple=%.4f", Bar, Creg, Pcouple);
      for(int k=0;k<TOP_K;k++){
        int a = idx[k];
        printf("\n  #%d %s  CA=%.4f  Score=%.4f",
          k+1, ASSET_NAMES[a], pairG[a].compactness, score[a]);
      }
    }
  }
};

static CrowdAverseStrategy* 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 CrowdAverseStrategy();
      S->init();
    }
  }

  if(is(EXITRUN))
  {
    if(S){
      S->shutdown();
      delete S;
      S = 0;
    }
    return;
  }

  if(!S) return;
  if(Bar < LookBack) return;

  S->onBar();
}