369 Zeilen
Kein EOL
18 KiB
MQL5
369 Zeilen
Kein EOL
18 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Test.mq5 |
|
|
//| Copyright 2026, DNG |
|
|
//| https://www.mql5.com/ru/users/dng |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2026, DNG"
|
|
#property link "https://www.mql5.com/ru/users/dng"
|
|
#property version "1.00"
|
|
#property description "AC-SRM Stage 03/05: инференс замороженной политики с оценкой на LIVE-счете."
|
|
#property description "На старте бара Actor принимает решение; реальные ордера исполняются на тестерном счете."
|
|
#property description "Источник — LIVE-котировки; результат — журнал PASS/FAIL и манифест-проверка."
|
|
#define Online
|
|
#include "Trajectory.mqh"
|
|
//---
|
|
int Epochs = 0;
|
|
//---
|
|
input group "---- AC-SRM inference ----"
|
|
input int InpDealHorizon = 24; //Deal horizon (manifest validation symmetry)
|
|
input double MinBalance = 50.0; //Minimum account balance (irrecoverable threshold)
|
|
//---
|
|
input group "---- SRM critic head ----"
|
|
input int InpRiskType = defSRM_MEAN_CVAR; //Risk measure (defSRM_*)
|
|
input float InpRiskParameter = 0.1f; //Risk parameter (alpha/lambda/nu)
|
|
input float InpMeanWeight = 0.0f; //Mean-CVaR omega
|
|
input int InpQuantiles = 32; //Head quantiles (factory-fixed)
|
|
//---
|
|
input group "---- AC-SRM production checkpoint ----"
|
|
input ENUM_OMPB_STAGE InpOMPBStage = OMPB_STAGE_BASE_POLICY; //Chain stage: keep 03 (base policy)
|
|
//---
|
|
CNet Actor;
|
|
CNet Q1;
|
|
CNet Q2;
|
|
|
|
CBufferFloat State;
|
|
CBufferFloat TimeState;
|
|
CBufferFloat Account;
|
|
CBufferFloat CurrentAction;
|
|
CBufferFloat CriticInput;
|
|
CBufferFloat SRMValue;
|
|
CBufferFloat PrevAccount;
|
|
CBufferFloat PrevAction;
|
|
|
|
int TestBars = 0;
|
|
ulong TestTerminal = 0;
|
|
ulong TestExecutable = 0;
|
|
ulong TestNoTrade = 0;
|
|
double RewardSum = 0;
|
|
double SRM1Sum = 0;
|
|
double SRM2Sum = 0;
|
|
double BalanceMin = 0;
|
|
double BalanceMax = 0;
|
|
double EquityMin = 0;
|
|
double EquityMax = 0;
|
|
ulong Irrecoverable = 0;
|
|
//--- LIVE stream state: previous bar post-execution snapshots.
|
|
double LivePrevBalance = 0;
|
|
double LivePrevEquity = 0;
|
|
bool LiveReady = false;
|
|
bool LiveFinalized = false;
|
|
//+------------------------------------------------------------------+
|
|
//| Validates the AC-SRM policy shapes (no D2 banks, ACSRM heads). |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidatePolicyShapeACSRM(CNet &actor, CNet &q1, CNet &q2)
|
|
{
|
|
CNeuronBaseOCL *a0 = actor.Layer(0);
|
|
CNeuronBaseOCL *a1 = actor.Layer(1);
|
|
CNeuronBaseOCL *a3 = actor.Layer(3);
|
|
if(!a0 || !a1 || !a3 || a0.getOutput().Total() != (int)AccountDescr ||
|
|
a1.Type() != defNeuronScenarioCrossAttention ||
|
|
a3.getOutput().Total() != (int)NActions)
|
|
ReturnFalse;
|
|
for(int i = 0; i < 2; i++)
|
|
{
|
|
CNet *critic = (i == 0 ? GetPointer(q1) : GetPointer(q2));
|
|
CNeuronBaseOCL *c0 = critic.Layer(0);
|
|
CNeuronBaseOCL *c3 = critic.Layer(3);
|
|
CNeuronBaseOCL *c4 = critic.Layer(4);
|
|
if(!c0 || !c3 || !c4 ||
|
|
c0.getOutput().Total() != (int)(AccountDescr + NActions) ||
|
|
c3.getOutput().Total() != 32 ||
|
|
c4.Type() != defNeuronACSRM ||
|
|
c4.getOutput().Total() != 1)
|
|
ReturnFalse;
|
|
}
|
|
return(true);
|
|
}
|
|
//+------------------------------------------------------------------------------------------------------------+
|
|
//| Inference requires the published Stage 03 checkpoint; creation is intentionally impossible in Test mode. |
|
|
//+------------------------------------------------------------------------------------------------------------+
|
|
bool LoadPoliciesInference(void)
|
|
{
|
|
if(!SkillACRecoverTransaction(Skill_ACTOR_FILE, Skill_Q1_FILE, Skill_Q2_FILE,
|
|
Skill_AC_MANIFEST_FILE, Skill_ACTOR_NEXT_FILE,
|
|
Skill_Q1_NEXT_FILE, Skill_Q2_NEXT_FILE,
|
|
Skill_AC_MANIFEST_NEXT_FILE, Skill_ACTOR_PREVIOUS_FILE,
|
|
Skill_Q1_PREVIOUS_FILE, Skill_Q2_PREVIOUS_FILE,
|
|
Skill_AC_MANIFEST_PREVIOUS_FILE,
|
|
Skill_AC_TRANSACTION_FILE))
|
|
{
|
|
Print("Skill policy recovery=FAIL; incomplete checkpoint requires repair");
|
|
ReturnFalse;
|
|
}
|
|
if(!FileIsExist(Skill_AC_MANIFEST_FILE, FILE_COMMON))
|
|
{
|
|
Print("Skill inference policy=FAIL reason=manifest_missing");
|
|
ReturnFalse;
|
|
}
|
|
if(!SkillValidateACManifestFile(true, Skill_AC_MANIFEST_FILE, false) ||
|
|
!SkillLoadPolicyNet(Actor, Skill_ACTOR_FILE) ||
|
|
!SkillLoadPolicyNet(Q1, Skill_Q1_FILE) ||
|
|
!SkillLoadPolicyNet(Q2, Skill_Q2_FILE))
|
|
{
|
|
Print("Skill inference policy=FAIL reason=tuple_invalid");
|
|
ReturnFalse;
|
|
}
|
|
if(!SkillBindPolicyOpenCLChecked(Actor, Q1, Q2) ||
|
|
!ValidatePolicyShapeACSRM(Actor, Q1, Q2))
|
|
ReturnFalse;
|
|
Actor.TrainMode(false);
|
|
Q1.TrainMode(false);
|
|
Q2.TrainMode(false);
|
|
if(!Actor.SetWeightsUpdate(false) || !Q1.SetWeightsUpdate(false) ||
|
|
!Q2.SetWeightsUpdate(false))
|
|
ReturnFalse;
|
|
return(true);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Returns the SRM risk score of one critic head distribution. |
|
|
//+------------------------------------------------------------------+
|
|
bool CriticRiskValue(CNet &critic, double &value)
|
|
{
|
|
CNeuronBaseOCL *head = critic.Layer(4);
|
|
if(!head || head.Type() != defNeuronACSRM)
|
|
ReturnFalse;
|
|
CNeuronACSRM *acsrm = (CNeuronACSRM*)head;
|
|
if(!SRMValue.BufferInit(1, 0) || !SRMValue.BufferCreate(SkillMarket.GetOpenCL()) ||
|
|
!SRMValue.BufferWrite())
|
|
ReturnFalse;
|
|
if(!acsrm.SRMForward(GetPointer(SRMValue), 1, InpRiskType, InpRiskParameter, InpMeanWeight))
|
|
ReturnFalse;
|
|
if(!SRMValue.BufferRead())
|
|
ReturnFalse;
|
|
value = double(SRMValue[0]);
|
|
return(MathIsValidNumber(value));
|
|
}
|
|
//+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
//| Refreshes the live market state from the last CLOSED bar and the scenario (forecast) context consumed by the actor cross- attention. SkillRefreshLiveMarket fills the Rates/indicator window; the explicit Market feed refreshes z/u/pi. |
|
|
//+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
bool LiveRefreshState(CBufferFloat *state, CBufferFloat *time_state)
|
|
{
|
|
if(!SkillRefreshLiveMarket(state, time_state))
|
|
ReturnFalse;
|
|
return(SkillMarket.feedForward(state, 1, false, (CBufferFloat*)NULL) &&
|
|
SkillForecast != NULL);
|
|
}
|
|
//+------------------------------------------------------------------------------------------------------------------------------------+
|
|
//| Keeps the PREVIOUS (account, action) host copies: the action is reported with its own bar and classified by executable contract. |
|
|
//+------------------------------------------------------------------------------------------------------------------------------------+
|
|
bool LiveKeepPrevious(void)
|
|
{
|
|
if(PrevAccount.Total() != (int)AccountDescr &&
|
|
!PrevAccount.BufferInit((int)AccountDescr, 0))
|
|
ReturnFalse;
|
|
if(PrevAction.Total() != (int)NActions &&
|
|
!PrevAction.BufferInit((int)NActions, 0))
|
|
ReturnFalse;
|
|
return(PrevAccount.AssignArray(GetPointer(Account)) &&
|
|
PrevAction.AssignArray(GetPointer(CurrentAction)));
|
|
}
|
|
//+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
//| Executes the bar decision on the LIVE account: builds the real account vector, runs the frozen Actor, retains this bar's (state, action) critic tuple for next-bar diagnostics and re- targets this expert's positions (CogDriver). The post-execution LIVE balance/equity snapshot is the reward base of this bar. |
|
|
//+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
bool LiveExecuteBar(const double previous_balance, const double previous_equity)
|
|
{
|
|
//--- Fresh CLOSED-bar market context: LiveRefreshState runs
|
|
//--- SkillRefreshLiveMarket (CopyRates offset 1 + Symb.RefreshRates)
|
|
//--- which SkillExecuteAction needs to derive valid SL/TP distances.
|
|
if(!LiveRefreshState(GetPointer(State), GetPointer(TimeState)))
|
|
ReturnFalse;
|
|
datetime now = TimeCurrent();
|
|
double buy_value = 0, sell_value = 0;
|
|
if(!SkillBuildLiveAccount(previous_balance, previous_equity, now,
|
|
GetPointer(Account), buy_value, sell_value))
|
|
ReturnFalse;
|
|
if(Account.GetIndex() < 0 && !Account.BufferCreate(SkillMarket.GetOpenCL()))
|
|
ReturnFalse;
|
|
if(Account.GetIndex() >= 0 && !Account.BufferWrite())
|
|
ReturnFalse;
|
|
if(!Actor.Clear() || !Actor.feedForward(GetPointer(Account), 1, false,
|
|
GetPointer(SkillMarket), -1) ||
|
|
!ReadAction(Actor, GetPointer(CurrentAction)))
|
|
ReturnFalse;
|
|
CNeuronBaseOCL *context_layer = Actor.Layer(0);
|
|
CNeuronBaseOCL *actor_layer = Actor.Layer(3);
|
|
if(!context_layer || !actor_layer)
|
|
ReturnFalse;
|
|
if(!BuildCriticInput(context_layer.getOutput(), actor_layer.getOutput(),
|
|
GetPointer(CriticInput)))
|
|
ReturnFalse;
|
|
double margin_penalty = 0;
|
|
bool market_closed = false;
|
|
if(!SkillExecuteAction(GetPointer(CurrentAction), buy_value, sell_value,
|
|
margin_penalty, market_closed))
|
|
ReturnFalse;
|
|
//--- Post-execution LIVE snapshot: the reward of this bar is evaluated
|
|
//--- from it on the next bar open.
|
|
LivePrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
LivePrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
return(LiveKeepPrevious());
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| First bar: no previous bar exists, only the opening decision. |
|
|
//+------------------------------------------------------------------+
|
|
bool LiveFirstBar(void)
|
|
{
|
|
LiveReady = true;
|
|
LivePrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
LivePrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
if(!LiveExecuteBar(LivePrevBalance, LivePrevEquity))
|
|
ReturnFalse;
|
|
PrintFormat("ACSRM_TEST_LIVE_BAR1 time=%s balance=%.2f equity=%.2f act=(%.8f %.8f %.8f %.8f %.8f %.8f)",
|
|
TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
|
|
LivePrevBalance, LivePrevEquity,
|
|
double(PrevAction[0]), double(PrevAction[1]), double(PrevAction[2]),
|
|
double(PrevAction[3]), double(PrevAction[4]), double(PrevAction[5]));
|
|
return(true);
|
|
}
|
|
//+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
//| One new-bar live transition: evaluates the just-closed bar from the LIVE account (reward = equity change vs the post-execution snapshot of the previous bar), emits critic SRM diagnostics on the retained tuple and places the new bar order target. |
|
|
//+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
bool LiveEvaluateBar(void)
|
|
{
|
|
if(!LiveReady)
|
|
return(LiveFirstBar());
|
|
//--- LIVE result of the just-closed bar.
|
|
const double balance_now = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
const double equity_now = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
const double reward = equity_now - LivePrevEquity;
|
|
if(!MathIsValidNumber(reward) || !MathIsValidNumber(balance_now) ||
|
|
!MathIsValidNumber(equity_now))
|
|
ReturnFalse;
|
|
TestBars++;
|
|
double srm1 = 0, srm2 = 0;
|
|
//--- Critic diagnostics on the retained previous (state, action) tuple.
|
|
if(!Q1.feedForward(GetPointer(CriticInput), 1, false, GetPointer(SkillMarket), -1) ||
|
|
!Q2.feedForward(GetPointer(CriticInput), 1, false, GetPointer(SkillMarket), -1) ||
|
|
!CriticRiskValue(Q1, srm1) || !CriticRiskValue(Q2, srm2))
|
|
ReturnFalse;
|
|
const double buy_lot = MathMax(0.0, double(PrevAction[0] - PrevAction[3]));
|
|
const double sell_lot = MathMax(0.0, double(PrevAction[3] - PrevAction[0]));
|
|
const bool executable = (IsExecutableOrder(buy_lot, PrevAction[1], PrevAction[2]) ||
|
|
IsExecutableOrder(sell_lot, PrevAction[4], PrevAction[5]));
|
|
if(executable)
|
|
TestExecutable++;
|
|
else
|
|
TestNoTrade++;
|
|
RewardSum += reward;
|
|
SRM1Sum += srm1;
|
|
SRM2Sum += srm2;
|
|
const bool terminal = (balance_now <= MinBalance);
|
|
if(terminal)
|
|
TestTerminal++;
|
|
if(balance_now <= MinBalance)
|
|
Irrecoverable++;
|
|
if(TestBars == 1)
|
|
{
|
|
BalanceMin = balance_now;
|
|
BalanceMax = balance_now;
|
|
EquityMin = equity_now;
|
|
EquityMax = equity_now;
|
|
}
|
|
else
|
|
{
|
|
BalanceMin = MathMin(BalanceMin, balance_now);
|
|
BalanceMax = MathMax(BalanceMax, balance_now);
|
|
EquityMin = MathMin(EquityMin, equity_now);
|
|
EquityMax = MathMax(EquityMax, equity_now);
|
|
}
|
|
if(TestBars <= 4 || TestBars % 1000 == 0)
|
|
PrintFormat("ACSRM_TEST_BAR bar=%d balance=%.2f equity=%.2f reward=%.8f srm1=%.8f srm2=%.8f act=(%.8f %.8f %.8f %.8f %.8f %.8f) terminal=%s",
|
|
TestBars, balance_now, equity_now, reward, srm1, srm2,
|
|
double(PrevAction[0]), double(PrevAction[1]), double(PrevAction[2]),
|
|
double(PrevAction[3]), double(PrevAction[4]), double(PrevAction[5]),
|
|
(terminal ? "true" : "false"));
|
|
//--- The new bar decision is made from the post-execution LIVE account
|
|
//--- of the previous bar (relative change fields) and the closed-bar
|
|
//--- market state.
|
|
return(LiveExecuteBar(LivePrevBalance, LivePrevEquity));
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Emits the final live evaluation report once. |
|
|
//+------------------------------------------------------------------+
|
|
void LiveFinalize(void)
|
|
{
|
|
if(LiveFinalized)
|
|
return;
|
|
LiveFinalized = true;
|
|
if(TestBars <= 0)
|
|
{
|
|
Print("ACSRM_TEST_FAIL reason=no_bars");
|
|
return;
|
|
}
|
|
const double r_mean = RewardSum / double(TestBars);
|
|
const double srm1_mean = SRM1Sum / double(TestBars);
|
|
const double srm2_mean = SRM2Sum / double(TestBars);
|
|
PrintFormat("ACSRM_TEST_PASS bars=%d episodes=1 terminal=%I64u executable=%I64u no_trade=%I64u " +
|
|
"r_mean=%.8f srm1_mean=%.8f srm2_mean=%.8f balance_min=%.2f balance_max=%.2f " +
|
|
"equity_min=%.2f equity_max=%.2f irrecoverable=%I64u",
|
|
TestBars, TestTerminal, TestExecutable, TestNoTrade,
|
|
r_mean, srm1_mean, srm2_mean, BalanceMin, BalanceMax, EquityMin, EquityMax,
|
|
Irrecoverable);
|
|
//--- Leave a clean tester account.
|
|
CloseByDirection(POSITION_TYPE_BUY);
|
|
CloseByDirection(POSITION_TYPE_SELL);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Initializes the Stage 03 inference Expert. |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
ResetLastError();
|
|
//--- Pin the deal-evaluation horizon for manifest validation symmetry.
|
|
SkillManifestDealHorizon = MathMax(1, InpDealHorizon);
|
|
//--- Confirm Stage 03, the frozen Forecast checkpoint and the policy tuple.
|
|
if(InpOMPBStage != OMPB_STAGE_BASE_POLICY ||
|
|
!SkillInitIndicators() || !SkillLoadForecastInference() ||
|
|
!SkillConfigureProductionACSRMCheckpoint() ||
|
|
!SkillValidateProductionACSRMCheckpoint() ||
|
|
!SkillCaptureProductionACSRMFingerprints() || !LoadPoliciesInference() ||
|
|
!SkillVerifyFrozenForecastExact() ||
|
|
!SkillVerifyProductionACSRMFingerprints())
|
|
{
|
|
PrintFormat("ACSRM inference initialization failed at line %d error=%d",
|
|
__LINE__, GetLastError());
|
|
return(INIT_FAILED);
|
|
}
|
|
PrintFormat("ACSRM_TEST_INIT_PASS tester=%d", MQLInfoInteger(MQL_TESTER));
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Function OnDeinit. |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
if(MQLInfoInteger(MQL_TESTER) == 1 && !LiveFinalized)
|
|
{
|
|
PrintFormat("%s live finalize reason=%d bars=%d", ACSRM_LOG_PREFIX, reason, TestBars);
|
|
LiveFinalize();
|
|
}
|
|
if(SkillForecast != NULL && !SkillVerifyFrozenForecastExact())
|
|
PrintFormat("%s -> %d forecast mutation", __FUNCTION__, __LINE__);
|
|
if(SkillProductionSignatureReady && !SkillVerifyProductionACSRMFingerprints())
|
|
PrintFormat("%s -> %d ACSRM production signature mutation", __FUNCTION__, __LINE__);
|
|
SkillForecast = NULL;
|
|
}
|
|
//+--------------------------------------------------------------------------------------------------------------------------------+
|
|
//| LIVE bar-driven evaluation on the tester account stream. Each new bar triggers one decision + one next-bar live evaluation. |
|
|
//+--------------------------------------------------------------------------------------------------------------------------------+
|
|
void OnTick(void)
|
|
{
|
|
if(!IsNewBar())
|
|
return;
|
|
if(!LiveEvaluateBar())
|
|
{
|
|
PrintFormat("ACSRM_TEST_FAIL reason=live_transition_failed bars=%d", TestBars);
|
|
ExpertRemove();
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+ |