//+------------------------------------------------------------------+ //| OMV Model.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #ifndef OMV_MODEL_MQH #define OMV_MODEL_MQH #include "OMV Protobuf.mqh" //+------------------------------------------------------------------+ //| Capacity limits | //+------------------------------------------------------------------+ #define OMV_MAX_NODES 512 // Layers the graph may hold #define OMV_MAX_TENSORS 512 // Weight tensors the graph may hold #define OMV_MAX_PORTS 8 // Names one node may carry per side #define OMV_MAX_DIMS 8 // Axes one tensor may have //+------------------------------------------------------------------+ //| Graph layer | //+------------------------------------------------------------------+ struct OmvNode { // Name the operator this layer applies string op; // Name this layer was given string name; // List the names feeding this layer string feeds[OMV_MAX_PORTS]; // Count the names present int feedCount; // List the names this layer produces string emits[OMV_MAX_PORTS]; // Count the names present int emitCount; }; //+------------------------------------------------------------------+ //| Weight tensor | //+------------------------------------------------------------------+ struct OmvTensor { // Name the nodes refer to it by string name; // Measure each axis int dim[OMV_MAX_DIMS]; // Count the axes present int dims; // Record the ONNX type code int dataType; // Multiply every axis to size the block int values; // Track the smallest weight held double minValue; // Track the largest weight held double maxValue; // Track the average weight double meanValue; }; //+------------------------------------------------------------------+ //| Exposed graph port | //+------------------------------------------------------------------+ struct OmvPort { // Name the graph exposes string name; // Measure each axis int dim[OMV_MAX_DIMS]; // Count the axes present int dims; // Record the ONNX type code int elemType; }; //+------------------------------------------------------------------+ //| ONNX file reader | //+------------------------------------------------------------------+ class COmvModel { private: // Store the layers in file order OmvNode m_node[]; // Store the weight tensors OmvTensor m_tensor[]; // Store the graph inputs OmvPort m_input[]; // Store the graph outputs OmvPort m_output[]; // Count the layers held int m_nodes; // Count the tensors held int m_tensors; // Count the graph inputs held int m_inputs; // Count the graph outputs held int m_outputs; // Record the format revision long m_irVersion; // Record the operator set version int m_opset; // Name the tool that wrote the file string m_producer; // Name the graph inside the file string m_graphName; // Remember the file that was read string m_path; // Explain why the last load failed string m_error; bool ParseGraph(const uchar &raw[], const int from, const int to); bool ParseNode(const uchar &raw[], const int from, const int to); bool ParseTensor(const uchar &raw[], const int from, const int to); bool ParseSparse(const uchar &raw[], const int from, const int to); bool ParsePort(const uchar &raw[], const int from, const int to, OmvPort &port); void TensorStats(const uchar &raw[], const int from, const int to, OmvTensor &tensor); void PackedStats(const uchar &raw[], const int from, const int to, const int field, OmvTensor &tensor); public: COmvModel(void); bool Load(const string path); void Clear(void); int Nodes(void) const { return m_nodes; } int Tensors(void) const { return m_tensors; } int Inputs(void) const { return m_inputs; } int Outputs(void) const { return m_outputs; } long IrVersion(void) const { return m_irVersion; } int Opset(void) const { return m_opset; } string Producer(void) const { return m_producer; } string GraphName(void) const { return m_graphName; } string Path(void) const { return m_path; } string Error(void) const { return m_error; } int Parameters(void) const; void NodeAt(const int i, OmvNode &out) const { if(i >= 0 && i < m_nodes) out = m_node[i]; } void TensorAt(const int i, OmvTensor &out) const { if(i >= 0 && i < m_tensors) out = m_tensor[i]; } void InputAt(const int i, OmvPort &out) const { if(i >= 0 && i < m_inputs) out = m_input[i]; } void OutputAt(const int i, OmvPort &out) const { if(i >= 0 && i < m_outputs) out = m_output[i]; } string DimsText(const int &dim[], const int dims) const; string TypeName(const int code) const; }; //+------------------------------------------------------------------+ //| Construct an empty model | //+------------------------------------------------------------------+ COmvModel::COmvModel(void) { //--- Clear every field so a query before loading reports empty Clear(); } //+------------------------------------------------------------------+ //| Drop everything the previous file filled | //+------------------------------------------------------------------+ void COmvModel::Clear(void) { //--- Zero the counts that bound every array m_nodes = 0; m_tensors = 0; m_inputs = 0; m_outputs = 0; //--- Release the stored structures ArrayResize(m_node, 0); ArrayResize(m_tensor, 0); ArrayResize(m_input, 0); ArrayResize(m_output, 0); //--- Clear the header fields m_irVersion = 0; m_opset = 0; m_producer = ""; m_graphName = ""; m_path = ""; m_error = ""; } //+------------------------------------------------------------------+ //| Walk raw weight bytes for their range and average | //+------------------------------------------------------------------+ void COmvModel::TensorStats(const uchar &raw[], const int from, const int to, OmvTensor &tensor) { //--- Take eight bytes per value for a double and four otherwise const int width = (tensor.dataType == 11) ? 8 : 4; //--- Count the whole values the payload holds const int count = (to - from) / width; //--- Abort when the payload holds nothing if(count <= 0) return; //--- Point a cursor at the payload CPbReader bytes; if(!bytes.Attach(raw, from, to)) return; //--- Start the running range and total double lowest = 0.0, highest = 0.0, total = 0.0; //--- Visit every stored value for(int i = 0; i < count; i++) { //--- Read the value at the stored width const double v = (width == 8) ? bytes.ReadDouble() : (double)bytes.ReadFloat(); //--- Stop on a broken read if(bytes.Failed()) break; //--- Seed the range from the first value if(i == 0) { lowest = v; highest = v; } //--- Lower the floor when this value is smaller if(v < lowest) lowest = v; //--- Raise the ceiling when this value is larger if(v > highest) highest = v; //--- Add the value to the running total total += v; } //--- Publish the range and the average tensor.minValue = lowest; tensor.maxValue = highest; tensor.meanValue = total / count; } //+------------------------------------------------------------------+ //| Walk a typed payload the file used instead of raw bytes | //+------------------------------------------------------------------+ void COmvModel::PackedStats(const uchar &raw[], const int from, const int to, const int field, OmvTensor &tensor) { //--- Point a cursor at the payload CPbReader bytes; if(!bytes.Attach(raw, from, to)) return; //--- Start the running range and total double lowest = 0.0, highest = 0.0, total = 0.0; int seen = 0; //--- Read the payload the way the field it arrived in is written while(!bytes.Eof()) { //--- Take the next value in the packed run double v = 0.0; if(field == 4) v = (double)bytes.ReadFloat(); else if(field == 10) v = bytes.ReadDouble(); else v = (double)(long)bytes.ReadVarint(); //--- Stop on a broken read if(bytes.Failed()) break; //--- Seed the range from the first value if(seen == 0) { lowest = v; highest = v; } //--- Lower the floor when this value is smaller if(v < lowest) lowest = v; //--- Raise the ceiling when this value is larger if(v > highest) highest = v; //--- Add the value to the running total total += v; seen++; } //--- Abort when the read stopped before any value arrived if(seen <= 0) return; //--- Publish the range and the average tensor.minValue = lowest; tensor.maxValue = highest; tensor.meanValue = total / seen; } //+------------------------------------------------------------------+ //| Read one node into a layer | //+------------------------------------------------------------------+ bool COmvModel::ParseNode(const uchar &raw[], const int from, const int to) { //--- Refuse a file that exceeds the node limit if(m_nodes >= OMV_MAX_NODES) return false; //--- Point a cursor at this node CPbReader reader; if(!reader.Attach(raw, from, to)) return false; //--- Start an empty layer OmvNode layer; layer.op = ""; layer.name = ""; layer.feedCount = 0; layer.emitCount = 0; //--- Read every field of the node while(!reader.Eof()) { //--- Take the next field tag int field, wire; if(!reader.ReadTag(field, wire)) break; //--- Skip anything that is not text if(wire != PB_LENGTH) { if(!reader.SkipField(wire)) break; continue; } //--- Read the field as text string text; if(!reader.ReadString(text)) break; //--- Store an input name if(field == 1 && layer.feedCount < OMV_MAX_PORTS) layer.feeds[layer.feedCount++] = text; //--- Store an output name else if(field == 2 && layer.emitCount < OMV_MAX_PORTS) layer.emits[layer.emitCount++] = text; //--- Store the layer name else if(field == 3) layer.name = text; //--- Store the operator else if(field == 4) layer.op = text; } //--- Reject a node carrying no operator if(StringLen(layer.op) == 0) return false; //--- Append the finished layer ArrayResize(m_node, m_nodes + 1); m_node[m_nodes++] = layer; //--- Report success return true; } //+------------------------------------------------------------------+ //| Read one initializer into a weight tensor | //+------------------------------------------------------------------+ bool COmvModel::ParseTensor(const uchar &raw[], const int from, const int to) { //--- Refuse a file that exceeds the tensor limit if(m_tensors >= OMV_MAX_TENSORS) return false; //--- Point a cursor at this tensor CPbReader reader; if(!reader.Attach(raw, from, to)) return false; //--- Keep the slice so nested payloads can be located uchar inner[]; reader.Bytes(inner); //--- Start an empty tensor OmvTensor weights; weights.name = ""; weights.dims = 0; weights.dataType = 0; weights.values = 0; weights.minValue = 0.0; weights.maxValue = 0.0; weights.meanValue = 0.0; //--- Mark both places a tensor may keep its weights as not yet found int rawFrom = -1, rawTo = -1; int packFrom = -1, packTo = -1, packField = 0; //--- Read every field of the tensor while(!reader.Eof()) { //--- Take the next field tag int field, wire; if(!reader.ReadTag(field, wire)) break; //--- Store one axis length if(field == 1 && wire == PB_VARINT) { //--- Keep the axis while room remains if(weights.dims < OMV_MAX_DIMS) weights.dim[weights.dims++] = (int)reader.ReadVarint(); else reader.ReadVarint(); } //--- Store several axis lengths packed together else if(field == 1 && wire == PB_LENGTH) { //--- Locate the packed block int f2, t2; if(!reader.ReadBlock(f2, t2)) break; //--- Read every length the block holds CPbReader packed; packed.Attach(inner, f2, t2); while(!packed.Eof() && weights.dims < OMV_MAX_DIMS) weights.dim[weights.dims++] = (int)packed.ReadVarint(); } //--- Store the element type else if(field == 2 && wire == PB_VARINT) weights.dataType = (int)reader.ReadVarint(); //--- Store the tensor name else if(field == 8 && wire == PB_LENGTH) reader.ReadString(weights.name); //--- Locate the raw weight payload else if(field == 9 && wire == PB_LENGTH) { if(!reader.ReadBlock(rawFrom, rawTo)) break; } //--- Locate a typed payload, which the file may use instead else if(wire == PB_LENGTH && (field == 4 || field == 5 || field == 7 || field == 10 || field == 11)) { //--- Remember which typed field carried the weights if(!reader.ReadBlock(packFrom, packTo)) break; packField = field; } //--- Step over anything else else if(!reader.SkipField(wire)) break; } //--- Multiply every axis to size the block weights.values = (weights.dims > 0) ? 1 : 0; for(int i = 0; i < weights.dims; i++) weights.values *= weights.dim[i]; //--- Summarize whichever payload the file actually used if(rawFrom >= 0 && (weights.dataType == 1 || weights.dataType == 11)) TensorStats(inner, rawFrom, rawTo, weights); else if(packFrom >= 0) PackedStats(inner, packFrom, packTo, packField, weights); //--- Reject a tensor carrying no name if(StringLen(weights.name) == 0) return false; //--- Append the finished tensor ArrayResize(m_tensor, m_tensors + 1); m_tensor[m_tensors++] = weights; //--- Report success return true; } //+------------------------------------------------------------------+ //| Read a sparse initializer through its values tensor | //+------------------------------------------------------------------+ bool COmvModel::ParseSparse(const uchar &raw[], const int from, const int to) { //--- Point a cursor at the sparse tensor CPbReader reader; if(!reader.Attach(raw, from, to)) return false; //--- Keep the slice so the values tensor can be located uchar inner[]; reader.Bytes(inner); //--- Remember where the values sit int valFrom = -1, valTo = -1; //--- Read every field of the sparse tensor while(!reader.Eof()) { //--- Take the next field tag int field, wire; if(!reader.ReadTag(field, wire)) break; //--- Locate the tensor holding the values themselves if(field == 1 && wire == PB_LENGTH) { if(!reader.ReadBlock(valFrom, valTo)) break; } //--- Step over the indices, the shape and anything else else if(!reader.SkipField(wire)) break; } //--- Nothing to report without the values tensor if(valFrom < 0) return false; //--- Read the values as an ordinary tensor return ParseTensor(inner, valFrom, valTo); } //+------------------------------------------------------------------+ //| Read one value info into an exposed port | //+------------------------------------------------------------------+ bool COmvModel::ParsePort(const uchar &raw[], const int from, const int to, OmvPort &port) { //--- Start an empty port port.name = ""; port.dims = 0; port.elemType = 0; //--- Point a cursor at this value info CPbReader value; if(!value.Attach(raw, from, to)) return false; //--- Keep the slice so nested payloads can be located uchar level1[]; value.Bytes(level1); //--- Read every field of the value info while(!value.Eof()) { //--- Take the next field tag int field, wire; if(!value.ReadTag(field, wire)) break; //--- Store the port name if(field == 1 && wire == PB_LENGTH) { value.ReadString(port.name); continue; } //--- Step over anything but the type description if(field != 2 || wire != PB_LENGTH) { if(!value.SkipField(wire)) break; continue; } //--- Descend into the type description int t1, t2; if(!value.ReadBlock(t1, t2)) break; CPbReader type; type.Attach(level1, t1, t2); uchar level2[]; type.Bytes(level2); //--- Read every field of the type while(!type.Eof()) { //--- Take the next field tag int f2, w2; if(!type.ReadTag(f2, w2)) break; //--- Step over anything but the tensor description if(f2 != 1 || w2 != PB_LENGTH) { if(!type.SkipField(w2)) break; continue; } //--- Descend into the tensor description int s1, s2; if(!type.ReadBlock(s1, s2)) break; CPbReader tt; tt.Attach(level2, s1, s2); uchar level3[]; tt.Bytes(level3); //--- Read every field of the tensor description while(!tt.Eof()) { //--- Take the next field tag int f3, w3; if(!tt.ReadTag(f3, w3)) break; //--- Store the element type if(f3 == 1 && w3 == PB_VARINT) { port.elemType = (int)tt.ReadVarint(); continue; } //--- Step over anything but the shape if(f3 != 2 || w3 != PB_LENGTH) { if(!tt.SkipField(w3)) break; continue; } //--- Descend into the shape int p1, p2; if(!tt.ReadBlock(p1, p2)) break; CPbReader shape; shape.Attach(level3, p1, p2); uchar level4[]; shape.Bytes(level4); //--- Read every axis the shape holds while(!shape.Eof()) { //--- Take the next field tag int f4, w4; if(!shape.ReadTag(f4, w4)) break; //--- Step over anything but an axis if(f4 != 1 || w4 != PB_LENGTH) { if(!shape.SkipField(w4)) break; continue; } //--- Descend into the axis int d1, d2; if(!shape.ReadBlock(d1, d2)) break; CPbReader dim; dim.Attach(level4, d1, d2); //--- Read every field of the axis while(!dim.Eof()) { //--- Take the next field tag int f5, w5; if(!dim.ReadTag(f5, w5)) break; //--- Store the axis length if(f5 == 1 && w5 == PB_VARINT) { if(port.dims < OMV_MAX_DIMS) port.dim[port.dims++] = (int)dim.ReadVarint(); else dim.ReadVarint(); } //--- Step over a named axis else if(!dim.SkipField(w5)) break; } } } } } //--- Report a port that carries a name return StringLen(port.name) > 0; } //+------------------------------------------------------------------+ //| Read the graph into nodes, weights and ports | //+------------------------------------------------------------------+ bool COmvModel::ParseGraph(const uchar &raw[], const int from, const int to) { //--- Point a cursor at the graph CPbReader graph; if(!graph.Attach(raw, from, to)) { m_error = "graph out of range"; return false; } //--- Keep the slice so nested payloads can be located uchar inner[]; graph.Bytes(inner); //--- Read every field of the graph while(!graph.Eof()) { //--- Take the next field tag int field, wire; if(!graph.ReadTag(field, wire)) break; //--- Step over anything that is not a nested message if(wire != PB_LENGTH && field != 0) { if(!graph.SkipField(wire)) break; continue; } //--- Locate the nested payload int f2, t2; if(!graph.ReadBlock(f2, t2)) break; //--- Read a node if(field == 1) ParseNode(inner, f2, t2); //--- Read an initializer else if(field == 5) ParseTensor(inner, f2, t2); //--- Read a sparse initializer, whose values sit one level deeper else if(field == 15) ParseSparse(inner, f2, t2); //--- Decode the graph name from its payload else if(field == 2) { uchar tmp[]; ArrayResize(tmp, t2 - f2); for(int i = f2; i < t2; i++) tmp[i - f2] = inner[i]; m_graphName = CharArrayToString(tmp, 0, t2 - f2, CP_UTF8); } //--- Read a graph input else if(field == 11) { OmvPort port; if(ParsePort(inner, f2, t2, port)) { ArrayResize(m_input, m_inputs + 1); m_input[m_inputs++] = port; } } //--- Read a graph output else if(field == 12) { OmvPort port; if(ParsePort(inner, f2, t2, port)) { ArrayResize(m_output, m_outputs + 1); m_output[m_outputs++] = port; } } } //--- Report success return true; } //+------------------------------------------------------------------+ //| Open a file and read its whole structure | //+------------------------------------------------------------------+ bool COmvModel::Load(const string path) { //--- Drop any model held from a previous call Clear(); //--- Remember which file is being read m_path = path; //--- Read the whole file into memory CPbReader reader; if(!reader.LoadFile(path)) { m_error = "cannot open " + path; return false; } //--- Keep the bytes so nested payloads can be located uchar raw[]; reader.Bytes(raw); //--- Read every field of the model while(!reader.Eof()) { //--- Take the next field tag int field, wire; if(!reader.ReadTag(field, wire)) break; //--- Store the format revision if(field == 1 && wire == PB_VARINT) m_irVersion = (long)reader.ReadVarint(); //--- Store the producing tool else if(field == 2 && wire == PB_LENGTH) reader.ReadString(m_producer); //--- Read the graph itself else if(field == 7 && wire == PB_LENGTH) { int from, to; if(!reader.ReadBlock(from, to)) break; if(!ParseGraph(raw, from, to)) return false; } //--- Read the operator set version else if(field == 8 && wire == PB_LENGTH) { //--- Locate the operator set message int from, to; if(!reader.ReadBlock(from, to)) break; //--- Read its version field CPbReader ops; ops.Attach(raw, from, to); while(!ops.Eof()) { int f2, w2; if(!ops.ReadTag(f2, w2)) break; if(f2 == 2 && w2 == PB_VARINT) m_opset = (int)ops.ReadVarint(); else if(!ops.SkipField(w2)) break; } } //--- Step over anything else else if(!reader.SkipField(wire)) break; } //--- Report a file that could not be decoded if(reader.Failed() && m_nodes == 0) { m_error = "malformed protobuf in " + path; return false; } //--- Report a file that holds no graph if(m_nodes == 0) { m_error = "no nodes found in " + path; return false; } //--- Report success return true; } //+------------------------------------------------------------------+ //| Total every weight the file carries | //+------------------------------------------------------------------+ int COmvModel::Parameters(void) const { //--- Add the value count of every tensor int total = 0; for(int i = 0; i < m_tensors; i++) total += m_tensor[i].values; //--- Return the running total return total; } //+------------------------------------------------------------------+ //| Render a shape as a bracketed list | //+------------------------------------------------------------------+ string COmvModel::DimsText(const int &dim[], const int dims) const { //--- Print empty brackets for an unshaped tensor if(dims <= 0) return "[]"; //--- Join every axis with a separator string out = "["; for(int i = 0; i < dims; i++) { //--- Separate all but the first axis if(i > 0) out += "x"; //--- Append this axis length out += IntegerToString(dim[i]); } //--- Close the bracket return out + "]"; } //+------------------------------------------------------------------+ //| Name an ONNX element type code | //+------------------------------------------------------------------+ string COmvModel::TypeName(const int code) const { //--- Match the numeric types this viewer expects to meet switch(code) { case 1: return "float"; case 2: return "uint8"; case 3: return "int8"; case 6: return "int32"; case 7: return "int64"; case 8: return "string"; case 9: return "bool"; case 10: return "float16"; case 11: return "double"; } //--- Print the raw code for anything unrecognized return "type" + IntegerToString(code); } #endif // OMV_MODEL_MQH //+------------------------------------------------------------------+