472 lines
19 KiB
Plaintext
472 lines
19 KiB
Plaintext
#property copyright "MultiAgent Test v7"
|
||
#property version "7.00"
|
||
#property description "6 agenti: Hurst, ADX, Consensus, MA, Momentum, Hunter"
|
||
#property description "Neural orchestrator (CNeuralNet) + Pattern hunter cross-agente"
|
||
|
||
#include <Trade\Trade.mqh>
|
||
#include <Trade\PositionInfo.mqh>
|
||
#include "Core\Orchestrator.mqh"
|
||
#include "Agents\RegimeDetector.mqh"
|
||
#include "Agents\RegimeADX.mqh"
|
||
#include "Agents\RegimeConsensus.mqh"
|
||
#include "Agents\PatternHunter.mqh"
|
||
#include "Agents\MAAgent.mqh"
|
||
#include "Agents\MomentumAgent.mqh"
|
||
|
||
input string Inp_Symbol = ""; // Simbolo da testare (vuoto = simbolo corrente)
|
||
input ENUM_TIMEFRAMES Inp_TF = PERIOD_CURRENT; // Timeframe del test
|
||
input double Inp_MinZ = 0.0; // Min |z| per aprire trade (0 = soglia adattiva automatica)
|
||
input double Inp_WMin = 0.0; // Peso minimo per agente (0 = 1/(N*0.1) automatico)
|
||
input int Inp_BufferBars = 0; // Barre buffer storico (0 = max(HurstPeriod*10, 300))
|
||
input double Inp_SLRiskATR = 0.0; // SL in ATR (0 = SL adattivo basato su MAE storico)
|
||
input double Inp_TPRiskATR = 0.0; // TP in ATR (0 = stesso fattore dello SL)
|
||
input int Inp_HurstPeriod = 0; // Periodo Hurst (0 = 200)
|
||
input int Inp_ADXPeriod = 0; // Periodo ADX (0 = 14)
|
||
input bool Inp_UseNeural = false; // Usa Neural Network invece di softmax pesato
|
||
input string Inp_NNModelFile = ""; // File modello NN da caricare (vuoto = auto)
|
||
input bool Inp_TrainMode = false; // Addestra NN durante il test (colleziona campioni)
|
||
input int Inp_TrainEpochs = 100; // Epoche di training NN per sessione
|
||
input double Inp_TrainLR = 0.001; // Learning rate NN
|
||
input int Inp_NNHidden = 6; // Neuroni hidden layer NN
|
||
input double Inp_RiskPerTrade = 0.01; // % capitale da rischiare per trade (0.01 = 1%)
|
||
input double Inp_RiskTotal = 0.05; // % capitale massima in rischio su TUTTE le posizioni
|
||
input bool Inp_UseReversalClose = true; // Chiudi posizione se segnale opposto (false = long+short coesistono)
|
||
|
||
Orchestrator *orchestrator;
|
||
CTrade *trade;
|
||
CPositionInfo posInfo;
|
||
string sym;
|
||
int dig;
|
||
datetime lastBarTime = 0;
|
||
int lastBarTotal = 0;
|
||
int bufferBars = 0;
|
||
|
||
int magicNumber;
|
||
|
||
// Hash semplice per magic number
|
||
int StringHash(string s) {
|
||
int h = 0;
|
||
int len = StringLen(s);
|
||
for(int i=0; i<len; i++) {
|
||
h = (h * 31 + (int)StringGetCharacter(s, i)) % 999999;
|
||
}
|
||
return h;
|
||
}
|
||
|
||
int OnInit() {
|
||
int nAgents = 6; // Hurst, ADX, MA, Momentum, Consensus, Hunter
|
||
|
||
// Buffer bars auto: HurstMaxPeriod * 10
|
||
int hurstMaxP = (Inp_HurstPeriod > 0) ? Inp_HurstPeriod : 200;
|
||
bufferBars = (Inp_BufferBars > 0) ? Inp_BufferBars : MathMax(300, hurstMaxP * 10);
|
||
|
||
// Peso minimo auto: 1/agentCount * 0.1
|
||
double wMinAuto = 1.0 / nAgents * 0.1;
|
||
double wMin = (Inp_WMin > 0.0) ? Inp_WMin : wMinAuto;
|
||
|
||
sym = (Inp_Symbol == "") ? _Symbol : Inp_Symbol;
|
||
|
||
magicNumber = StringHash(sym + (string)Inp_TF + "MAT") % 999999;
|
||
if(magicNumber < 10000) magicNumber += 10000;
|
||
|
||
// weightAlpha: 1/(N+10) per N agenti
|
||
orchestrator = new Orchestrator(Inp_MinZ, wMin, 1.0 / (nAgents + 10),
|
||
Inp_UseNeural, Inp_NNModelFile,
|
||
Inp_TrainMode, Inp_TrainEpochs,
|
||
Inp_TrainLR, Inp_NNHidden,
|
||
Inp_RiskPerTrade, Inp_RiskTotal);
|
||
trade = new CTrade();
|
||
|
||
dig = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
|
||
trade.SetExpertMagicNumber(magicNumber);
|
||
|
||
orchestrator.AddAgent(new RegimeDetector("Hurst", 1.0, Inp_HurstPeriod));
|
||
orchestrator.AddAgent(new RegimeADX("ADX", 1.0, Inp_ADXPeriod));
|
||
orchestrator.AddAgent(new RegimeConsensus("Consensus", 1.0));
|
||
orchestrator.AddAgent(new MAAgent("MA", 1.0, 8, 40));
|
||
orchestrator.AddAgent(new MomentumAgent("Momentum", 1.0, 6, 40));
|
||
orchestrator.AddAgent(new PatternHunter("Hunter", 1.0));
|
||
orchestrator.InitAgents(sym, Inp_TF);
|
||
orchestrator.LoadState(sym, Inp_TF);
|
||
|
||
// Carica o inizializza modello NN
|
||
if(Inp_UseNeural || Inp_TrainMode) {
|
||
if(!orchestrator.LoadNNModel()) {
|
||
if(Inp_UseNeural) {
|
||
Print(" NN model not found, initializing fresh network...");
|
||
}
|
||
orchestrator.InitNeuralNet(8, Inp_NNHidden, 3);
|
||
}
|
||
}
|
||
|
||
string modeStr = Inp_UseNeural ? "NEURAL" : "SOFTMAX";
|
||
Print("MultiAgentTest v7 avviato: ", sym, " ", EnumToString(Inp_TF), " [", modeStr, "]");
|
||
Print("Agenti: ", orchestrator.TotalAgents(), " (Hurst, ADX, MA, Momentum, Consensus, Hunter)");
|
||
Print("Min |z|: ", Inp_MinZ, " | Buffer: ", bufferBars, " barre | Neural: ", modeStr);
|
||
|
||
// Warm-up statistiche: passa i dati storici per inizializzare le EWMA
|
||
MarketData warmup(sym, Inp_TF, bufferBars);
|
||
if(warmup.Fetch()) {
|
||
Print("Warm-up statistiche... (", warmup.count, " barre)");
|
||
// Processa ogni barra storica una volta, dalla più vecchia alla più recente
|
||
for(int bar = warmup.count - 1; bar >= 1; bar--) {
|
||
MarketData single(sym, Inp_TF, 1);
|
||
single.open[0] = warmup.open[bar];
|
||
single.high[0] = warmup.high[bar];
|
||
single.low[0] = warmup.low[bar];
|
||
single.close[0] = warmup.close[bar];
|
||
single.volume[0] = warmup.volume[bar];
|
||
single.time[0] = warmup.time[bar];
|
||
single.count = 1;
|
||
orchestrator.Analyze(single);
|
||
}
|
||
// Ultima barra (current) processata una volta
|
||
orchestrator.Analyze(warmup);
|
||
Print("Hurst: H=", StringFormat("%.3f", SHARED_regimeH),
|
||
" ADX: ", StringFormat("%.1f", SHARED_adxRaw),
|
||
" Consensus: ", StringFormat("%+.2f", SHARED_regimeConsensus),
|
||
" pattern: ", SHARED_patternName);
|
||
}
|
||
|
||
if(Inp_UseNeural) {
|
||
if(orchestrator.IsNeuralReady()) {
|
||
Print(" Neural: READY — ", orchestrator.NeuralInfo());
|
||
} else {
|
||
Print(" Neural: INITIALIZED (untrained — will fallback to softmax)");
|
||
}
|
||
}
|
||
if(Inp_TrainMode) Print(" TrainMode: ON (collecting samples)");
|
||
Print(orchestrator.TotalAgents(), " agenti pronti.");
|
||
return INIT_SUCCEEDED;
|
||
}
|
||
|
||
void OnDeinit(const int reason) {
|
||
CloseAllPositions();
|
||
|
||
Print("=== MultiAgentTest: Final Report ===");
|
||
Print(" Reason: ", reasonToStr(reason));
|
||
Print(" Symbol: ", sym, " | TF: ", EnumToString(Inp_TF));
|
||
Print(" Mode: ", Inp_UseNeural ? "NEURAL" : "SOFTMAX",
|
||
" | TrainMode: ", Inp_TrainMode ? "ON" : "OFF");
|
||
Print(" Trades: ", orchestrator.TradeCount(),
|
||
" | Win rate: ", orchestrator.TradeCount() > 0
|
||
? StringFormat("%.1f%%", 100.0 * orchestrator.WinCount() / orchestrator.TradeCount())
|
||
: "N/A");
|
||
PrintAgentStats();
|
||
|
||
// Neural: training e salvataggio
|
||
bool nnTrained = false;
|
||
if(Inp_TrainMode && orchestrator.TradeCount() >= 3) {
|
||
Print("=== Neural Network Training (fine backtest) ===");
|
||
Print(" Trade count: ", orchestrator.TradeCount());
|
||
double loss = orchestrator.TrainNN();
|
||
if(loss >= 0) {
|
||
nnTrained = true;
|
||
Print(" Training complete: loss=", StringFormat("%.6f", loss));
|
||
PrintNeuralStats();
|
||
} else {
|
||
Print(" Training skipped: insufficient samples (need >=3)");
|
||
}
|
||
}
|
||
|
||
// Agent learning summary
|
||
orchestrator.PrintAgentLearningSummary();
|
||
orchestrator.SaveAgentLearningCsv();
|
||
|
||
// Per-bar agent z-scores, combinedZ, SHARED variables
|
||
orchestrator.SaveBarHistoryCSV(sym, Inp_TF);
|
||
|
||
// Per-bar agent weights, biases, correlations, raw signals
|
||
orchestrator.SaveAgentInteractionCSV(sym, Inp_TF);
|
||
|
||
// Per-trade details with PnL, exitReason, full features
|
||
orchestrator.SaveTradeHistoryCSV(sym, Inp_TF);
|
||
|
||
// Entry/exit decision log
|
||
orchestrator.SaveDecisionLogCSV(sym, Inp_TF);
|
||
|
||
// Analisi completa: tutti i parametri derivati, soglie, periodi, stats
|
||
orchestrator.SaveAnalysisCSV(sym, Inp_TF);
|
||
|
||
// Save state file (include NN weights inline se addestrata)
|
||
orchestrator.SaveState(sym, Inp_TF);
|
||
|
||
// Salva anche NN standalone (utile per debug/backup)
|
||
if(nnTrained || orchestrator.IsNeuralReady())
|
||
orchestrator.SaveNNModel();
|
||
|
||
orchestrator.ReleaseAgents();
|
||
delete orchestrator;
|
||
delete trade;
|
||
Print("=== MultiAgentTest terminato ===");
|
||
}
|
||
|
||
string reasonToStr(int r) {
|
||
switch(r) {
|
||
case REASON_PROGRAM: return "Program";
|
||
case REASON_REMOVE: return "Remove";
|
||
case REASON_CHARTCLOSE: return "Chart Close";
|
||
case REASON_PARAMETERS: return "Parameters Changed";
|
||
case REASON_RECOMPILE: return "Recompiled";
|
||
case REASON_ACCOUNT: return "Account Changed";
|
||
case REASON_TEMPLATE: return "Template";
|
||
case REASON_INITFAILED: return "Init Failed";
|
||
case REASON_CLOSE: return "Terminal Close";
|
||
default: return "Unknown (" + (string)r + ")";
|
||
}
|
||
}
|
||
|
||
void PrintAgentStats() {
|
||
Print(" Hurst H: ", StringFormat("%.3f", SHARED_regimeH),
|
||
" | ADX: ", StringFormat("%.1f", SHARED_adxRaw),
|
||
" | Consensus: ", StringFormat("%+.2f", SHARED_regimeConsensus),
|
||
" | Agreement: ", StringFormat("%.2f", SHARED_regimeAgreement),
|
||
" | Pattern: ", SHARED_patternName);
|
||
}
|
||
|
||
void PrintNeuralStats() {
|
||
Print(" " + orchestrator.NeuralInfo());
|
||
// Feature importanza approssimata: media |W1| per input
|
||
if(orchestrator.IsNeuralReady()) {
|
||
// Non possiamo accedere direttamente ai pesi
|
||
Print(" (vedi CSV per loss history e training samples)");
|
||
}
|
||
}
|
||
|
||
int CountPositions() {
|
||
int count = 0;
|
||
for(int i=PositionsTotal()-1; i>=0; i--) {
|
||
if(posInfo.SelectByIndex(i)) {
|
||
if(posInfo.Symbol() == sym && posInfo.Magic() == magicNumber)
|
||
count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
void CloseAllPositions() {
|
||
for(int i=PositionsTotal()-1; i>=0; i--) {
|
||
if(posInfo.SelectByIndex(i)) {
|
||
if(posInfo.Symbol() == sym && posInfo.Magic() == magicNumber)
|
||
trade.PositionClose(posInfo.Ticket());
|
||
}
|
||
}
|
||
}
|
||
|
||
void OnTick() {
|
||
if(!IsNewBar()) return;
|
||
|
||
MarketData data(sym, Inp_TF, bufferBars);
|
||
if(!data.Fetch()) return;
|
||
|
||
double z = orchestrator.Analyze(data);
|
||
FinalSignal fs = orchestrator.GetFinalSignal(Inp_MinZ);
|
||
|
||
double minZ = (Inp_MinZ > 0) ? Inp_MinZ : orchestrator.AdaptiveMinZ();
|
||
if(fs.IsActionable(minZ)) {
|
||
ManagePositions(data, fs);
|
||
}
|
||
|
||
LogSignal(data, fs);
|
||
|
||
if(CountPositions() == 0 && orchestrator.HasOpenTrade()) {
|
||
orchestrator.ResetTradeState();
|
||
}
|
||
}
|
||
|
||
bool IsNewBar() {
|
||
int barsTotal = Bars(sym, Inp_TF);
|
||
datetime timeArr[];
|
||
CopyTime(sym, Inp_TF, 0, 1, timeArr);
|
||
if(ArraySize(timeArr) < 1) return false;
|
||
datetime barTime = timeArr[0];
|
||
|
||
if(lastBarTime == 0) {
|
||
lastBarTime = barTime;
|
||
lastBarTotal = barsTotal;
|
||
return false;
|
||
}
|
||
|
||
if(barTime != lastBarTime || barsTotal != lastBarTotal) {
|
||
lastBarTime = barTime;
|
||
lastBarTotal = barsTotal;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void ManagePositions(const MarketData &data, const FinalSignal &fs) {
|
||
double minZ = (Inp_MinZ > 0) ? Inp_MinZ : orchestrator.AdaptiveMinZ();
|
||
|
||
// 1. Update MAE/MFE per tutti gli aperti (traccia high/low intra-barra)
|
||
orchestrator.UpdateOpenTrades(data.High(0), data.Low(0));
|
||
orchestrator.TrailStops();
|
||
|
||
// 2. Reversal close (opzionale): chiudi posizioni con segnale opposto
|
||
// Se disabilitato, long e short coesistono — massimizza profitto multi-direzionale
|
||
if(Inp_UseReversalClose) {
|
||
int closeTickets[];
|
||
orchestrator.GetTradesToClose(closeTickets, fs.zScore, minZ);
|
||
for(int c = 0; c < ArraySize(closeTickets); c++) {
|
||
int tkt = closeTickets[c];
|
||
if(PositionSelectByTicket(tkt)) {
|
||
bool isBuy = orchestrator.IsBuyTrade(tkt);
|
||
double closePrice = isBuy ? SymbolInfoDouble(sym, SYMBOL_BID) : SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
orchestrator.OnTradeClose(tkt, closePrice, "REVERSAL");
|
||
trade.PositionClose(tkt);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. Aggiorna trailing stop sul terminale (tester supporta PositionModify)
|
||
for(int i=PositionsTotal()-1; i>=0; i--) {
|
||
if(posInfo.SelectByIndex(i)) {
|
||
if(posInfo.Symbol() != sym || posInfo.Magic() != magicNumber) continue;
|
||
int tkt = (int)posInfo.Ticket();
|
||
double newSL = orchestrator.GetTradeSL(tkt);
|
||
if(newSL <= 0) continue;
|
||
double curSL = posInfo.StopLoss();
|
||
bool isBuy = posInfo.PositionType() == POSITION_TYPE_BUY;
|
||
bool shouldUpdate = isBuy ? (newSL > curSL + _Point) : (newSL < curSL - _Point);
|
||
if(shouldUpdate) {
|
||
double curTP = posInfo.TakeProfit();
|
||
trade.PositionModify(tkt, newSL, curTP);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. Apri nuova posizione OGNI BARRA con segnale actionable (aggressivo)
|
||
if(MathAbs(fs.zScore) > minZ) {
|
||
double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
||
double price = (fs.direction == 1) ? ask : bid;
|
||
// ATR period: scala con N agenti e buffer disponibile
|
||
int atrPeriod = MathMax(orchestrator.TotalAgents() + 1, MathMin(bufferBars / 15, 50));
|
||
double atr = data.ATR(atrPeriod);
|
||
if(atr <= 0) return;
|
||
|
||
// 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 slPoints = price - sl;
|
||
if(fs.direction == -1) slPoints = sl - price;
|
||
if(slPoints <= 0) { Print("SL troppo stretto — skip"); return; }
|
||
|
||
// 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
|
||
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
||
double marginReq = lot * SymbolInfoDouble(sym, SYMBOL_MARGIN_INITIAL);
|
||
if(marginReq >= freeMargin && freeMargin > 0) {
|
||
Print("Margine insufficiente: lot=", lot, " free=", freeMargin, " req=", marginReq);
|
||
return; // Skip — solo il margine blocca i trade
|
||
}
|
||
|
||
int ticket = 0;
|
||
// Calcolo TakeProfit opzionale (usiamo lo stesso fattoreATR per TP)
|
||
double tp = 0;
|
||
// TakeProfit: se impostato usa Inp_TPRiskATR, altrimenti usa lo stesso fattore di SL (dynamic)
|
||
double tpWidth = (Inp_TPRiskATR > 0) ? Inp_TPRiskATR : ((Inp_SLRiskATR > 0) ? Inp_SLRiskATR : orchestrator.AdaptiveSLWidth());
|
||
if(tpWidth > 0) {
|
||
tp = (fs.direction == 1) ? price + atr * tpWidth : price - atr * tpWidth;
|
||
tp = NormalizeDouble(tp, dig);
|
||
}
|
||
if(fs.direction == 1) {
|
||
// BUY – validate SL and TP
|
||
if(sl <= 0 || sl >= ask) {
|
||
Print("Invalid BUY SL (", sl, ") – order skipped");
|
||
} else if(tp > 0 && tp <= sl) {
|
||
Print("Invalid BUY TP (", tp, ") – must be > SL – order skipped");
|
||
} else {
|
||
ticket = trade.Buy(lot, sym, ask, sl, tp); // TP may be 0 (no TP)
|
||
}
|
||
} else {
|
||
// SELL – validate SL and TP
|
||
if(sl <= 0 || sl <= bid) {
|
||
Print("Invalid SELL SL (", sl, ") – order skipped");
|
||
} else if(tp > 0 && tp >= sl) {
|
||
Print("Invalid SELL TP (", tp, ") – must be < SL – order skipped");
|
||
} else {
|
||
ticket = trade.Sell(lot, sym, bid, sl, tp);
|
||
}
|
||
}
|
||
|
||
if(ticket > 0) {
|
||
orchestrator.OnTradeOpen(ticket, price, atr, lot);
|
||
}
|
||
}
|
||
}
|
||
|
||
void LogSignal(const MarketData &data, const FinalSignal &fs) {
|
||
string timeStr = TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES);
|
||
string dir = "NONE";
|
||
if(fs.direction == 1) dir = "BUY";
|
||
if(fs.direction == -1) dir = "SELL";
|
||
|
||
string mode = Inp_UseNeural && orchestrator.IsNeuralReady() ? "NN" : "SM";
|
||
string log = timeStr + " [" + mode + "] " + dir;
|
||
log += " | z=" + StringFormat("%+.3f", fs.zScore);
|
||
log += " | conf=" + StringFormat("%.0f%%", fs.confidence*100);
|
||
log += " | agree=" + (string)fs.agreeingCount + "/" + (string)fs.totalAgents;
|
||
|
||
static int printCounter = 0;
|
||
if(printCounter % 5 == 0) {
|
||
orchestrator.PrintAgentStatus();
|
||
if(Inp_UseNeural)
|
||
Print(" ", orchestrator.NeuralInfo());
|
||
}
|
||
printCounter++;
|
||
|
||
// Log decision even for non-actionable signals
|
||
double minZ = (Inp_MinZ > 0) ? Inp_MinZ : orchestrator.AdaptiveMinZ();
|
||
orchestrator.LogDecision("SIGNAL", fs.direction, minZ, data.Close(0));
|
||
|
||
if(MathAbs(fs.zScore) > Inp_MinZ) {
|
||
Print(log);
|
||
Print(" Agents: ", fs.contributingAgents);
|
||
}
|
||
}
|
||
|
||
// Rileva chiusure (SL/TP) fatte dal broker — multi-trade
|
||
void OnTrade() {
|
||
static int onTradeCloses = 0;
|
||
static int saveInterval = 0;
|
||
if(saveInterval == 0) {
|
||
// Minimum 5 trades, scaled with number of agents
|
||
saveInterval = MathMax(5, orchestrator.TotalAgents() * 5);
|
||
}
|
||
if(!orchestrator.HasOpenTrade()) return;
|
||
int maxSlots = orchestrator.MaxTradeSlots();
|
||
for(int t = 0; t < maxSlots; t++) {
|
||
int tkt = orchestrator.GetTrackedTicket(t);
|
||
if(tkt < 0) break; // no more active trades
|
||
bool found = false;
|
||
for(int p = PositionsTotal() - 1; p >= 0; p--) {
|
||
if(posInfo.SelectByIndex(p)) {
|
||
if((int)posInfo.Ticket() == tkt) { found = true; break; }
|
||
}
|
||
}
|
||
if(!found) {
|
||
// Trade closed by SL/TP (not by ManagePositions)
|
||
bool isBuy = orchestrator.IsBuyTrade(tkt);
|
||
double closePrice = isBuy ? SymbolInfoDouble(sym, SYMBOL_BID) : SymbolInfoDouble(sym, SYMBOL_ASK);
|
||
orchestrator.OnTradeClose(tkt, closePrice, "SLTP");
|
||
onTradeCloses++;
|
||
Print("SL/TP chiuso trade #", tkt, " (totale SL/TP: ", onTradeCloses, ")");
|
||
if(onTradeCloses % saveInterval == 0)
|
||
orchestrator.SaveState(sym, Inp_TF);
|
||
}
|
||
}
|
||
}
|
||
|
||
double OnTester() {
|
||
double sharpe = orchestrator.TradeSharpe();
|
||
int trades = orchestrator.TradeCount();
|
||
Print("OnTester: ", trades, " trade, Sharpe=", StringFormat("%.3f", sharpe));
|
||
return sharpe;
|
||
}
|