Market_State_Board/Include/MarketStateBoard_Weights.mqh

268 lines
11 KiB
MQL5

//+------------------------------------------------------------------+
//| MarketStateBoard_Weights.mqh |
//| PATCH-11 - DYNAMIC WEIGHT ENGINE |
//+------------------------------------------------------------------+
// PATCH-11 semantics (frozen by specification):
// * Converts the direction-neutral PATCH-10 Context Quality components
// (atrQuality, volumeQuality, bodyQuality, closeLocationQuality) into
// deterministic, per-timeframe dynamic weights for the four fuzzy
// oscillator channels: RSI, Stochastic, CCI, MACD.
// * A dynamic weight is the contextual influence assigned to an evidence
// channel under the current market context. It is NOT a confidence, a
// probability, statistical importance proven by history, predictive
// quality, expected return or trade-success probability.
// * This layer consumes ONLY MSB_ContextQuality. It never reads barDir,
// rsiState/stochState/cciState/macdState, oscillatorAgreement or
// alignState, so the same context produces the same channel weights
// regardless of whether the current market direction is bullish or
// bearish (direction-neutrality invariant).
// * No score, probability, confidence, trade-outcome feedback, optimizer,
// temporal smoothing, recursive weight carry-over or cross-timeframe
// input exists in this layer.
// * Exactly one authoritative implementation: the indicator and the static
// validation harness include this file verbatim.
//
// INCLUDE CONTRACT:
// MSB_ContextQuality (PATCH-10) must be declared BEFORE including this file
// (the indicator declares it in section 5; the validation harness declares
// an identical fixture struct).
//
// CALIBRATION GOVERNANCE (spec section 36):
// The base weights, WEIGHT_EPS and the channel profiles below are
// provisional structural placeholders marked WEIGHT_CALIBRATION_PENDING.
// None of them is an authorized calibration and none may be claimed
// empirically validated or predictively superior. They must NOT be
// optimized, backtested, swept or tuned to market observations;
// replacement values require a separate authorization.
// WEIGHT_CALIBRATION_PENDING
// Base structural weights (spec section 8): equal structural baselines that
// express the absence of an empirically established preference before
// contextual modulation. Not probabilities. Not tuned from market outcomes.
const double BASE_RSI = 1.0;
const double BASE_STOCH = 1.0;
const double BASE_CCI = 1.0;
const double BASE_MACD = 1.0;
// Weight-normalization tolerance (spec section 17). Must not be made large
// enough to hide numerical errors.
const double WEIGHT_EPS = 1.0e-12;
// </WEIGHT_CALIBRATION_PENDING>
//==============================================================
// 1. STRUCTURES
//==============================================================
struct MSB_DynamicWeights
{
double rsi; // normalized dynamic weight for the RSI channel [0,1]
double stoch; // normalized dynamic weight for the Stochastic channel [0,1]
double cci; // normalized dynamic weight for the CCI channel [0,1]
double macd; // normalized dynamic weight for the MACD channel [0,1]
bool valid; // false when context is unusable; weight fields are storage only
};
// Fixed structural context profile of one oscillator channel (spec section 11).
// Contract per channel: every coefficient >= 0 and
// atr + volume + body + closeLocation == 1. The profile states how compatible
// the current market context is with the structural characteristics of that
// oscillator; it is not directional and not an accuracy statement.
struct MSB_ContextProfile
{
double atr;
double volume;
double body;
double closeLocation;
};
//==============================================================
// 2. CHANNEL CONTEXT PROFILES
//==============================================================
// WEIGHT_CALIBRATION_PENDING
// Provisional channel-specific context profiles (spec section 12). These
// values are structural placeholders - an explicit, deterministic research
// starting specification. The coding agent must NOT change them because of
// observed runtime behavior; they are not statistically validated.
// RSI : 0.25 + 0.15 + 0.25 + 0.35 = 1.00
// STOCH: 0.20 + 0.20 + 0.25 + 0.35 = 1.00
// CCI : 0.25 + 0.15 + 0.30 + 0.30 = 1.00
// MACD : 0.40 + 0.15 + 0.35 + 0.10 = 1.00
const MSB_ContextProfile MSB_PROFILE_RSI = {0.25, 0.15, 0.25, 0.35};
const MSB_ContextProfile MSB_PROFILE_STOCH = {0.20, 0.20, 0.25, 0.35};
const MSB_ContextProfile MSB_PROFILE_CCI = {0.25, 0.15, 0.30, 0.30};
const MSB_ContextProfile MSB_PROFILE_MACD = {0.40, 0.15, 0.35, 0.10};
// </WEIGHT_CALIBRATION_PENDING>
//==============================================================
// 3. HELPERS
//==============================================================
bool WeightFinite(double v)
{
return (v == v) && (v < DBL_MAX) && (v > -DBL_MAX);
}
// Defensive domain clamp to [0,1]. The PATCH-10 contract already guarantees
// qualities in [0,1]; the clamp only guards floating-point edge cases.
double Clamp01(double v)
{
return MathMin(1.0, MathMax(0.0, v));
}
// True when at least one context component is usable (valid flag AND finite
// quality). An all-unusable context can never produce weights (spec section
// 15: do not invent default weights from unavailable context).
bool HasUsableContext(const MSB_ContextQuality &ctx)
{
return (ctx.atrValid && WeightFinite(ctx.atrQuality)) ||
(ctx.volumeValid && WeightFinite(ctx.volumeQuality)) ||
(ctx.bodyValid && WeightFinite(ctx.bodyQuality)) ||
(ctx.closeLocationValid && WeightFinite(ctx.closeLocationQuality));
}
//==============================================================
// 4. SUITABILITY
//==============================================================
// Contextual suitability of one channel (spec sections 13/14/19):
// s_i = (sum over USABLE k of p_i,k * Q_k) / (sum over USABLE k of p_i,k)
// The denominator renormalizes the used profile coefficients so their sum
// equals 1. Missing context components are never substituted with NORMAL or
// 0.5 and never given a zero influence automatically: they are excluded and
// the remaining used coefficients are renormalized. A profile as a whole is
// never silently renormalized here - a malformed profile must be caught by
// the profile sanity verification, not hidden by this function.
// A non-finite quality inside a valid-flagged component makes only that
// component unusable; the remaining usable components still define the
// context.
// Returns 0.0 when no component is usable. ComputeDynamicWeights already
// rejects that case via HasUsableContext; this guards the public API only.
double ComputeChannelSuitability(const MSB_ContextQuality &ctx,
const MSB_ContextProfile &profile)
{
bool atrOk = ctx.atrValid && WeightFinite(ctx.atrQuality);
bool volOk = ctx.volumeValid && WeightFinite(ctx.volumeQuality);
bool bodyOk = ctx.bodyValid && WeightFinite(ctx.bodyQuality);
bool closeOk = ctx.closeLocationValid && WeightFinite(ctx.closeLocationQuality);
double num = 0.0;
double den = 0.0;
if(atrOk)
{
num += profile.atr * Clamp01(ctx.atrQuality);
den += profile.atr;
}
if(volOk)
{
num += profile.volume * Clamp01(ctx.volumeQuality);
den += profile.volume;
}
if(bodyOk)
{
num += profile.body * Clamp01(ctx.bodyQuality);
den += profile.body;
}
if(closeOk)
{
num += profile.closeLocation * Clamp01(ctx.closeLocationQuality);
den += profile.closeLocation;
}
if(den <= 0.0)
return 0.0; // no usable component -> no suitability evidence
double s = num / den;
// Defensive clamp (spec section 29): with qualities in [0,1] and
// non-negative coefficients the weighted average is already inside
// [0,1]; gross corruption is rejected by ComputeDynamicWeights, never
// silently renormalized.
return MathMin(1.0, MathMax(0.0, s));
}
//==============================================================
// 5. DYNAMIC WEIGHTS
//==============================================================
// Deterministic invalid storage (spec section 7): zeroed weights, valid=false.
MSB_DynamicWeights InvalidDynamicWeights()
{
MSB_DynamicWeights w;
w.rsi = 0.0;
w.stoch = 0.0;
w.cci = 0.0;
w.macd = 0.0;
w.valid = false;
return w;
}
// Per-timeframe dynamic weight vector (spec sections 16/17/18/29):
// r_i = BASE_i * s_i (raw weights, explicit baseline)
// w_i = r_i / sum(r_j) when sum(r_j) > WEIGHT_EPS
// Invariants enforced before returning valid=true:
// * every suitability finite and inside [0,1]
// * sum(raw) > WEIGHT_EPS (never divide by zero)
// * every weight inside [0,1] and sum(w) == 1 within WEIGHT_EPS
// Any unexpected violation yields valid=false - never silent renormalization.
// Stateless by design (spec section 27): current context -> current weights.
MSB_DynamicWeights ComputeDynamicWeights(const MSB_ContextQuality &ctx)
{
MSB_DynamicWeights out = InvalidDynamicWeights();
// All context invalid: weights are unavailable, no invented defaults
// (spec section 15).
if(!HasUsableContext(ctx))
return out;
double sRsi = ComputeChannelSuitability(ctx, MSB_PROFILE_RSI);
double sStoch = ComputeChannelSuitability(ctx, MSB_PROFILE_STOCH);
double sCci = ComputeChannelSuitability(ctx, MSB_PROFILE_CCI);
double sMacd = ComputeChannelSuitability(ctx, MSB_PROFILE_MACD);
// Numerical safety (spec section 29): suitability finite and in [0,1].
if(!WeightFinite(sRsi) || sRsi < 0.0 || sRsi > 1.0 ||
!WeightFinite(sStoch) || sStoch < 0.0 || sStoch > 1.0 ||
!WeightFinite(sCci) || sCci < 0.0 || sCci > 1.0 ||
!WeightFinite(sMacd) || sMacd < 0.0 || sMacd > 1.0)
return out;
// Raw structural weights (spec section 16): the baseline expression is
// kept explicit so future research can distinguish the baseline from the
// contextual modulation.
double rRsi = BASE_RSI * sRsi;
double rStoch = BASE_STOCH * sStoch;
double rCci = BASE_CCI * sCci;
double rMacd = BASE_MACD * sMacd;
double sumRaw = rRsi + rStoch + rCci + rMacd;
// Zero raw-weight condition (spec section 18): valid=false; no division
// by zero, no equal fallback weights.
if(sumRaw <= WEIGHT_EPS)
return out;
// Normalization (spec section 17): w_i = r_i / sum(r_j).
double wRsi = rRsi / sumRaw;
double wStoch = rStoch / sumRaw;
double wCci = rCci / sumRaw;
double wMacd = rMacd / sumRaw;
// Numerical safety (spec section 29): defensive clamp after the division.
wRsi = MathMin(1.0, MathMax(0.0, wRsi));
wStoch = MathMin(1.0, MathMax(0.0, wStoch));
wCci = MathMin(1.0, MathMax(0.0, wCci));
wMacd = MathMin(1.0, MathMax(0.0, wMacd));
// Weight invariant: sum(w) == 1 within WEIGHT_EPS or the state is invalid.
double sumW = wRsi + wStoch + wCci + wMacd;
if(MathAbs(sumW - 1.0) > WEIGHT_EPS)
return InvalidDynamicWeights();
out.rsi = wRsi;
out.stoch = wStoch;
out.cci = wCci;
out.macd = wMacd;
out.valid = true;
return out;
}