#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

// ============================================================================
//  SelfExcitingMomentumCluster  (MQL5 port of the Algobot C# strategy)
//  ---------------------------------------------------------------------------
//  Momentum / trend follower driven by a self-exciting point process (Hawkes).
//  Two directional intensities decay geometrically each bar and jump on a
//  volatility-normalised directional shock (|Close-PrevClose| > EventThreshold
//  * ATR). The NET intensity  Lambda = lambdaUp - lambdaDown  is directional and
//  supplies both the trigger and the direction:
//      * LONG  on a FRESH cross of Lambda above +IgnitionLevel while price > EMA
//      * SHORT on a FRESH cross of Lambda below -IgnitionLevel while price < EMA
//      * Stop-and-reverse on an opposite fresh ignition.
//      * Dissipation exit: flatten when Lambda crosses back through zero against
//        the open trade.
//  Risk: ATR stop / target, break-even + ATR trail on every tick once past +1R.
// ============================================================================

#include <Trade\Trade.mqh>
CTrade trade;

// --- inputs ---
input double EventThreshold = 0.70;   // shock = |Close-PrevClose| > this * ATR
input double Decay          = 0.85;   // per-bar geometric decay of intensities
input double Excitation     = 1.00;   // intensity jump added on each shock
input double IgnitionLevel  = 1.60;   // net-intensity band edge for an ignition
input int    TrendEmaPeriod = 50;     // regime EMA the ignition must agree with
input int    AtrPeriod      = 14;     // ATR length (shock scale + risk)
input double AtrSlMult      = 1.60;   // stop = entry -/+ AtrSlMult * ATR (=1R)
input double AtrTpMult      = 3.20;   // take-profit = entry +/- AtrTpMult * ATR
input double AtrTrailMult   = 1.80;   // trailing distance (ATR) once past +1R
input double Lots           = 0.10;   // fixed lot size
input long   Magic          = 5711;   // magic number

// --- indicator handles ---
int g_atrHandle = INVALID_HANDLE;
int g_emaHandle = INVALID_HANDLE;

// --- self-exciting (Hawkes) state, persisted across bars ---
double   g_lambdaUp   = 0.0;   // intensity of up-shocks
double   g_lambdaDown = 0.0;   // intensity of down-shocks
double   g_prevLambda = 0.0;   // net intensity on the prior bar (fresh-cross)
bool     g_hasPrev    = false;
double   g_atr        = 0.0;   // ATR cached at last bar close (for trailing)

int OnInit()
{
    trade.SetExpertMagicNumber((ulong)Magic);

    g_atrHandle = iATR(_Symbol, _Period, AtrPeriod);
    g_emaHandle = iMA(_Symbol, _Period, TrendEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);

    if(g_atrHandle == INVALID_HANDLE || g_emaHandle == INVALID_HANDLE)
        return INIT_FAILED;

    g_lambdaUp   = 0.0;
    g_lambdaDown = 0.0;
    g_prevLambda = 0.0;
    g_hasPrev    = false;
    g_atr        = 0.0;

    return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
    if(g_atrHandle != INVALID_HANDLE) IndicatorRelease(g_atrHandle);
    if(g_emaHandle != INVALID_HANDLE) IndicatorRelease(g_emaHandle);
}

void OnTick()
{
    // Break-even + ATR trail runs on EVERY tick (tick-by-tick lock).
    ManagePositions();

    // The self-exciting update / signal logic acts once per newly-closed bar.
    if(!IsNewBar()) return;

    if(Bars(_Symbol, _Period) < 2) return;

    // Warmup: mirror the C# `needed = max(TrendEma, AtrPeriod+1) + 3`.
    int needed = MathMax(TrendEmaPeriod, AtrPeriod + 1) + 3;
    if(Bars(_Symbol, _Period) < needed + 2) return;

    // ---- ATR (volatility scale) + regime EMA for the just-closed bar (shift 1) ----
    double atrBuf[], emaBuf[];
    ArraySetAsSeries(atrBuf, true);
    ArraySetAsSeries(emaBuf, true);
    if(CopyBuffer(g_atrHandle, 0, 1, 1, atrBuf) < 1) return;
    if(CopyBuffer(g_emaHandle, 0, 1, 1, emaBuf) < 1) return;

    g_atr       = atrBuf[0];
    double ema  = emaBuf[0];
    double last = iClose(_Symbol, _Period, 1);   // close of just-closed bar

    if(g_atr <= 0.0) return;

    // ---- Advance the self-exciting (Hawkes) intensities by one bar ----
    double ret       = iClose(_Symbol, _Period, 1) - iClose(_Symbol, _Period, 2);
    double shockSize = EventThreshold * g_atr;

    g_lambdaUp   *= Decay;
    g_lambdaDown *= Decay;
    if(MathAbs(ret) > shockSize)
    {
        if(ret > 0.0) g_lambdaUp   += Excitation;
        else          g_lambdaDown += Excitation;
    }

    double lambda = g_lambdaUp - g_lambdaDown;   // net directional intensity

    double thr       = IgnitionLevel;
    bool   crossUp   = g_hasPrev && g_prevLambda <=  thr && lambda >  thr;   // up-cluster ignites
    bool   crossDown = g_hasPrev && g_prevLambda >= -thr && lambda < -thr;   // down-cluster ignites

    g_prevLambda = lambda;
    g_hasPrev    = true;

    bool regimeUp   = last > ema;
    bool regimeDown = last < ema;

    // ---- Dissipation exit: flatten once the net intensity has crossed back
    //      through zero against the open trade ----
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong t = PositionGetTicket(i);
        if(!PositionSelectByTicket(t)) continue;
        if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
        if(PositionGetInteger(POSITION_MAGIC) != Magic)   continue;

        long ptype = PositionGetInteger(POSITION_TYPE);
        if(ptype == POSITION_TYPE_BUY  && lambda <= 0.0) trade.PositionClose(t);
        else if(ptype == POSITION_TYPE_SELL && lambda >= 0.0) trade.PositionClose(t);
    }

    // ---- Fresh-ignition entries with stop-and-reverse ----
    if(crossUp && regimeUp)
    {
        CloseSide(false);                          // close any shorts
        if(!HasSide(true))                         // no long yet
            TryEnter(true, g_atr, lambda);
    }
    else if(crossDown && regimeDown)
    {
        CloseSide(true);                           // close any longs
        if(!HasSide(false))                        // no short yet
            TryEnter(false, g_atr, lambda);
    }
}

// Break-even + ATR trail, evaluated every tick against the cached ATR.
void ManagePositions()
{
    if(g_atr <= 0.0) return;

    double risk = AtrSlMult * g_atr;
    if(risk <= 0.0) return;

    double eps = g_atr * 0.02;

    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong t = PositionGetTicket(i);
        if(!PositionSelectByTicket(t)) continue;
        if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
        if(PositionGetInteger(POSITION_MAGIC) != Magic)   continue;

        long   ptype  = PositionGetInteger(POSITION_TYPE);
        double open   = PositionGetDouble(POSITION_PRICE_OPEN);
        double curSl  = PositionGetDouble(POSITION_SL);
        double curTp  = PositionGetDouble(POSITION_TP);

        if(ptype == POSITION_TYPE_BUY)
        {
            double price  = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            double profit = price - open;
            if(profit < risk) continue;                       // not yet +1R
            double trail  = price - AtrTrailMult * g_atr;
            double newSl  = MathMax(open, trail);             // break-even or better
            if(newSl > curSl + eps)
                trade.PositionModify(t, newSl, curTp);
        }
        else if(ptype == POSITION_TYPE_SELL)
        {
            double price  = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            double profit = open - price;
            if(profit < risk) continue;
            double trail  = price + AtrTrailMult * g_atr;
            double newSl  = MathMin(open, trail);
            if(curSl <= 0.0 || newSl < curSl - eps)
                trade.PositionModify(t, newSl, curTp);
        }
    }
}

// True if a position of the requested side (long/short) exists for this magic.
bool HasSide(bool isLong)
{
    long want = isLong ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong t = PositionGetTicket(i);
        if(!PositionSelectByTicket(t)) continue;
        if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
        if(PositionGetInteger(POSITION_MAGIC) != Magic)   continue;
        if(PositionGetInteger(POSITION_TYPE) == want) return true;
    }
    return false;
}

// Close all positions of the requested side for this magic.
void CloseSide(bool isLong)
{
    long want = isLong ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong t = PositionGetTicket(i);
        if(!PositionSelectByTicket(t)) continue;
        if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
        if(PositionGetInteger(POSITION_MAGIC) != Magic)   continue;
        if(PositionGetInteger(POSITION_TYPE) == want) trade.PositionClose(t);
    }
}

// Open a fresh trade with ATR-based SL/TP.
void TryEnter(bool isLong, double atr, double lambda)
{
    if(AtrSlMult * atr <= 0.0) return;

    double price, sl, tp;

    if(isLong)
    {
        price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        sl    = price - AtrSlMult * atr;
        tp    = price + AtrTpMult * atr;
        if(trade.Buy(Lots, _Symbol, 0.0, sl, tp, "SEMC-long"))
            PrintFormat("SelfExcitingMomentumCluster LONG @ %.5f SL %.5f TP %.5f lambda=%.2f ATR %.5f",
                        price, sl, tp, lambda, atr);
    }
    else
    {
        price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        sl    = price + AtrSlMult * atr;
        tp    = price - AtrTpMult * atr;
        if(trade.Sell(Lots, _Symbol, 0.0, sl, tp, "SEMC-short"))
            PrintFormat("SelfExcitingMomentumCluster SHORT @ %.5f SL %.5f TP %.5f lambda=%.2f ATR %.5f",
                        price, sl, tp, lambda, atr);
    }
}

bool IsNewBar()
{
    static datetime last = 0;
    datetime cur = iTime(_Symbol, _Period, 0);
    if(cur != last){ last = cur; return true; }
    return false;
}
