VWAP Markov State Trading Strategy for Zorro
The Strategy consist of an algorithmic trading strategy that combines VWAP volatility bands, candlestick price action, and Markov state-transition probabilities to identify trading opportunities across 28 currency pairs.

How the strategy works
1. VWAP Analysis: Calculates VWAP and six volatility bands at ±1, ±2, and ±2.5 standard deviations to identify important market price zones.
2. Market Classification: Classifies completed candles according to their bullish, bearish, or doji structure and their positions relative to the VWAP bands.
3. Markov Probability Model: Learns historical transitions between market states and estimates the probability that the next candle will be bullish, bearish, or neutral.
4. Trading Decisions: Supports 36 predefined research strategies, 300 VWAP event/action rules, custom filters, and Markov-directed trading.
5. Risk Management: Includes configurable stop-loss levels, pending entries, VWAP-based exits, and portfolio position limits.

Key technical characteristics
Feature Configuration
Currency pairs 28
Default timeframe 5 minutes (configurable to H4)
Markov states 780
Transition matrix 780 × 780 per pair
Historical learning window 1,024 observations
VWAP period 20
Default execution Market orders
Default stop-loss buffer 10% of candle range

Important observations
Although the filename says Markov720, the actual code defines 780 states: 330 bullish, 330 bearish, and 120 doji.
The current default configuration runs research strategy M00 while the Markov model monitors transitions. Markov-directed trading must be explicitly enabled.
I also noticed that the code's sweep-direction logic does not match your earlier bullish-candle SELL / bearish-candle BUY reversal specification. That should be corrected if the original reversal behavior is intended.

Code
// VWAP_Rulebook_28Pair_Markov780.c

#include <default.c>

// ---- EDIT THESE CONFIGURATION PARAMETERS (native lite-C, no MQL input syntax) ----
#define ENGINE_MODE 0           // 0 M00-M35, 1 Catalog 0..299, 2 S/B custom, 3 Markov
#define RESEARCH_ID 0           // M00..M35
#define CATALOG_ID 0            // 0..299 (100 observable events x BUY/SELL/EXIT)
#define SELL_STATIC_ID 0        // 0..30
#define BUY_STATIC_ID 0         // 0..30
#define SELL_EVENT_ID 1         // 1..12
#define BUY_EVENT_ID 1          // 1..12
#define CANDLE_RULE_ID 0        // 0 disabled, 1..25
#define SIGNATURE_FILTER -1     // -1 disabled; 0..779
#define TIMEFRAME_MINUTES 5     // 5 M5 default, 240 for H4
#define SINGLE_ASSET 0         // 0 = 28 assets, 1 = named symbol only
#define SINGLE_ASSET_NAME "EUR/USD"
#define ASSET_LIST "AssetsForex" // Broker-compatible list MUST contain requested pairs

#define ENTRY_STYLE 0          // 0 market; 1 Zorro-managed Entry stop
#define PENDING_PERCENT 10.0
#define STOP_PERCENT 10.0
#define ENTRY_WAIT_BARS 2
#define ENABLE_BROKER_STOP 1   // Zorro StopFactor=1, when broker plugin supports it
#define ENABLE_SOFTWARE_EXIT 1
#define EXIT_RULE_ID 14        // X01..X20
#define MAX_SPREAD_PIPS 0.0    // 0 disabled
#define ORDER_LOTS 1           // Zorro INTEGER micro/mini/standard lots, NOT MQL "0.01"
#define MAX_PORTFOLIO_POSITIONS 28

#define VWAP_PERIOD 20
#define VWAP_MATCH_MQL 1       // N+1 VWAP bars, N STD bars
#define VWAP_VOLUME_MODE 1     // 1 use marketVol (Zorro S required), 0 unweighted approximation
#define VWAP_PRICE_MODE 0      // 0 close; 1 open; 2 high; 3 low; 4 median; 5 typical; 6 weighted
#define DEVIATION_1 1.0
#define DEVIATION_2 2.0
#define DEVIATION_3 2.5

#define MARKOV_MODE 0          // 0 monitor only, 1 gate existing engines
#define MARKOV_MIN_COUNT 8
#define MARKOV_MIN_DIRECTION 0.50
#define MARKOV_PRIOR 0.01
#define MARKOV_BACKOFF 1
#define MARKOV_BLOCK_SPARSE 0
#define MARKOV_NEED_SWEEP 1
#define MK_WINDOW 1024
#define MAX_PAIRS 28
#define MK_STATES 780

#define Z_THRESHOLD 1.0
#define WICK_THRESHOLD 0.35
#define EPSILON_PIPS 1.0
#define MIN_REWARD_PIPS 1.0
#define MIN_RISK_REWARD 1.0
#define SMALL_CANDLE_SPREAD_MULTIPLE 3.0
#define SWEEP_TIMEOUT_SECONDS 60
#define MIN_RETURN_TICKS 3
#define EXIT_SIGMA_MULTIPLE 1.0
#define EXIT_RANGE_MULTIPLE 1.0
#define EXIT_RANGE_PERCENT 10.0
#define EXIT_WAIT_BARS 3
#define EXIT_DEADLINE_MINUTES 120
#define PARTIAL_PERCENT 50.0
#define TRAIL_SIGMA_MULTIPLE 1.0

#define PH_SWEEP 0
#define PH_RETURN 1
#define PH_READY 2
#define PH_RETEST 3

// Exact 28-currency universe, Zorro's usual slash format.
string ForexNames[28];
void initForexNames()
{
  // lite-C requires explicit initialization for arrays of string pointers.
  ForexNames[0]="EUR/USD";ForexNames[1]="GBP/USD";ForexNames[2]="USD/JPY";
  ForexNames[3]="GBP/JPY";ForexNames[4]="USD/CAD";ForexNames[5]="EUR/AUD";
  ForexNames[6]="EUR/JPY";ForexNames[7]="AUD/CAD";ForexNames[8]="AUD/JPY";
  ForexNames[9]="AUD/NZD";ForexNames[10]="AUD/USD";ForexNames[11]="CAD/JPY";
  ForexNames[12]="EUR/CAD";ForexNames[13]="EUR/CHF";ForexNames[14]="EUR/GBP";
  ForexNames[15]="EUR/NZD";ForexNames[16]="GBP/CAD";ForexNames[17]="GBP/CHF";
  ForexNames[18]="NZD/CAD";ForexNames[19]="NZD/JPY";ForexNames[20]="NZD/USD";
  ForexNames[21]="USD/CHF";ForexNames[22]="CHF/JPY";ForexNames[23]="AUD/CHF";
  ForexNames[24]="GBP/NZD";ForexNames[25]="NZD/CHF";ForexNames[26]="CAD/CHF";
  ForexNames[27]="GBP/AUD";
}

typedef struct
{
  int usable;
  string symbol;
  int valid,signature,dir;
  int phase,crossMask,retouch,quoteKnown,activeBefore;
  int observedBar,lastEntryBar,lastPersistBar;
  int ringHead,ringSize;
  int predictionSource,nEffective,topNext,topCount;
  int seenPosition;
  int partialDone;
  var o,h,l,c,range,body,uw,lw;
  var v,sigma,band[7]; // D3 D2 D1 VWAP U1 U2 U3
  var previousAsk,sweepStamp;
  var probabilityBull,probabilityBear,probabilityDoji;
  var frozenCenter,highWater,lowWater;
  var lastEntryLevel,entryStamp;
} FXSTATE;

FXSTATE FX[MAX_PAIRS];
int* MarkovEdges = 0;
int* MarkovOutgoing = 0;
int* MarkovBull = 0;
int* MarkovBear = 0;
int* MarkovDoji = 0;
int* MarkovRing = 0;
int MarkovDirection[MK_STATES],MarkovCloseZone[MK_STATES];
int ActivePairs = 0;

// ---- Signature ranking: 0..329 bullish, 330..659 bearish, 660..779 doji ----
// a=zone(L), b=zone(min(O,C)), c=zone(max(O,C)), d=zone(H).
int signatureRank(int a,int b,int c,int d,int doji)
{
  int n=0,i,j,k,h;
  if(doji) {
    for(i=0;i<8;i++)for(j=i;j<8;j++)for(k=j;k<8;k++) {
      if(i==a && j==b && k==d)return n;
      n++;
    }
    return -1;
  }
  for(i=0;i<8;i++)for(j=i;j<8;j++)
   for(k=j;k<8;k++)for(h=k;h<8;h++) {
     if(i==a && j==b && k==c && h==d)return n;
     n++;
   }
  return -1;
}

void makeMetadata()
{
  int n=0,i,j,k,h;
  for(i=0;i<8;i++)for(j=i;j<8;j++)
   for(k=j;k<8;k++)for(h=k;h<8;h++) {
     MarkovDirection[n]=1; MarkovCloseZone[n]=k;
     MarkovDirection[330+n]=-1; MarkovCloseZone[330+n]=j;
     n++;
   }
  n=0;
  for(i=0;i<8;i++)for(j=i;j<8;j++)for(k=j;k<8;k++) {
    MarkovDirection[660+n]=0; MarkovCloseZone[660+n]=j;
    n++;
  }
}

int initMarkov()
{
  int edgeCount = ActivePairs*MK_STATES*MK_STATES;
  int rowCount=ActivePairs*MK_STATES;
  MarkovEdges = malloc(edgeCount*sizeof(int));
  MarkovOutgoing = malloc(rowCount*sizeof(int));
  MarkovBull = malloc(rowCount*sizeof(int));
  MarkovBear = malloc(rowCount*sizeof(int));
  MarkovDoji = malloc(rowCount*sizeof(int));
  MarkovRing = malloc(ActivePairs*MK_WINDOW*sizeof(int));
  if(!MarkovEdges || !MarkovOutgoing || !MarkovBull || !MarkovBear ||
     !MarkovDoji || !MarkovRing) return 0;
  memset(MarkovEdges,0,edgeCount*sizeof(int));
  memset(MarkovOutgoing,0,rowCount*sizeof(int));
  memset(MarkovBull,0,rowCount*sizeof(int));
  memset(MarkovBear,0,rowCount*sizeof(int));
  memset(MarkovDoji,0,rowCount*sizeof(int));
  int i;
  for(i=0;i<ActivePairs*MK_WINDOW;i++)MarkovRing[i]=-1;
  makeMetadata();
  return 1;
}
void releaseMarkov()
{
  if(MarkovEdges)free(MarkovEdges);
  if(MarkovOutgoing)free(MarkovOutgoing);
  if(MarkovBull)free(MarkovBull);
  if(MarkovBear)free(MarkovBear);
  if(MarkovDoji)free(MarkovDoji);
  if(MarkovRing)free(MarkovRing);
  MarkovEdges=0;MarkovOutgoing=0;MarkovBull=0;MarkovBear=0;
  MarkovDoji=0;MarkovRing=0;
}
int mkRow(int id,int from) {return id*MK_STATES+from;}
int mkEdge(int id,int from,int to) {return (id*MK_STATES+from)*MK_STATES+to;}
int mkRingIndex(int id,int offset) {return id*MK_WINDOW+offset;}

void adjustTransition(int id,int from,int to,int delta)
{
  if(from<0 || to<0 || from>=MK_STATES || to>=MK_STATES)return;
  int e=mkEdge(id,from,to),r=mkRow(id,from);
  if(delta<0 && MarkovEdges[e]<=0)return;
  MarkovEdges[e]+=delta;MarkovOutgoing[r]+=delta;
  if(to<330)MarkovBull[r]+=delta;
  else if(to<660)MarkovBear[r]+=delta;
  else MarkovDoji[r]+=delta;
}
void addObservation(int id,int sig)
{
  if(sig<-1 || sig>=MK_STATES)return;
  int head=FX[id].ringHead,n=FX[id].ringSize;
  if(n>=MK_WINDOW) {
    int old=MarkovRing[mkRingIndex(id,head)];
    int next=MarkovRing[mkRingIndex(id,(head+1)%MK_WINDOW)];
    adjustTransition(id,old,next,-1);
    head=(head+1)%MK_WINDOW;n--;
  }
  if(n>0) {
    int last=MarkovRing[mkRingIndex(id,(head+n-1)%MK_WINDOW)];
    adjustTransition(id,last,sig,1);
  }
  MarkovRing[mkRingIndex(id,(head+n)%MK_WINDOW)]=sig;
  FX[id].ringHead=head;FX[id].ringSize=n+1;
}

int zoneAt(int id,var p)
{
  int j;for(j=0;j<7;j++)if(p<FX[id].band[j])return j;
  return 7;
}
int signatureOf(int id)
{
  int zl=zoneAt(id,FX[id].l);
  int zb=zoneAt(id,min(FX[id].o,FX[id].c));
  int zt=zoneAt(id,max(FX[id].o,FX[id].c));
  int zh=zoneAt(id,FX[id].h);
  if(FX[id].dir==0)return 660+signatureRank(zl,zb,zt,zh,1);
  if(FX[id].dir>0)return signatureRank(zl,zb,zt,zh,0);
  return 330+signatureRank(zl,zb,zt,zh,0);
}

void predict(int id)
{
  FXSTATE* F=&FX[id];
  int st=F->signature;
  F->nEffective=0;F->predictionSource=0;
  F->probabilityBull=0;F->probabilityBear=0;F->probabilityDoji=0;
  F->topNext=-1;F->topCount=0;
  if(st<0 || st>=MK_STATES)return;
  int row=mkRow(id,st),n=MarkovOutgoing[row],bull=MarkovBull[row];
  int bear=MarkovBear[row],doji=MarkovDoji[row],source=1,k,r;
  if(MARKOV_BACKOFF && n<MARKOV_MIN_COUNT) {
    n=0;bull=0;bear=0;doji=0;source=2;
    for(k=0;k<MK_STATES;k++)
      if(MarkovDirection[k]==MarkovDirection[st] &&
         MarkovCloseZone[k]==MarkovCloseZone[st]) {
        r=mkRow(id,k);
        n+=MarkovOutgoing[r];bull+=MarkovBull[r];
        bear+=MarkovBear[r];doji+=MarkovDoji[r];
      }
    if(n<MARKOV_MIN_COUNT) {
      n=0;bull=0;bear=0;doji=0;source=3;
      for(k=0;k<MK_STATES;k++)if(MarkovDirection[k]==MarkovDirection[st]) {
        r=mkRow(id,k);
        n+=MarkovOutgoing[r];bull+=MarkovBull[r];
        bear+=MarkovBear[r];doji+=MarkovDoji[r];
      }
    }
    if(n<MARKOV_MIN_COUNT) {
      n=0;bull=0;bear=0;doji=0;source=4;
      for(k=0;k<MK_STATES;k++) {
        r=mkRow(id,k);
        n+=MarkovOutgoing[r];bull+=MarkovBull[r];
        bear+=MarkovBear[r];doji+=MarkovDoji[r];
      }
    }
  }
  F->predictionSource=source;F->nEffective=n;
  var denom=n+MK_STATES*MARKOV_PRIOR;
  if(denom<=0)return;
  F->probabilityBull=(bull+330.0*MARKOV_PRIOR)/denom;
  F->probabilityBear=(bear+330.0*MARKOV_PRIOR)/denom;
  F->probabilityDoji=(doji+120.0*MARKOV_PRIOR)/denom;
  for(k=0;k<MK_STATES;k++) {
    int count=MarkovEdges[mkEdge(id,st,k)];
    if(count>F->topCount) { F->topCount=count;F->topNext=k; }
  }
}
int markovDecision(int id)
{
  FXSTATE* F=&FX[id];
  if(F->nEffective<MARKOV_MIN_COUNT)return 0;
  if(F->probabilityBear>=MARKOV_MIN_DIRECTION &&
     F->probabilityBear>F->probabilityBull)return -1;
  if(F->probabilityBull>=MARKOV_MIN_DIRECTION &&
     F->probabilityBull>F->probabilityBear)return 1;
  return 0;
}
int passMarkov(int id,int side)
{
  if(ENGINE_MODE!=3 && MARKOV_MODE==0)return 1;
  if(FX[id].nEffective<MARKOV_MIN_COUNT)
    return (ENGINE_MODE!=3 && !MARKOV_BLOCK_SPARSE);
  return markovDecision(id)==side;
}

// VWAP original: N+1 prices in numerator but N deviations around same center.
// In Zorro, marketVol may be unavailable without Zorro S or compatible .t6 feed.
// VWAP_VOLUME_MODE=0 intentionally uses equal weights, NOT exact MQL tick-volume VWAP.
// marketVol() in Zorro S can be trade volume, quote rate or historical fVol.
// Matching MQL's iVolume() requires verified matching tick-volume history.
var selectedPrice(int offset)
{
  var o=priceO(offset),h=priceH(offset),l=priceL(offset),c=priceC(offset);
  if(VWAP_PRICE_MODE==1)return o;
  if(VWAP_PRICE_MODE==2)return h;
  if(VWAP_PRICE_MODE==3)return l;
  if(VWAP_PRICE_MODE==4)return (h+l)*0.5;
  if(VWAP_PRICE_MODE==5)return (h+l+c)/3.;
  if(VWAP_PRICE_MODE==6)return (h+l+2*c)/4.;
  return c;
}
int calculateBands(int id)
{
  FXSTATE* F=&FX[id];
  int window=VWAP_PERIOD, j;
  if(VWAP_MATCH_MQL)window++;
  var sv=0,pv=0,p,w;
  for(j=0;j<window;j++) {
    p=selectedPrice(j);
    if(p<=0)return 0;
    w=1;
    if(VWAP_VOLUME_MODE==1)w=marketVol(j);
    if(w<=0)return 0;
    pv+=p*w;sv+=w;
  }
  if(sv<=0)return 0;
  F->v=pv/sv;
  var sq=0;
  for(j=0;j<VWAP_PERIOD;j++) {
    p=selectedPrice(j);
    sq+=(p-F->v)*(p-F->v);
  }
  F->sigma=sqrt(sq/VWAP_PERIOD);
  if(F->sigma<=0)return 0;
  F->band[0]=F->v-DEVIATION_3*F->sigma;
  F->band[1]=F->v-DEVIATION_2*F->sigma;
  F->band[2]=F->v-DEVIATION_1*F->sigma;
  F->band[3]=F->v;
  F->band[4]=F->v+DEVIATION_1*F->sigma;
  F->band[5]=F->v+DEVIATION_2*F->sigma;
  F->band[6]=F->v+DEVIATION_3*F->sigma;
  for(j=1;j<7;j++)if(F->band[j]<=F->band[j-1])return 0;
  return 1;
}
void resetPhase(int id)
{
  FX[id].phase=PH_SWEEP;FX[id].crossMask=0;
  FX[id].sweepStamp=0;FX[id].retouch=0;
}

int findFX(string name)
{
  int id;for(id=0;id<ActivePairs;id++)
    if(FX[id].usable && !strcmp(FX[id].symbol,name))return id;
  return -1;
}
var clockSeconds() {return wdate(0)*86400.;} // simulated/bar clock, not PC wall time
void completeCandle(int id)
{
  FXSTATE* F=&FX[id];
  F->o=priceO(0);F->h=priceH(0);F->l=priceL(0);F->c=priceC(0);
  F->range=F->h-F->l;
  F->valid=0;F->signature=-1;
  F->dir=0;F->body=abs(F->c-F->o);
  F->uw=F->h-max(F->o,F->c);F->lw=min(F->o,F->c)-F->l;
  if(F->c>F->o)F->dir=1; else if(F->c<F->o)F->dir=-1;
  if(F->range>0)F->valid=calculateBands(id);
  if(F->valid)F->signature=signatureOf(id);
  addObservation(id,F->signature);
  predict(id);
  if(!is(LOOKBACK) && F->valid)
    printf("\n%s VWAP %.5f sig %i N %i Pbull %.3f Pbear %.3f",
      F->symbol,F->v,F->signature,F->nEffective,
      F->probabilityBull,F->probabilityBear);
  resetPhase(id);
  F->quoteKnown=0;
  F->observedBar=Bar;
}
int crossedMask(int id,var before,var after)
{
  int k,m=0;
  for(k=0;k<7;k++) {
    var b=FX[id].band[k];
    if(before<b && after>=b)m |= (1<<k);
    if(before>b && after<=b)m |= (1<<(7+k));
  }
  return m;
}
int crossedBit(int level,int up)
{
  if(up)return (1<<level);
  return (1<<(level+7));
}
void sweepEvent(int id,var before,var after,int newQuote)
{
  if(!newQuote || FX[id].dir==0)return;
  FXSTATE* F=&FX[id];
  var t=PIP;
  int m=crossedMask(id,before,after);
  if(F->phase!=PH_SWEEP)F->crossMask |= m;
  if(F->phase==PH_SWEEP) {
    if(F->dir<0 && before<F->h && after>=F->h) {
      F->phase=PH_RETURN;F->sweepStamp=clockSeconds();F->crossMask=m;
    } else if(F->dir>0 && before>=F->l && after<F->l) {
      F->phase=PH_RETURN;F->sweepStamp=clockSeconds();F->crossMask=m;
    }
    return;
  }
  if(ENGINE_MODE==0 && RESEARCH_ID==30 && SWEEP_TIMEOUT_SECONDS>0 &&
      clockSeconds()-F->sweepStamp>SWEEP_TIMEOUT_SECONDS) {
    resetPhase(id);return;
  }
  if(F->phase==PH_RETURN) {
    if(F->dir<0 && after<F->h) {
      F->phase=PH_READY;
      if(ENGINE_MODE==0 && RESEARCH_ID==24)F->phase=PH_RETEST;
    } else if(F->dir>0 && after>F->l) {
      F->phase=PH_READY;
      if(ENGINE_MODE==0 && RESEARCH_ID==24)F->phase=PH_RETEST;
    }
    return;
  }
  if(F->phase==PH_RETEST) {
    if(F->dir<0) {
      if(after>F->h+t) {resetPhase(id);return;}
      if(after>=F->h-t)F->retouch=1;
      if(F->retouch && before>after && after<F->h-t)F->phase=PH_READY;
    } else {
      if(after<F->l-t) {resetPhase(id);return;}
      if(after<=F->l+t)F->retouch=1;
      if(F->retouch && before<after && after>F->l+t)F->phase=PH_READY;
    }
    return;
  }
  if(F->phase==PH_READY) {
    if(F->dir<0 && after>=F->h-t && after<F->h)F->retouch=1;
    if(F->dir>0 && after<=F->l+t && after>F->l)F->retouch=1;
    if(F->dir<0 && after>=F->h)resetPhase(id);
    if(F->dir>0 && after<=F->l)resetPhase(id);
  }
}

// All filters use side=+1 BUY and -1 SELL; candle sign is separate.
int payoffOK(int id,int side,var ask,var minPips)
{
  if(side<0)return (ask-FX[id].v)>=minPips*PIP;
  return (FX[id].v-ask)>=minPips*PIP;
}
int anatomy(int id,int rule,var ask)
{
  FXSTATE* F=&FX[id];var R=F->range,B=F->body;
  var u=F->uw,l=F->lw,o=F->o,c=F->c,h=F->h,lo=F->l;
  if(rule==0)return 1;
  if(R<=0)return 0;
  if(rule==1)return c>o;
  if(rule==2)return c<o;
  if(rule==3)return c==o;
  if(rule==4)return B/R>=0.70;
  if(rule==5)return B/R<=0.20;
  if(rule==6)return u/R>=0.50;
  if(rule==7)return l/R>=0.50;
  if(rule==8)return u>=2*l;
  if(rule==9)return l>=2*u;
  if(rule==10)return (c-lo)/R>=0.90;
  if(rule==11)return (c-lo)/R<=0.10;
  if(rule==12)return (o-lo)/R>=0.90;
  if(rule==13)return (o-lo)/R<=0.10;
  if(rule==14)return min(o,c)>lo+0.5*R;
  if(rule==15)return max(o,c)<lo+0.5*R;
  if(rule==16)return min(o,c)<=lo+0.5*R && max(o,c)>=lo+0.5*R;
  if(rule==17)return abs(u-l)/R<=0.10;
  if(rule==18)return B/R>=0.80;
  if(rule==19) {if(F->dir<0)return u>=B;return l>=B;}
  if(rule==20)return R<=SMALL_CANDLE_SPREAD_MULTIPLE*Spread;
  if(rule==21)return abs(h-o)/R<=0.10 || abs(o-lo)/R<=0.10;
  if(rule==22)return abs(h-c)/R<=0.10 || abs(c-lo)/R<=0.10;
  if(rule==23)return c>o && u>B;
  if(rule==24)return c<o && l>B;
  if(rule==25)return F->sigma>0 && R/F->sigma>=Z_THRESHOLD;
  return 0;
}
int bandRule(int id,int side,int n,var q,var ask)
{
  FXSTATE* F=&FX[id];
  var O=F->o,H=F->h,L=F->l,C=F->c,R=F->range;
  var B=F->body,W=F->lw;if(side<0)W=F->uw;
  var v=F->v,sd=F->sigma,d1=F->band[2],d2=F->band[1],d3=F->band[0];
  var u1=F->band[4],u2=F->band[5],u3=F->band[6];
  var eps=EPSILON_PIPS*PIP;
  if(side<0) {
    if(n==0)return q<=u3;
    if(n==1)return H>=u1;if(n==2)return H>=u2;if(n==3)return H>=u3;
    if(n==4)return C>=v;if(n==5)return C>=u1;
    if(n==6)return O<v && C>=v;
    if(n==7)return O<u1 && C>=u1;
    if(n==8)return O<u2 && C>=u2;
    if(n==9)return H>=u3 && C<u3;
    if(n==10)return H>=u3 && C<u2;
    if(n==11)return H>=u2 && C<u1;
    if(n==12)return H>=u1 && C<v;
    if(n==13)return q>=v && q<=u1;
    if(n==14)return q>=u1 && q<=u2;
    if(n==15)return q>=u2 && q<=u3;
    if(n==16)return q<=v;
    if(n==17)return q>v && q<=u3;
    if(n==18)return H>=u3 && q>v && q<=u2;
    if(n==19)return H>=u3 && q>v && q<=u1;
    if(n==20)return H>=u2 && W>B;
    if(n==21)return H>=u3 && W/R>=0.35;
    if(n==22)return sd>0 && (H-v)/sd>=Z_THRESHOLD;
    if(n==23)return sd>0 && (q-v)/sd>=0.5 && (q-v)/sd<=2.5;
    if(n==24)return abs(q-u2)<=eps;
    if(n==25)return abs(H-u3)<=eps;
    if(n==26)return H>u3 && q<u3;
    if(n==27)return q<u3 && H>=u3 && C>O;
    if(n==28)return sd>0 && R/sd>=Z_THRESHOLD;
    if(n==29)return W/R>=WICK_THRESHOLD && q>v;
    if(n==30)return q>v && payoffOK(id,side,ask,MIN_REWARD_PIPS);
  } else {
    if(n==0)return q>=d3;
    if(n==1)return L<=d1;if(n==2)return L<=d2;if(n==3)return L<=d3;
    if(n==4)return C<=v;if(n==5)return C<=d1;
    if(n==6)return O>v && C<=v;
    if(n==7)return O>d1 && C<=d1;
    if(n==8)return O>d2 && C<=d2;
    if(n==9)return L<=d3 && C>d3;
    if(n==10)return L<=d3 && C>d2;
    if(n==11)return L<=d2 && C>d1;
    if(n==12)return L<=d1 && C>v;
    if(n==13)return q>=d1 && q<=v;
    if(n==14)return q>=d2 && q<=d1;
    if(n==15)return q>=d3 && q<=d2;
    if(n==16)return q>=v;
    if(n==17)return q>=d3 && q<v;
    if(n==18)return L<=d3 && q>=d2 && q<v;
    if(n==19)return L<=d3 && q>=d1 && q<v;
    if(n==20)return L<=d2 && W>B;
    if(n==21)return L<=d3 && W/R>=0.35;
    if(n==22)return sd>0 && (v-L)/sd>=Z_THRESHOLD;
    if(n==23)return sd>0 && (q-v)/sd>=-2.5 && (q-v)/sd<=-0.5;
    if(n==24)return abs(q-d2)<=eps;
    if(n==25)return abs(L-d3)<=eps;
    if(n==26)return L<d3 && q>d3;
    if(n==27)return q>d3 && L<=d3 && C<O;
    if(n==28)return sd>0 && R/sd>=Z_THRESHOLD;
    if(n==29)return W/R>=WICK_THRESHOLD && q<v;
    if(n==30)return q<v && payoffOK(id,side,ask,MIN_REWARD_PIPS);
  }
  return 0;
}
int confirmation(int id,int side,int n,var q)
{
  FXSTATE* F=&FX[id];
  if(n==1)return 1;
  if(n>=2 && n<=5) {
    int b=3;
    if(side<0) {
      if(n==2)b=6;if(n==3)b=5;if(n==4)b=4;
      return (F->crossMask & crossedBit(b,0))!=0;
    }
    if(n==2)b=0;if(n==3)b=1;if(n==4)b=2;
    return (F->crossMask & crossedBit(b,1))!=0;
  }
  if(n>=6 && n<=8) {
    var ratio=0.10;
    if(n==7)ratio=0.25;
    if(n==8)ratio=0.50;
    if(side<0)return q<=F->h-ratio*F->range;
    return q>=F->l+ratio*F->range;
  }
  if(n==9)return F->sweepStamp>0 && !F->retouch &&
     clockSeconds()-F->sweepStamp>=SWEEP_TIMEOUT_SECONDS;
  if(n==10)return F->retouch;
  if(n==11) {
    if(side<0)return (F->crossMask & crossedBit(6,0))!=0 &&
        q>=F->band[5] && q<=F->band[6];
    return (F->crossMask & crossedBit(0,1))!=0 &&
        q>=F->band[0] && q<=F->band[1];
  }
  if(n==12) {
    if(side<0)return (F->crossMask & crossedBit(5,0))!=0 &&
        q>=F->band[4] && q<=F->band[5];
    return (F->crossMask & crossedBit(1,1))!=0 &&
        q>=F->band[1] && q<=F->band[2];
  }
  return 0;
}
var protectiveStop(int id,int side)
{
  if(side<0)return FX[id].h+STOP_PERCENT*0.01*FX[id].range;
  return FX[id].l-STOP_PERCENT*0.01*FX[id].range;
}
int currencyConflict(int id)
{
  // All 28 are known 6-letter FX roots. Split by currency strings.
  int k;
  string a=ForexNames[id];
  string base=strmid(a,0,3),quote=strmid(a,4,3);
  for(k=0;k<ActivePairs;k++) {
    if(k==id || !FX[k].usable)continue;
    if(!asset(FX[k].symbol))continue;
    algo("MK780");
    if(NumOpenLong+NumOpenShort<=0)continue;
    string b=strmid(ForexNames[k],0,3),q=strmid(ForexNames[k],4,3);
    if(!strcmp(base,b) || !strcmp(base,q) ||
       !strcmp(quote,b) || !strcmp(quote,q)) {
      asset(FX[id].symbol);algo("MK780");return 1;
    }
  }
  asset(FX[id].symbol);algo("MK780");
  return 0;
}
int researchGate(int id,int n,int side,var q,var ask)
{
  FXSTATE* F=&FX[id];int s=(side<0);
  var o=F->o,h=F->h,l=F->l,c=F->c,r=F->range;
  var u1=F->band[4],u2=F->band[5],u3=F->band[6];
  var d1=F->band[2],d2=F->band[1],d3=F->band[0],v=F->v;
  var wick=F->lw;if(s)wick=F->uw;
  if(n==0 || n==26)return bandRule(id,side,0,q,ask);
  if(n>=1 && n<=3)return bandRule(id,side,n,q,ask);
  if(n==4) {if(s)return h>=u3 && q<u3;return l<=d3 && q>d3;}
  if(n==5 || n==19) {if(s)return h>=u3 && q<=u2;return l<=d3 && q>=d2;}
  if(n==6 || n==20) {if(s)return h>=u3 && q<=u1;return l<=d3 && q>=d1;}
  if(n==7) {if(s)return h>=u2 && wick>F->body;return l<=d2 && wick>F->body;}
  if(n==8) {if(s)return h>=u3 && wick/r>=0.35;return l<=d3 && wick/r>=0.35;}
  if(n==9) {if(s)return h>=u2 && c<u1;return l<=d2 && c>d1;}
  if(n==10) {if(s)return o<u1 && c>=u1;return o>d1 && c<=d1;}
  if(n==11) {if(s)return o<u2 && c>=u2;return o>d2 && c<=d2;}
  if(n==12) {if(s)return q>v && q<=u1;return q>=d1 && q<v;}
  if(n==13) {if(s)return q>=u1 && q<=u2;return q>=d2 && q<=d1;}
  if(n==14) {if(s)return q>=u2 && q<=u3;return q>=d3 && q<=d2;}
  if(n==15)return payoffOK(id,side,ask,MIN_REWARD_PIPS);
  if(n==16)return r/F->sigma>=1.;
  if(n==17)return r/F->sigma<=1.;
  if(n==18) {if(s)return h>=u3 && wick/r>=0.4;return l<=d3 && wick/r>=0.4;}
  if(n>=21 && n<=23) {
    if(s && h<u3)return 0;
    if(!s && l>d3)return 0;
    return confirmation(id,side,n-19,q);
  }
  if(n==24) {
    if(s)return h>=u3 && F->retouch && F->phase==PH_READY;
    return l<=d3 && F->retouch && F->phase==PH_READY;
  }
  if(n==25) {if(s)return h>=u3 && q<=h-0.1*r;return l<=d3 && q>=l+0.1*r;}
  if(n==27) {
    var stop=protectiveStop(id,side);
    var risk=ask-stop,reward=v-ask;
    if(s){risk=stop-ask;reward=ask-v;}
    return risk>0 && reward>0 && reward/risk>=MIN_RISK_REWARD;
  }
  if(n==28) {
    if(s)return h>=u3 && c>=u2 && zoneAt(id,q)==6;
    return l<=d3 && c<=d2 && zoneAt(id,q)==1;
  }
  if(n==29) {
    if(s)return h>=u3 && c<u2 && zoneAt(id,q)==5;
    return l<=d3 && c>d2 && zoneAt(id,q)==2;
  }
  if(n==30) {
    if(s && h<u3)return 0;if(!s && l>d3)return 0;
    return clockSeconds()-F->sweepStamp<=60.;
  }
  if(n==31) {
    if(s)return h>=u3 && q<=h-MIN_RETURN_TICKS*PIP;
    return l<=d3 && q>=l+MIN_RETURN_TICKS*PIP;
  }
  if(n==32) {
    if(s)return h>=u3 && Spread<=PIP;
    return l<=d3 && Spread<=PIP;
  }
  if(n==33) {
    if(s && h<u3)return 0;if(!s && l>d3)return 0;
    return !currencyConflict(id);
  }
  if(n==34 || n==35) {if(s)return h>=u3;return l<=d3;}
  return 0;
}

// Universal 300-event event catalog from uploaded MQL4:
// event=CATALOG_ID/3 (0..99), action=CATALOG_ID%3 (BUY/SELL/EXIT).
int catalogEvent(int id,var prior,var current,int observed,int newBar)
{
  FXSTATE* F=&FX[id];
  int evt=CATALOG_ID/3;
  int beforeZone=7-zoneAt(id,prior),nowZone=7-zoneAt(id,current);
  int bandIndex, up, source,dest,counter;
  var line;
  if(evt<8)return observed && beforeZone!=nowZone && nowZone==evt;
  if(evt<22) {
    if(!observed)return 0;
    bandIndex=(evt-8)/2;up=((evt-8)%2)==0;
    line=F->band[6-bandIndex];
    if(up)return prior<=line && current>line;
    return prior>=line && current<line;
  }
  if(evt<36) {
    if(!newBar)return 0;
    bandIndex=(evt-22)/2;up=((evt-22)%2)==0;
    line=F->band[6-bandIndex];
    if(up)return F->h>=line && F->c<line;
    return F->l<=line && F->c>line;
  }
  if(evt<92) {
    if(!observed || beforeZone==nowZone)return 0;
    counter=evt-36;
    for(source=0;source<8;source++)for(dest=0;dest<8;dest++) {
      if(source==dest)continue;
      if(counter==0)return beforeZone==source && nowZone==dest;
      counter--;
    }
    return 0;
  }
  // One persistence signal per completed bar after two observations in same zone.
  if(!observed || nowZone!=evt-92 || beforeZone!=nowZone ||
     F->lastPersistBar==Bar)return 0;
  F->lastPersistBar=Bar;return 1;
}
int effectiveExit()
{
  if(ENGINE_MODE==0 && (RESEARCH_ID==18 || RESEARCH_ID==19))return 2;
  if(ENGINE_MODE==0 && RESEARCH_ID==34)return 1;
  return EXIT_RULE_ID;
}
var exitTarget(int id,int side,var entry,int rule)
{
  FXSTATE* F=&FX[id];
  var v=F->v,sd=F->sigma,r=F->range;
  if(rule==2) {if(side<0)return F->band[4];return F->band[2];}
  if(rule==3) {if(side<0)return F->band[5];return F->band[1];}
  if(rule==4) {if(side<0)return F->band[2];return F->band[4];}
  if(rule==5) {if(side<0)return F->band[1];return F->band[5];}
  if(rule==6) {if(side<0)return F->band[0];return F->band[6];}
  if(rule==7)return (entry+v)*0.5;
  if(rule==8)return entry+side*EXIT_SIGMA_MULTIPLE*sd;
  if(rule==9)return entry+side*EXIT_RANGE_MULTIPLE*r;
  if(rule==10)return v+side*EXIT_RANGE_PERCENT*0.01*r;
  return v;
}
int crossedExit(int side,var entry,var price,var target,int allowAdverse)
{
  if(side<0) {
    if(target<entry)return price<=target;
    return allowAdverse && price>=target;
  }
  if(target>entry)return price>=target;
  return allowAdverse && price<=target;
}
int managePosition(int id,int newBar)
{
  FXSTATE* F=&FX[id];
  int rule=effectiveExit();
  int wanted=0;
  // for(current_trades) loops only current Asset AND Algo.
  for(current_trades) {
    if(!TradeIsOpen || TradeIsPhantom)continue;
    int side=1;
    if(TradeIsShort)side=-1;
    var entry=TradePriceOpen; // Zorro's ask-based entry reference
    var ask=AskPrice,bid=BidPrice;
    var target=exitTarget(id,side,entry,rule);
    if(ENGINE_MODE==0 && RESEARCH_ID==34 && F->frozenCenter>0)
      target=F->frozenCenter;
    wanted=0;
    if(rule>=1 && rule<=15) {
      if(rule!=15 || TradeBars>=EXIT_WAIT_BARS)
        if(side<0)wanted=crossedExit(side,entry,ask,target,rule==14);
        else wanted=crossedExit(side,entry,bid,target,rule==14);
    }
    if(rule==16)wanted=(TradeBars*TIMEFRAME_MINUTES>=EXIT_DEADLINE_MINUTES);
    if(rule==17 && newBar)wanted=(F->dir==-side);
    if(rule==18)wanted=(F->dir==-side && F->phase==PH_READY);
    if(rule==19) {
      if(!F->partialDone) {
        var first=exitTarget(id,side,entry,2);
        int hitFirst=0;
        if(side<0)hitFirst=crossedExit(side,entry,ask,first,0);
        else hitFirst=crossedExit(side,entry,bid,first,0);
        if(hitFirst) {
          int partial=(int)(TradeLots*PARTIAL_PERCENT/100.);
          if(partial>=1 && TradeLots-partial>=1) {
            if(exitTrade(ThisTrade,0,partial))F->partialDone=1;
          } else F->partialDone=1;
        }
      }
      if(F->partialDone)
        { if(side<0)wanted=crossedExit(side,entry,ask,F->v,0);
          else wanted=crossedExit(side,entry,bid,F->v,0); }
    }
    if(rule==20) {
      var gap=TRAIL_SIGMA_MULTIPLE*F->sigma;
      if(F->highWater==0 || bid>F->highWater)F->highWater=bid;
      if(F->lowWater==0 || ask<F->lowWater)F->lowWater=ask;
      if(side>0 && F->highWater>entry+gap)
        wanted=(bid<=max(F->v-gap,F->highWater-gap));
      if(side<0 && F->lowWater<entry-gap)
        wanted=(ask>=min(F->v+gap,F->lowWater+gap));
    }
    if(wanted && ENABLE_SOFTWARE_EXIT)
      exitTrade(ThisTrade); // auto-retry on subsequent quote if still open
  }
  return wanted;
}
int countPortfolio()
{
  int n=0;
  for(open_trades)
    if(TradeIsOpen && !TradeIsPhantom && !strcmp(TradeAlgo,"MK780"))n++;
  return n;
}
int entryEligible(int id,int side,var ask)
{
  if(!FX[id].valid || FX[id].range<=0)return 0;
  if(NumOpenLong+NumOpenShort+NumPendingLong+NumPendingShort>0)return 0;
  if(countPortfolio()>=MAX_PORTFOLIO_POSITIONS)return 0;
  var spreadLimit=MAX_SPREAD_PIPS;
  if(spreadLimit>0 && Spread>spreadLimit*PIP)return 0;
  if(side<0 && protectiveStop(id,side)<=ask+PIP)return 0;
  if(side>0 && protectiveStop(id,side)>=ask-Spread-PIP)return 0;
  return 1;
}
int placeSignal(int id,int side,var ask)
{
  FXSTATE* F=&FX[id];
  if(!entryEligible(id,side,ask))return 0;
  var stop=protectiveStop(id,side);
  var trigger=0;
  if(ENTRY_STYLE==1) {
    if(side<0)trigger=F->h-PENDING_PERCENT*0.01*F->range;
    else trigger=F->l+PENDING_PERCENT*0.01*F->range;
    // Zorro Entry is always an ASK price; SELL stop needs ask conversion.
    // Current Spread is only an estimate of future spread at execution.
    if(side<0)trigger+=Spread;
    if(side<0 && !(trigger<ask-PIP))return 0;
    if(side>0 && !(trigger>ask+PIP))return 0;
  }
  Entry=trigger;
  Stop=stop; // absolute ASK-level; original MQL broker stops may differ
  TakeProfit=0; // software VWAP exits, do not set a wrong-side TP
  StopFactor=0;
  if(ENABLE_BROKER_STOP)StopFactor=1;
  EntryTime=ENTRY_WAIT_BARS;
  MaxLong=1;MaxShort=1;
  Lots=ORDER_LOTS;
  TRADE* tr=0;
  if(side<0)tr=enterShort();
  else tr=enterLong();
  Entry=0;Stop=0;TakeProfit=0;
  if(!tr)return 0;
  F->activeBefore=1;
  F->frozenCenter=F->v;
  F->highWater=0;F->lowWater=0;
  F->partialDone=0;
  F->lastEntryBar=Bar;
  F->lastEntryLevel=trigger;
  F->entryStamp=clockSeconds();
  resetPhase(id);
  return 1;
}
void cancelOldPending()
{
  // Called after a new completed candle (reference levels changed).
  for(current_trades) {
    if(TradeIsPending)exitTrade(ThisTrade);
  }
}
void onQuote(int id,int newBar)
{
  FXSTATE* F=&FX[id];
  if(!F->usable || !F->valid || F->signature<0)return;
  // Set the current asset/algo context before invoking trade statistics/functions.
  if(!asset(F->symbol))return;
  algo("MK780");
  var ask=AskPrice;
  if(ask<=0)return;
  var spreadLimit=MAX_SPREAD_PIPS;
  if(spreadLimit>0 && Spread>spreadLimit*PIP)return;
  int present=NumOpenLong+NumOpenShort+NumPendingLong+NumPendingShort;
  if(F->activeBefore && present==0) {
    resetPhase(id);
    F->quoteKnown=0;F->activeBefore=0;
    F->partialDone=0;F->frozenCenter=0;
  }
  if(present>0)F->activeBefore=1;

  int observed=F->quoteKnown && abs(ask-F->previousAsk)>PIP*0.001;
  var prev=F->previousAsk;
  F->previousAsk=ask;
  F->quoteKnown=1;

  if(observed && (present==0 || effectiveExit()==18))
    sweepEvent(id,prev,ask,observed);

  int cat=0;
  if(ENGINE_MODE==1)cat=catalogEvent(id,prev,ask,observed,newBar);
  if(NumOpenLong+NumOpenShort>0) {
    if(ENGINE_MODE==1 && cat && CATALOG_ID%3==2) {
      exitLong();exitShort();
      return;
    }
    managePosition(id,newBar);
    return;
  }
  if(NumPendingLong+NumPendingShort>0)return;
  if(is(LOOKBACK))return;
  if(SIGNATURE_FILTER>=0 && F->signature!=SIGNATURE_FILTER)return;
  if(!anatomy(id,CANDLE_RULE_ID,ask))return;

  if(ENGINE_MODE==3 && !MARKOV_NEED_SWEEP) {
    if(F->lastEntryBar==Bar)return;
    int decision=markovDecision(id);
    if(decision && passMarkov(id,decision))placeSignal(id,decision,ask);
    return;
  }
  if(ENGINE_MODE==1) {
    if(!cat || CATALOG_ID%3==2)return;
    int side=-1;
    if(CATALOG_ID%3==0)side=1;
    if(passMarkov(id,side))placeSignal(id,side,ask);
    return;
  }
  if(F->phase!=PH_READY || F->dir==0)return;
  int side=F->dir;
  if(!passMarkov(id,side))return;
  if(ENGINE_MODE==0 && !researchGate(id,RESEARCH_ID,side,ask,ask))return;
  if(ENGINE_MODE==2) {
    if(side<0 && (!bandRule(id,side,SELL_STATIC_ID,ask,ask) ||
                  !confirmation(id,side,SELL_EVENT_ID,ask)))return;
    if(side>0 && (!bandRule(id,side,BUY_STATIC_ID,ask,ask) ||
                  !confirmation(id,side,BUY_EVENT_ID,ask)))return;
  }
  placeSignal(id,side,ask);
}
// Zorro run() is called after each completed bar. In run, [0] is the
// finished bar; in tick, [0] is still changing, and must NOT update VWAP.
void run()
{
  if(is(INITRUN)) {
    initForexNames();
    BarPeriod=TIMEFRAME_MINUTES;
    LookBack=MK_WINDOW+VWAP_PERIOD+40;
    set(TICKS);
    TickTime=0;
    MaxLong=1;MaxShort=1;
    ActivePairs=MAX_PAIRS;
    if(SINGLE_ASSET)ActivePairs=1;
    if(!initMarkov()) {quit("!Cannot allocate 780-state Markov tables");return;}
    int loaded=assetList(ASSET_LIST);
    if(!loaded) {quit("!Asset list unavailable");return;}
    int i;
    for(i=0;i<ActivePairs;i++) {
      FX[i].symbol=ForexNames[i];
      if(SINGLE_ASSET)FX[i].symbol=SINGLE_ASSET_NAME;
      FX[i].usable=0;FX[i].signature=-1;FX[i].lastEntryBar=-1;
      FX[i].lastPersistBar=-1;FX[i].ringHead=0;FX[i].ringSize=0;
      resetPhase(i);
      // Assets must be selected in INITRUN to load/subscribed histories.
      if(asset(FX[i].symbol)) {
        FX[i].usable=1;
        algo("MK780");
      } else
        printf("\nUnavailable asset: %s (check %s)",FX[i].symbol,ASSET_LIST);
    }
    return;
  }
  if(is(EXITRUN)) {releaseMarkov();return;}
  int i;
  for(i=0;i<ActivePairs;i++) {
    if(!FX[i].usable)continue;
    if(!asset(FX[i].symbol))continue;
    algo("MK780");
    // reference bar data + new Markov state, no future data or files.
    completeCandle(i);
    if(!is(LOOKBACK)) {
      cancelOldPending();  // new candle invalidates prior candle's STOP level
      onQuote(i,1);       // completed-candle rejection and X17 exit
    }
  }
}
void tick()
{
  if(is(INITRUN) || is(EXITRUN))return;
  // tick() is called for the subscribed asset that just got a new quote.
  int id=findFX(Asset);
  if(id<0)return;
  onQuote(id,0);
}