MomentumBias Nexus is a multi-asset ranking and selection strategy designed for a broad FX universe (28 major and cross pairs). Instead of placing trades directly, it behaves like a “market scanner” that continuously evaluates each pair’s current quality and then surfaces the most attractive candidates. The core idea is to combine structure (how coherent the internal features of a pair look) with environment (how tightly the whole universe is moving together), and then apply a strict momentum gate so the system only considers pairs that are already moving in the preferred direction.

On every bar (set to hourly bars via BarPeriod = 60), the strategy updates a compact set of nine features per asset. These features capture short-term return, multi-bar return, moving-average slope, volatility, a momentum proxy, an RSI-like nonlinear transform, a Bollinger-style position measure, a simple “flow” proxy (absolute 1-bar move), and a Markov-style persistence proxy (how often the last 20 closes were higher than the previous close). Each feature is stored in rolling buffers, allowing the strategy to measure how these signals behave together over time.

The unique part is the use of graph-based “compactness.” For each asset, the strategy builds a feature graph where edges represent how strongly two features are correlated over a window (200 bars). High correlation reduces edge distance, low correlation increases it. By running an all-pairs shortest-path calculation, the strategy produces a single compactness value: a higher compactness implies the features are behaving in a more internally consistent, mutually reinforcing way, which is treated as a favorable condition.

In parallel, a meta-graph is constructed across all assets using correlations of the primary return feature between pairs. This yields a “coupling penalty” that rises when the universe becomes tightly synchronized (a regime where diversification and pair selection advantages may shrink). The final score blends three forces: (1) a regime compactness term, (2) the asset’s own compactness, and (3) the universe coupling penalty (subtracted). Finally, a momentum filter requires the most recent 1-bar log return to be positive; otherwise the asset receives a zero score regardless of other conditions.

Every few bars, the strategy ranks all pairs by a sigmoid-compressed score and prints the top candidates, producing a disciplined, momentum-aligned shortlist for execution or downstream portfolio logic.

Code
// TGr06E_MomentumBias.cpp - Zorro64 Strategy DLL (C++)
// Strategy E: Momentum-Biased
// Adds momentum filter: only trades pairs with positive Ret1.

#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;

// Standard weights, momentum filter applied in scoring
static const double alpha = 2.0;
static const double beta  = 1.5;
static const double gamma = 2.0;
static const double MOMENTUM_THRESHOLD = 0.0;

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;
  double momentum = 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);
    momentum = feat[0].get(0);
  }
};

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 MomentumBiasStrategy {
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 mom = pairG[a].momentum;
      double momFactor = (mom > MOMENTUM_THRESHOLD) ? 1.0 : 0.0;
      double x = (alpha*Creg + gamma*CA - beta*Pcouple) * momFactor;
      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[MomentumBias] 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 Mom=%.4f Score=%.4f",
          k+1, ASSET_NAMES[a], pairG[a].compactness, pairG[a].momentum, score[a]);
      }
    }
  }
};

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

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

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

  S->onBar();
}