Risk-based position sizing: replace Inp_Lot+Inp_RiskFraction with Inp_RiskPerTrade+Inp_RiskTotal, proper CalcRiskLot() using tickValue/tickSize, total risk cap, track lot/riskAmount per trade

This commit is contained in:
pietro_giacobazzi
2026-06-14 09:55:51 +02:00
parent e2ca6336af
commit e7d38ec937
2 changed files with 155 additions and 97 deletions
+126 -68
View File
@@ -73,6 +73,8 @@ struct TrackedTrade {
double entryZScores[MAX_AGENTS];
double entryFeatures[NN_FEATURES];
double slPrice;
double lot;
double riskAmount;
bool isBuy;
double highestPrice;
double lowestPrice;
@@ -166,7 +168,8 @@ private:
int m_nnEpochs;
double m_nnLR;
int m_nnHidden;
double m_riskFraction;
double m_riskPerTrade;
double m_riskTotal;
string m_symbol;
// --- History cache ---
@@ -250,11 +253,11 @@ public:
for(int i=0; i<maxOpenTrades; i++) openTrades[i].active = false;
}
Orchestrator(double minZ=0.5, double wMin=0.05, double wAlpha=0.05,
bool useNeural=false, string modelFile="",
bool trainMode=false, int nnEpochs=100,
double nnLR=0.001, int nnHidden=6,
double riskFraction=0.20) {
Orchestrator(double minZ=0.5, double wMin=0.05, double wAlpha=0.05,
bool useNeural=false, string modelFile="",
bool trainMode=false, int nnEpochs=100,
double nnLR=0.001, int nnHidden=6,
double riskPerTrade=0.01, double riskTotal=0.05) {
agentCount = 0;
combinedZ = 0;
minActionableZ = minZ;
@@ -293,8 +296,9 @@ public:
m_modelFilename = modelFile;
m_nnEpochs = nnEpochs;
m_nnLR = nnLR;
m_nnHidden = nnHidden;
m_riskFraction = riskFraction;
m_nnHidden = nnHidden;
m_riskPerTrade = riskPerTrade;
m_riskTotal = riskTotal;
m_symbol = _Symbol;
m_neuralNet = NULL;
m_trainBuffer = NULL;
@@ -475,60 +479,72 @@ public:
}
}
int AddTrade(int ticket, double price, double atr, double z, bool isBuy) {
// Solo il margine libero limita i trade — nessun cap artificiale
EnsureTradeCapacity();
// Trova slot libero
int idx = -1;
for(int i=0; i<maxOpenTrades; i++) {
if(!openTrades[i].active) { idx = i; break; }
int AddTrade(int ticket, double price, double atr, double z, bool isBuy, double lot) {
// Solo il margine libero limita i trade — nessun cap artificiale
EnsureTradeCapacity();
// Trova slot libero
int idx = -1;
for(int i=0; i<maxOpenTrades; i++) {
if(!openTrades[i].active) { idx = i; break; }
}
if(idx < 0) { // non dovrebbe mai succedere dopo EnsureTradeCapacity
Print("ERROR: slot non disponibile nonostante capacity expansion");
return -1;
}
openTrades[idx].ticket = ticket;
openTrades[idx].entryPrice = price;
openTrades[idx].entryATR = MathMax(atr, 1e-10);
openTrades[idx].entryZ = z;
openTrades[idx].isBuy = isBuy;
openTrades[idx].lot = lot;
openTrades[idx].entryTime = TimeCurrent();
openTrades[idx].highestPrice = price;
openTrades[idx].lowestPrice = price;
openTrades[idx].barsHeld = 0;
openTrades[idx].maeATR = 0;
openTrades[idx].mfeATR = 0;
openTrades[idx].active = true;
for(int i=0; i<agentCount; i++)
openTrades[idx].entryZScores[i] = agents[i].lastZScore;
// Feature vector per NN
for(int f = 0; f < NN_FEATURES; f++) openTrades[idx].entryFeatures[f] = 0.0;
for(int i=0; i<agentCount; i++) {
if(agents[i].name == "Hurst") openTrades[idx].entryFeatures[0] = agents[i].lastZScore;
else if(agents[i].name == "ADX") openTrades[idx].entryFeatures[1] = agents[i].lastZScore;
else if(agents[i].name == "MA") openTrades[idx].entryFeatures[2] = agents[i].lastZScore;
else if(agents[i].name == "Momentum") openTrades[idx].entryFeatures[3] = agents[i].lastZScore;
else if(agents[i].name == "Consensus") openTrades[idx].entryFeatures[4] = agents[i].lastZScore;
else if(agents[i].name == "Hunter") openTrades[idx].entryFeatures[5] = agents[i].lastZScore;
}
if(idx < 0) { // non dovrebbe mai succedere dopo EnsureTradeCapacity
Print("ERROR: slot non disponibile nonostante capacity expansion");
return -1;
}
openTrades[idx].ticket = ticket;
openTrades[idx].entryPrice = price;
openTrades[idx].entryATR = MathMax(atr, 1e-10);
openTrades[idx].entryZ = z;
openTrades[idx].isBuy = isBuy;
openTrades[idx].entryTime = TimeCurrent();
openTrades[idx].highestPrice = price;
openTrades[idx].lowestPrice = price;
openTrades[idx].barsHeld = 0;
openTrades[idx].maeATR = 0;
openTrades[idx].mfeATR = 0;
openTrades[idx].active = true;
openTrades[idx].entryFeatures[6] = SHARED_regimeAgreement;
openTrades[idx].entryFeatures[7] = SHARED_trendStrength;
for(int i=0; i<agentCount; i++)
openTrades[idx].entryZScores[i] = agents[i].lastZScore;
// SL iniziale adattivo
double slWidth = AdaptiveSLWidth();
openTrades[idx].slPrice = isBuy ? price - atr * slWidth : price + atr * slWidth;
// Feature vector per NN
for(int f = 0; f < NN_FEATURES; f++) openTrades[idx].entryFeatures[f] = 0.0;
for(int i=0; i<agentCount; i++) {
if(agents[i].name == "Hurst") openTrades[idx].entryFeatures[0] = agents[i].lastZScore;
else if(agents[i].name == "ADX") openTrades[idx].entryFeatures[1] = agents[i].lastZScore;
else if(agents[i].name == "MA") openTrades[idx].entryFeatures[2] = agents[i].lastZScore;
else if(agents[i].name == "Momentum") openTrades[idx].entryFeatures[3] = agents[i].lastZScore;
else if(agents[i].name == "Consensus") openTrades[idx].entryFeatures[4] = agents[i].lastZScore;
else if(agents[i].name == "Hunter") openTrades[idx].entryFeatures[5] = agents[i].lastZScore;
}
openTrades[idx].entryFeatures[6] = SHARED_regimeAgreement;
openTrades[idx].entryFeatures[7] = SHARED_trendStrength;
// Rischio effettivo in valuta conto per questo trade
double tickValue = SymbolInfoDouble(m_symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(m_symbol, SYMBOL_TRADE_TICK_SIZE);
double slPoints = MathAbs(price - openTrades[idx].slPrice);
if(tickSize > 0 && tickValue > 0 && slPoints > 0)
openTrades[idx].riskAmount = lot * (tickValue / tickSize) * slPoints;
else
openTrades[idx].riskAmount = 0;
// SL iniziale adattivo
double slWidth = AdaptiveSLWidth();
openTrades[idx].slPrice = isBuy ? price - atr * slWidth : price + atr * slWidth;
Print("Trade #", ticket, " ", isBuy ? "BUY" : "SELL",
" entry=", price, " z=", StringFormat("%+.3f", z),
" SL=", StringFormat("%.5f", openTrades[idx].slPrice),
" (", StringFormat("%.1f", slWidth), " ATR)",
" lot=", StringFormat("%.3f", lot),
" risk=", StringFormat("%.2f", openTrades[idx].riskAmount));
return idx;
}
Print("Trade #", ticket, " ", isBuy ? "BUY" : "SELL",
" entry=", price, " z=", StringFormat("%+.3f", z),
" SL=", StringFormat("%.5f", openTrades[idx].slPrice),
" (", StringFormat("%.1f", slWidth), " ATR)");
return idx;
}
void OnTradeOpen(int ticket, double price, double atr) {
AddTrade(ticket, price, atr, combinedZ, combinedZ > 0);
void OnTradeOpen(int ticket, double price, double atr, double lot) {
AddTrade(ticket, price, atr, combinedZ, combinedZ > 0, lot);
LogDecision("ENTRY", combinedZ > 0 ? 1 : -1, 0, price, ticket);
}
@@ -1017,15 +1033,55 @@ UpdateHealth(actualReturn);
return sl * rr;
}
double AdaptiveBaseLot() const {
// Somma del rischio (in valuta conto) di tutte le posizioni aperte
double TotalRiskUsed() const {
double total = 0;
for(int i=0; i<maxOpenTrades; i++)
if(openTrades[i].active) total += openTrades[i].riskAmount;
return total;
}
// Posizione sizing risk-based: calcola lotto da capitale da rischiare ÷ distanza SL
// slPoints = distanza in prezzo dall'entry allo StopLoss (es. 0.00150 per 15 pips EURUSD)
double CalcRiskLot(double zScore, double slPoints) const {
double bal = AccountInfoDouble(ACCOUNT_BALANCE);
double minLot = SymbolInfoDouble(m_symbol, SYMBOL_VOLUME_MIN);
// Lotto base = riskFraction del capitale. Il chiamante scala per |z|.
// Esempio: 20% di 10000$ / 100000 = 0.20 lot, × |z|=1.0 → 0.20 lot.
double lot = bal * m_riskFraction / 100000.0;
if(minLot > 0 && lot < minLot) lot = minLot;
double tickValue = SymbolInfoDouble(m_symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(m_symbol, SYMBOL_TRADE_TICK_SIZE);
double lotStep = SymbolInfoDouble(m_symbol, SYMBOL_VOLUME_STEP);
double lotMin = SymbolInfoDouble(m_symbol, SYMBOL_VOLUME_MIN);
double lotMax = SymbolInfoDouble(m_symbol, SYMBOL_VOLUME_MAX);
// Rischio desiderato per questo trade = bal × riskPerTrade × |z| (confidenza)
double riskAmount = bal * m_riskPerTrade * MathAbs(zScore);
// Cap sul rischio totale (tutte le posizioni aperte insieme)
double maxTotalRisk = bal * m_riskTotal;
double usedRisk = TotalRiskUsed();
double remainingRisk = maxTotalRisk - usedRisk;
if(remainingRisk <= 0) { Print(" Risk budget esaurito (", StringFormat("%.2f", usedRisk), "/", StringFormat("%.2f", maxTotalRisk), ")"); return 0; }
riskAmount = MathMin(riskAmount, remainingRisk);
// Costo in valuta conto per 1 lotto a questa distanza SL
double riskPerLot = 0;
if(tickSize > 0 && tickValue > 0)
riskPerLot = (tickValue / tickSize) * MathAbs(slPoints);
if(riskPerLot <= 0) {
Print(" CalcRiskLot: tickValue=", tickValue, " tickSize=", tickSize, " — impossibile calcolare lotto");
return lotMin;
}
double lot = riskAmount / riskPerLot;
lot = MathRound(lot / lotStep) * lotStep;
lot = MathMax(lotMin, MathMin(lotMax, lot));
Print(" Risk: bal=", StringFormat("%.0f", bal),
" risk=", StringFormat("%.2f", riskAmount),
" SL=", StringFormat("%.5f", slPoints),
" risk/Lot=", StringFormat("%.2f", riskPerLot),
" → lot=", StringFormat("%.4f", lot));
return lot;
}
}
double AutocorrelationQuick() const {
int n = histCount;
@@ -1323,7 +1379,8 @@ UpdateHealth(actualReturn);
FileWriteString(fh, "Generated," + TimeToString(TimeCurrent()) + "\r\n");
FileWriteString(fh, "AgentCount," + (string)agentCount + "\r\n");
FileWriteString(fh, "NeuralMode," + (string)m_useNeural + "\r\n");
FileWriteString(fh, "RiskFraction," + StringFormat("%.4f", m_riskFraction) + "\r\n");
FileWriteString(fh, "RiskPerTrade," + StringFormat("%.4f", m_riskPerTrade) + "\r\n");
FileWriteString(fh, "RiskTotal," + StringFormat("%.4f", m_riskTotal) + "\r\n");
double bal = AccountInfoDouble(ACCOUNT_BALANCE);
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
FileWriteString(fh, "Balance," + StringFormat("%.2f", bal) + "\r\n");
@@ -1377,8 +1434,8 @@ UpdateHealth(actualReturn);
FileWriteString(fh, "TrailTrigger," + StringFormat("%.4f", derivTrailTrig) + "\r\n");
double derivTrailOff = AdaptiveTrailOffset();
FileWriteString(fh, "TrailOffset," + StringFormat("%.4f", derivTrailOff) + "\r\n");
double derivBaseLot = AdaptiveBaseLot();
FileWriteString(fh, "BaseLot," + StringFormat("%.4f", derivBaseLot) + "\r\n");
FileWriteString(fh, "RiskPerTrade," + StringFormat("%.4f", m_riskPerTrade) + "\r\n");
FileWriteString(fh, "RiskTotal," + StringFormat("%.4f", m_riskTotal) + "\r\n");
FileWriteString(fh, "CorrMinSamples," + (string)corrMinSamples + "\r\n");
FileWriteString(fh, "WeightMin," + StringFormat("%.6f", weightMin) + "\r\n");
FileWriteString(fh, "WeightAlpha," + StringFormat("%.6f", weightAlpha) + "\r\n");
@@ -1427,7 +1484,8 @@ UpdateHealth(actualReturn);
FileWriteString(fh, "Inp_TrainMode," + (string)m_trainMode + "\r\n");
FileWriteString(fh, "Inp_TrainEpochs," + (string)m_nnEpochs + "\r\n");
FileWriteString(fh, "Inp_TrainLR," + StringFormat("%.5f", m_nnLR) + "\r\n");
FileWriteString(fh, "Inp_RiskFraction," + StringFormat("%.4f", m_riskFraction) + "\r\n");
FileWriteString(fh, "Inp_RiskPerTrade," + StringFormat("%.4f", m_riskPerTrade) + "\r\n");
FileWriteString(fh, "Inp_RiskTotal," + StringFormat("%.4f", m_riskTotal) + "\r\n");
FileWriteString(fh, "MaxOpenTrades," + (string)maxOpenTrades + "\r\n");
FileClose(fh);
+29 -29
View File
@@ -14,23 +14,23 @@
#include "Agents\MomentumAgent.mqh"
input string Inp_Symbol = "";
input ENUM_TIMEFRAMES Inp_TF = PERIOD_CURRENT; // CURRENT = timeframe del chart
input double Inp_Lot = 0.0; // 0 = adaptive (AdaptiveBaseLot * |z|), >0 = fixed lot
input double Inp_MinZ = 0.0; // 0=adattivo, >0=fisso
input double Inp_WMin = 0.0; // 0 = auto (1/agentCount * 0.1)
input int Inp_BufferBars = 0; // 0 = auto (HurstMaxPeriod * 10)
input double Inp_SLRiskATR = 0.0; // 0=adattivo, >0=fisso
input double Inp_TPRiskATR = 0.0; // 0=adattivo, >0=fisso
input int Inp_HurstPeriod = 0; // 0=auto, >0=fisso
input int Inp_ADXPeriod = 0; // 0=auto, >0=fisso
input bool Inp_UseNeural = false; // true = neural orchestrator, false = softmax
input string Inp_NNModelFile = ""; // modello NN (vuoto = auto Nome_Symbol_TF.dat)
input bool Inp_TrainMode = false; // colleziona campioni e allena NN in backtest
input int Inp_TrainEpochs = 100; // epoche di training
input double Inp_TrainLR = 0.001; // learning rate Adam
input int Inp_NNHidden = 6; // neuroni hidden layer
input double Inp_RiskFraction = 0.20; // frazione di capitale da rischiare per trade
input bool Inp_UseReversalClose = true; // true=chiudi posizioni su segnale opposto, false=long+short coesistono
input ENUM_TIMEFRAMES Inp_TF = PERIOD_CURRENT;
input double Inp_MinZ = 0.0;
input double Inp_WMin = 0.0;
input int Inp_BufferBars = 0;
input double Inp_SLRiskATR = 0.0;
input double Inp_TPRiskATR = 0.0;
input int Inp_HurstPeriod = 0;
input int Inp_ADXPeriod = 0;
input bool Inp_UseNeural = false;
input string Inp_NNModelFile = "";
input bool Inp_TrainMode = false;
input int Inp_TrainEpochs = 100;
input double Inp_TrainLR = 0.001;
input int Inp_NNHidden = 6;
input double Inp_RiskPerTrade = 0.01; // % capitale da rischiare per trade (es. 0.01 = 1%)
input double Inp_RiskTotal = 0.05; // % capitale massima totale in rischio
input bool Inp_UseReversalClose = true;
Orchestrator *orchestrator;
CTrade *trade;
@@ -74,7 +74,7 @@ int OnInit() {
Inp_UseNeural, Inp_NNModelFile,
Inp_TrainMode, Inp_TrainEpochs,
Inp_TrainLR, Inp_NNHidden,
Inp_RiskFraction);
Inp_RiskPerTrade, Inp_RiskTotal);
trade = new CTrade();
dig = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
@@ -347,18 +347,18 @@ void ManagePositions(const MarketData &data, const FinalSignal &fs) {
// SL adattivo via MAE (trailing stop gestisce l'uscita, no TP fisso)
double slWidth = (Inp_SLRiskATR > 0) ? Inp_SLRiskATR : orchestrator.AdaptiveSLWidth();
double sl = (fs.direction == 1) ? price - atr * slWidth
: price + atr * slWidth;
sl = NormalizeDouble(sl, dig);
double sl = (fs.direction == 1) ? price - atr * slWidth
: price + atr * slWidth;
sl = NormalizeDouble(sl, dig);
double slPoints = price - sl;
if(fs.direction == -1) slPoints = sl - price;
if(slPoints <= 0) { Print("SL troppo stretto — skip"); return; }
double lot = (Inp_Lot > 0) ? Inp_Lot : orchestrator.AdaptiveBaseLot() * MathAbs(fs.zScore);
double lotStep = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
double lotMin = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
double lotMax = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
lot = MathRound(lot / lotStep) * lotStep;
lot = MathMax(lotMin, MathMin(lotMax, lot));
// Posizione sizing risk-based tramite Orchestrator
double lot = orchestrator.CalcRiskLot(fs.zScore, slPoints);
if(lot <= 0) { Print("Lot calcolato = 0 — skip"); return; }
// Solo il margine libero limita i trade
// Solo il margine libero limita i trade
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
double marginReq = lot * SymbolInfoDouble(sym, SYMBOL_MARGIN_INITIAL);
if(marginReq >= freeMargin && freeMargin > 0) {
@@ -396,7 +396,7 @@ void ManagePositions(const MarketData &data, const FinalSignal &fs) {
}
if(ticket > 0) {
orchestrator.OnTradeOpen(ticket, price, atr);
orchestrator.OnTradeOpen(ticket, price, atr, lot);
}
}
}