Article-24231-ONNX-Models-P.../OMV Model.mqh

848 lines
No EOL
28 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+