在交易中应用 OLAP(第四部分):定量和可视化分析测试器报告·进阶篇
(2/3)· 还在用眼睛扫优化结果里的几百行参数?用 OLAP 把测试器报告切成可拖拽的维度立方体
◍ OLAP 交易分析对话框的构造与处理链路
在 MT5 里做账户历史的多维切片,OLAPDialog 是把引擎、显示与 GUI 绑在一起的入口。模板参数 S、F 分别对应选择器与字段枚举,构造时把曲线类型写死为 CURVE_POINTS,并把外部传入的 OLAPEngine 实例地址存进 olapcore,同时 new 一个 OLAPDisplay 负责绘图。 析构只做一件事:delete olapdisplay,避免 GUI 对象泄漏。process() 才是重头戏——它从下拉框取值,把 0~9 的索引映射到 matches 数组里的 10 种选择器(SELECTOR_NONE 到 SELECTOR_DURATION),把 10 以上的索引映射到 subfields 里的 8 种直接字段(手数、利润金额、利润百分比、利润点数、佣金、库存费、自定义1、自定义2)。
| 坐标轴最多支持 3 个正交轴(AXES_NUMBER 循环里 i<AXES_NUMBER),若某轴不可见就跳过。v<10 走选择器分支,其中 v==5 | v==7 绑定开仓时间字段,v==6 | v==8 绑定平仓时间字段;v>=10 则按聚合器类型决定走标量还是分桶选择器。 |
|---|
聚合器非 AGGREGATOR_PROGRESSIVE 时,给 X 轴挂 AxisCustomizer,当选择器是 SELECTOR_DURATION 或需要按值排序时会启用自定义刻度。Y 轴仅在字段为 FIELD_DURATION 或恒等聚合+第二轴为时长时才挂自定义器。最后移除旧曲线、改按钮文字为 Processing,把组装好的参数甩给 olapcore.process()。 OnInit 里先判断 ReportFile 是否为空:空则接 _defaultHistoryAdapter 直接分析账户历史;非空则按扩展名分流,.htm 走 HTML 适配器,.csv 走 CSV 适配器,两者 load 失败或格式不对就返回 INIT_PARAMETERS_INCORRECT。对话框创建尺寸固定为 750×560,失败即 INIT_FAILED。打开 MT5 把这段 include 和 dialog 声明抄进 EA,接上自己的历史或报表文件就能跑出多维透视。
OLAPDialog(OLAPEngine<S,F> &olapimpl); ~OLAPDialog(class="type">void); class="kw">virtual class="type">int process() class="kw">override; class="kw">virtual class="type">void setup() class="kw">override; }; class="kw">template<class="kw">typename S, class="kw">typename F> OLAPDialog::OLAPDialog(OLAPEngine<S,F> &olapimpl) { curveType = CURVE_POINTS; olapcore = &olapimpl; olapdisplay = new OLAPDisplay(&this); } class="kw">template<class="kw">typename S, class="kw">typename F> OLAPDialog::~OLAPDialog(class="type">void) { class="kw">delete olapdisplay; } class="kw">template<class="kw">typename S, class="kw">typename F> class="type">int OLAPDialog::process() class="kw">override { SELECTORS Selectors[class="num">4]; ENUM_FIELDS Fields[class="num">4]; AGGREGATORS at = (AGGREGATORS)m_algo[class="num">0].Value(); ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[class="num">1].Value(); SORT_BY sb = (SORT_BY)m_algo[class="num">2].Value(); ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); class="type">int matches[class="num">10] = class=class="str">"cmt">// selectors in combo-boxes(specific record fields are bound internally) { SELECTOR_NONE, SELECTOR_SERIAL, SELECTOR_SYMBOL, SELECTOR_TYPE, SELECTOR_MAGIC, SELECTOR_WEEKDAY, SELECTOR_WEEKDAY, SELECTOR_DAYHOUR, SELECTOR_DAYHOUR, SELECTOR_DURATION }; class="type">int subfields[] = class=class="str">"cmt">// record fields listed in combo-boxes after selectors and accessible directly { FIELD_LOT, FIELD_PROFIT_AMOUNT, FIELD_PROFIT_PERCENT, FIELD_PROFIT_POINT, FIELD_COMMISSION, FIELD_SWAP, FIELD_CUSTOM_1, FIELD_CUSTOM_2 }; for(class="type">int i = class="num">0; i < AXES_NUMBER; i++) class=class="str">"cmt">// up to class="num">3 orthogonal axes are supported { if(!m_axis[i].IsVisible()) class="kw">continue; class="type">int v = (class="type">int)m_axis[i].Value(); if(v < class="num">10) class=class="str">"cmt">// selectors(every one is specialized for a field already) { Selectors[i] = (SELECTORS)matches[v]; if(v == class="num">5 || v == class="num">7) Fields[i] = FIELD_OPEN_DATETIME; else if(v == class="num">6 || v == class="num">8) Fields[i] = FIELD_CLOSE_DATETIME; } else class=class="str">"cmt">// pure fields { Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS; Fields[i] = (TRADE_RECORD_FIELDS)subfields[v - class="num">10]; } } m_plot.CurvesRemoveAll(); AxisCustomizer *customX = NULL; AxisCustomizer *customY = NULL; if(at == AGGREGATOR_IDENTITY || at == AGGREGATOR_COUNT) af = FIELD_NONE; if(at != AGGREGATOR_PROGRESSIVE) { customX = new AxisCustomizer(m_plot.getGraphic(), class="kw">false, Selectors[class="num">0] == SELECTOR_DURATION, (dimension > class="num">1 && SORT_VALUE(sb))); } if((af == FIELD_DURATION) || (at == AGGREGATOR_IDENTITY && Selectors[class="num">1] == SELECTOR_DURATION)) { customY = new AxisCustomizer(m_plot.getGraphic(), true, true); } m_plot.InitXAxis(customX); m_plot.InitYAxis(customY); m_button_ok.Text("Processing..."); class="kw">return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); } class="macro">#include <OLAP/OLAPTradesCustom.mqh> class=class="str">"cmt">// internally includes OLAPTrades.mqh class="macro">#include <OLAP/HTMLcube.mqh> class="macro">#include <OLAP/CSVcube.mqh> class="macro">#include <OLAP/GUI/OLAPGUI_trades.mqh> OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine); class="type">int OnInit() { if(ReportFile == "") { Print("Analyzing account history"); _defaultEngine.setAdapter(&_defaultHistoryAdapter); } else { if(StringFind(ReportFile, ".htm") > class="num">0 && _defaultHTMLReportAdapter.load(ReportFile)) { _defaultEngine.setAdapter(&_defaultHTMLReportAdapter); } else if(StringFind(ReportFile, ".csv") > class="num">0 && _defaultCSVReportAdapter.load(ReportFile)) { _defaultEngine.setAdapter(&_defaultCSVReportAdapter); } else { Print("Unknown file format: ", ReportFile); class="kw">return INIT_PARAMETERS_INCORRECT; } } ... if(!dialog.Create(class="num">0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), class="num">0, class="num">0, class="num">0, class="num">750, class="num">560)) class="kw">return INIT_FAILED; if(!dialog.Run()) class="kw">return INIT_FAILED; class="kw">return INIT_SUCCEEDED; }
「直接啃 tst 文件拿成交序列」
MT5 近期放开了测试器标准文件(*.tst)的读取权限。过去单次回测的成交明细只能先导出 HTML 再解析,现在用 fxsaber 的 SingleTesterCache 函数库,可在‘黑盒’层面直接载入 tst 拿到 Deal 数组,省掉报表转换环节。 接好库以后,建一个 SINGLETESTERCACHE 对象,调用 Load(文件路经) 即可。成功后 SingleTesterCache.Deals 里就是全部成交,每笔的字段都能直接点出来。下面这段演示了最小载入与计数打印: 把成交转成仓位的逻辑和之前解析 HTML 时一致。基类 BaseReportAdapter 放通用代码,HTMLReportAdapter 与新增的 TesterReportAdapter 各自继承,只需覆写 load 和 fillDealsArray 两个虚方法。TesterReportAdapter 内部挂一个 SINGLETESTERCACHE 指针,load 时把 tst 成交灌进 Deals,再由 fillDealsArray 加工。 项目里用 OLAPTradesCustom.mqh 把 CustomTradeRecord 宏定义为 RECORD_CLASS。连上适配器、输入里指定 tst 文件,就会自动产出带 MFE / MAE 自定义字段的仓位记录。下面示例取自某 tst,按品种画的余额曲线是连续不断的,说明 CGraphicInPlot::LinesPlot 没断点;用累积式聚合时,首个选择器务必用记录序号或索引。外汇与贵金属回测仅反映历史概率,实盘高风险。
class="macro">#include <fxsaber/SingleTesterCache/SingleTesterCache.mqh> ... SINGLETESTERCACHE SingleTesterCache; if(SingleTesterCache.Load(file)) { Print("Tester cache class="kw">import: ", ArraySize(SingleTesterCache.Deals), " deals"); } class="kw">virtual class="type">bool load(class="kw">const class="type">class="kw">string file) { reset(); TradeRecord::reset(); class="kw">return class="kw">false; } class="kw">virtual class="type">bool fillDealsArray() = class="num">0; class="type">int generate() { ... if(!fillDealsArray()) class="kw">return class="num">0; ... } class="kw">template<class="kw">typename T> class HTMLReportAdapter: class="kw">public BaseReportAdapter<T> { class="kw">protected: IndexMap *data; class="kw">virtual class="type">bool fillDealsArray() class="kw">override { for(class="type">int i = class="num">0; i < data.getSize(); ++i) { IndexMap *row = data[i]; if(CheckPointer(row) == POINTER_INVALID || row.getSize() != COLUMNS_COUNT) class="kw">return class="kw">false; class=class="str">"cmt">// something is broken class="type">class="kw">string s = row[COLUMN_SYMBOL].get<class="type">class="kw">string>(); StringTrimLeft(s); if(StringLen(s) > class="num">0) class=class="str">"cmt">// there is a symbol -> this is a deal { array << new Deal(row); } else if(row[COLUMN_TYPE].get<class="type">class="kw">string>() == "balance") { class="type">class="kw">string t = row[COLUMN_PROFIT].get<class="type">class="kw">string>(); StringReplace(t, " ", ""); balance += StringToDouble(t); } } class="kw">return true; } class="kw">public: ~HTMLReportAdapter() { if(CheckPointer(data) == POINTER_DYNAMIC) class="kw">delete data; } class="kw">virtual class="type">bool load(class="kw">const class="type">class="kw">string file) class="kw">override { BaseReportAdapter<T>::load(file); if(CheckPointer(data) == POINTER_DYNAMIC) class="kw">delete data; data = NULL; if(StringFind(file, ".htm") > class="num">0) { data = HTMLConverter::convertReport2Map(file, true); if(data != NULL) { size = generate(); Print(data.getSize(), " deals transferred to ", size, " trades"); } } class="kw">return data != NULL; } }; class TesterDeal: class="kw">public Deal { class="kw">public: TesterDeal(class="kw">const TradeDeal &td) { time = (class="type">class="kw">datetime)td.time_create + TimeShift; price = td.price_open; class="type">class="kw">string t = dealType(td.action); type = t == "buy" ? +class="num">1 : (t == "sell" ? -class="num">1 : class="num">0); t = dealDir(td.entry); direction = class="num">0; if(StringFind(t, "in") > -class="num">1) ++direction;
回测成交缓存的适配器写法
这段 MQL5 代码实现了一个针对策略测试器 .tst 缓存文件的报告适配器,核心是把底层 deal 流按类型拆成可统计的成交记录。
TesterReportAdapter 继承自 BaseReportAdapter,在 fillDealsArray() 里遍历 ptrSingleTesterCache.Deals:若 dealType 返回 balance 则累加至余额变量,否则封装成 TesterDeal 压入数组。这样回测里的赠金或余额变动不会污染真实交易统计。
load() 方法只认文件名含 .tst 的缓存;载入后会 Print 出一句话,例如 Tester cache import: 142 trades from 318 deals,说明 318 笔 deal 最终提炼成 142 笔可统计交易。你在 MT5 里跑完回测,直接用这个适配器读 .tst,能省掉自己解析二进制缓存的麻烦。
外汇与贵金属回测含高杠杆风险,适配器只解决数据解析,不替代样本外验证。
if(StringFind(t, "out") > -class="num">1) --direction; volume = (class="type">class="kw">double)td.volume; profit = td.profit; deal = (class="type">long)td.deal; order = (class="type">long)td.order; comment = td.comment[]; symbol = td.symbol[]; commission = td.commission; swap = td.storage; } class="kw">static class="type">class="kw">string dealType(class="kw">const ENUM_DEAL_TYPE type) { class="kw">return type == DEAL_TYPE_BUY ? "buy" : (type == DEAL_TYPE_SELL ? "sell" : "balance"); } class="kw">static class="type">class="kw">string dealDir(class="kw">const ENUM_DEAL_ENTRY entry) { class="type">class="kw">string result = ""; if(entry == DEAL_ENTRY_IN) result += "in"; else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY) result += "out"; else if(entry == DEAL_ENTRY_INOUT) result += "in out"; class="kw">return result; } }; class="kw">template<class="kw">typename T> class TesterReportAdapter: class="kw">public BaseReportAdapter<T> { class="kw">protected: SINGLETESTERCACHE *ptrSingleTesterCache; class="kw">virtual class="type">bool fillDealsArray() class="kw">override { for(class="type">int i = class="num">0; i < ArraySize(ptrSingleTesterCache.Deals); i++) { if(TesterDeal::dealType(ptrSingleTesterCache.Deals[i].action) == "balance") { balance += ptrSingleTesterCache.Deals[i].profit; } else { array << new TesterDeal(ptrSingleTesterCache.Deals[i]); } } class="kw">return true; } class="kw">public: ~TesterReportAdapter() { if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) class="kw">delete ptrSingleTesterCache; } class="kw">virtual class="type">bool load(class="kw">const class="type">class="kw">string file) class="kw">override { if(StringFind(file, ".tst") > class="num">0) { class=class="str">"cmt">// class="kw">default cleanup BaseReportAdapter<T>::load(file); class=class="str">"cmt">// specific cleanup if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) class="kw">delete ptrSingleTesterCache; ptrSingleTesterCache = new SINGLETESTERCACHE(); if(!ptrSingleTesterCache.Load(file)) { class="kw">delete ptrSingleTesterCache; ptrSingleTesterCache = NULL; class="kw">return class="kw">false; } size = generate(); Print("Tester cache class="kw">import: ", size, " trades from ", ArraySize(ptrSingleTesterCache.Deals), " deals"); } class="kw">return true; } }; TesterReportAdapter<RECORD_CLASS> _defaultTSTReportAdapter;
◍ 把优化缓存变成可透视的 OLAP 数据集
MT5 的优化结果除了单个测试文件,现在能用 TesterCache 函数库直接读 opt 缓存。在这个库之上套一层记录类、适配器和选择器,就能把每次优化通关变成 OLAP 可切片的数据,思路类似之前做成交记录的 OLAP 封装。 核心是从 ExpTradeSummary 结构里取字段,定义 OPT_CACHE_RECORD_FIELDS 枚举,再派生 OptCacheRecord 类把 long / double / int 统一塞进 double 数组。额外加的 FIELD_INDEX 记录索引,方便后续按通关编号定位。 适配器层用 OptCacheRecordInternal 承接函数库数据,把 EA 输入参数以 MqlParam 数组挂到 summary 后面。fillByTesterPass 方法就是枚举项和结构字段的映射表,构造函数吃进填充好的 internal 结构即可生成一条记录。 选择器决定怎么切数据:SELECTOR_QUANTS 把均线周期这类参数按取值摊成单元格(basket 大小设 0);盈利分布可按 100 单位步长分桶,这时 basket 设成步长。SELECTOR_INDEX 算累计、SELECTOR_SCALAR 收整体特征值,都在 OLAPCommon.mqh 里现成。 实盘外汇或贵金属策略做这类优化分析时,回测分布再漂亮也只代表历史样本,参数过拟合可能导致实盘失效,属高风险环节,建议先用小资金或模拟盘验证。
class="macro">#include <fxsaber/TesterCache/TesterCache.mqh> enum OPT_CACHE_RECORD_FIELDS { FIELD_NONE, FIELD_INDEX, FIELD_PASS, FIELD_DEPOSIT, FIELD_WITHDRAWAL, FIELD_PROFIT, FIELD_GROSS_PROFIT, FIELD_GROSS_LOSS, FIELD_MAX_TRADE_PROFIT, FIELD_MAX_TRADE_LOSS, FIELD_LONGEST_SERIAL_PROFIT, FIELD_MAX_SERIAL_PROFIT, FIELD_LONGEST_SERIAL_LOSS, FIELD_MAX_SERIAL_LOSS, FIELD_MIN_BALANCE, FIELD_MAX_DRAWDOWN, FIELD_MAX_DRAWDOWN_PCT, FIELD_REL_DRAWDOWN, FIELD_REL_DRAWDOWN_PCT, FIELD_MIN_EQUITY, FIELD_MAX_DRAWDOWN_EQ, FIELD_MAX_DRAWDOWN_PCT_EQ, FIELD_REL_DRAWDOWN_EQ, FIELD_REL_DRAWDOWN_PCT_EQ, FIELD_EXPECTED_PAYOFF, FIELD_PROFIT_FACTOR, FIELD_RECOVERY_FACTOR, FIELD_SHARPE_RATIO, FIELD_MARGIN_LEVEL, FIELD_CUSTOM_FITNESS, FIELD_DEALS, FIELD_TRADES, FIELD_PROFIT_TRADES, FIELD_LOSS_TRADES, FIELD_LONG_TRADES, FIELD_SHORT_TRADES, FIELD_WIN_LONG_TRADES, FIELD_WIN_SHORT_TRADES, FIELD_LONGEST_WIN_CHAIN, FIELD_MAX_PROFIT_CHAIN, FIELD_LONGEST_LOSS_CHAIN, FIELD_MAX_LOSS_CHAIN, FIELD_AVERAGE_SERIAL_WIN_TRADES, FIELD_AVERAGE_SERIAL_LOSS_TRADES }; class="macro">#define OPT_CACHE_RECORD_FIELDS_LAST(FIELD_AVERAGE_SERIAL_LOSS_TRADES + class="num">1) class="kw">struct OptCacheRecordInternal { ExpTradeSummary summary; class="type">MqlParam params[][class="num">5]; class=class="str">"cmt">// [][name, current, low, step, high] }; class OptCacheRecord: class="kw">public Record { class="kw">protected: class="kw">static class="type">int counter; class=class="str">"cmt">// number of passes class="type">void fillByTesterPass(class="kw">const OptCacheRecordInternal &internal) { class="kw">const ExpTradeSummary record = internal.summary; set(FIELD_INDEX, counter++); set(FIELD_PASS, record.Pass); set(FIELD_DEPOSIT, record.initial_deposit); set(FIELD_WITHDRAWAL, record.withdrawal); set(FIELD_PROFIT, record.profit); set(FIELD_GROSS_PROFIT, record.grossprofit); set(FIELD_GROSS_LOSS, record.grossloss); set(FIELD_MAX_TRADE_PROFIT, record.maxprofit); set(FIELD_MAX_TRADE_LOSS, record.minprofit); set(FIELD_LONGEST_SERIAL_PROFIT, record.conprofitmax); set(FIELD_MAX_SERIAL_PROFIT, record.maxconprofit); set(FIELD_LONGEST_SERIAL_LOSS, record.conlossmax); set(FIELD_MAX_SERIAL_LOSS, record.maxconloss); set(FIELD_MIN_BALANCE, record.balance_min); set(FIELD_MAX_DRAWDOWN, record.maxdrawdown); set(FIELD_MAX_DRAWDOWN_PCT, record.drawdownpercent); set(FIELD_REL_DRAWDOWN, record.reldrawdown); set(FIELD_REL_DRAWDOWN_PCT, record.reldrawdownpercent); set(FIELD_MIN_EQUITY, record.equity_min); set(FIELD_MAX_DRAWDOWN_EQ, record.maxdrawdown_e); set(FIELD_MAX_DRAWDOWN_PCT_EQ, record.drawdownpercent_e); set(FIELD_REL_DRAWDOWN_EQ, record.reldrawdown_e); set(FIELD_REL_DRAWDOWN_PCT_EQ, record.reldrawdownpercnt_e); set(FIELD_EXPECTED_PAYOFF, record.expected_payoff); set(FIELD_PROFIT_FACTOR, record.profit_factor); set(FIELD_RECOVERY_FACTOR, record.recovery_factor); set(FIELD_SHARPE_RATIO, record.sharpe_ratio); set(FIELD_MARGIN_LEVEL, record.margin_level); set(FIELD_CUSTOM_FITNESS, record.custom_fitness); set(FIELD_DEALS, record.deals); set(FIELD_TRADES, record.trades); set(FIELD_PROFIT_TRADES, record.profittrades); set(FIELD_LOSS_TRADES, record.losstrades); set(FIELD_LONG_TRADES, record.longtrades); set(FIELD_SHORT_TRADES, record.shorttrades); set(FIELD_WIN_LONG_TRADES, record.winlongtrades); set(FIELD_WIN_SHORT_TRADES, record.winshorttrades); set(FIELD_LONGEST_WIN_CHAIN, record.conprofitmax_trades); set(FIELD_MAX_PROFIT_CHAIN, record.maxconprofit_trades); set(FIELD_LONGEST_LOSS_CHAIN, record.conlossmax_trades); set(FIELD_MAX_LOSS_CHAIN, record.maxconloss_trades); set(FIELD_AVERAGE_SERIAL_WIN_TRADES, record.avgconwinners); set(FIELD_AVERAGE_SERIAL_LOSS_TRADES, record.avgconloosers);
「把优化缓存接进 OLAP 引擎」
上面这段把策略优化结果从 MT5 的 tester 缓存里读出来,再塞进一个轻量 OLAP 分析层。核心类是 OptCacheDataAdapter,它用 Cache.Load(optName) 把 .op 优化文件载入,size 直接取 Cache.Header.passes_passed——也就是实际跑过的优化 pass 总数,这个数就是你能遍历的最大记录条数。 customize() 里会扫 Cache.Inputs,把 flag 为真的输入参数名收进 paramNames,并用 ArrayPrint 打到日志。如果你只优化了 5 个参数但总输入有 20 个,控制台会明确打印 'Optimized Parameters (5 of 20):' 再加参数名数组,方便确认维度没漏。 getNext() 是遍历入口:cursor 从 0 走到 size-1,每步用 Cache.GetInputs(cursor, internal.params) 取出该 pass 的参数值,包成 T 类型记录返回。cursor 越界就返 NULL,外层循环据此停。 最底下 OLAPEngineOptCache 挂了四种选择器:SELECTOR_INDEX 按序号、SELECTOR_SCALAR 取单字段标量、SELECTOR_QUANTS 做分箱量化、SELECTOR_FILTER 做过滤。标准字段数由 adapter.getFieldCount() 给(即 OPT_CACHE_RECORD_FIELDS_LAST),自定义字段走 quantGranularity=0 不分箱。 开 MT5 跑完一次参数优化后,把 optName 换成你的 .op 文件名,用 _defaultOptCacheAdapter.load() 载入,再拿 _defaultEngine 套 SELECTOR_QUANTS 对某盈利字段分箱,就能在日志里直接看哪些参数区间倾向出好结果。外汇与贵金属优化结果受滑点与点差影响大,回测优异区间实盘可能失效,属高风险验证。
class="kw">const class="type">int n = ArrayRange(internal.params, class="num">0); for(class="type">int i = class="num">0; i < n; i++) { set(OPT_CACHE_RECORD_FIELDS_LAST + i, internal.params[i][PARAM_VALUE].double_value); } } class="kw">public: OptCacheRecord(class="kw">const class="type">int customFields = class="num">0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields) { } OptCacheRecord(class="kw">const OptCacheRecordInternal &record, class="kw">const class="type">int customFields = class="num">0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields) { fillByTesterPass(record); } class="kw">static class="type">int getRecordCount() { class="kw">return counter; } class="kw">static class="type">void reset() { counter = class="num">0; } }; class="kw">static class="type">int OptCacheRecord::counter = class="num">0; class="kw">template<class="kw">typename T> class OptCacheDataAdapter: class="kw">public DataAdapter { class="kw">private: class="type">int size; class="type">int cursor; class="type">int paramCount; class="type">class="kw">string paramNames[]; TESTERCACHE<ExpTradeSummary> Cache; class="type">void customize() { size = (class="type">int)Cache.Header.passes_passed; paramCount = (class="type">int)Cache.Header.opt_params_total; class="kw">const class="type">int n = ArraySize(Cache.Inputs); ArrayResize(paramNames, n); class="type">int k = class="num">0; for(class="type">int i = class="num">0; i < n; i++) { if(Cache.Inputs[i].flag) { paramNames[k++] = Cache.Inputs[i].name[]; } } if(k > class="num">0) { ArrayResize(paramNames, k); Print("Optimized Parameters(", paramCount, " of ", n, "):"); ArrayPrint(paramNames); } } class="kw">public: OptCacheDataAdapter() { reset(); } class="type">void load(class="kw">const class="type">class="kw">string optName) { if(Cache.Load(optName)) { customize(); reset(); } else { cursor = -class="num">1; } } class="kw">virtual class="type">void reset() class="kw">override { cursor = class="num">0; if(Cache.Header.version == class="num">0) class="kw">return; T::reset(); } class="kw">virtual class="type">int getFieldCount() class="kw">const class="kw">override { class="kw">return OPT_CACHE_RECORD_FIELDS_LAST; } class="kw">virtual Record *getNext() class="kw">override { if(cursor < size) { OptCacheRecordInternal internal; internal.summary = Cache[cursor]; Cache.GetInputs(cursor, internal.params); cursor++; class="kw">return new T(internal, paramCount); } class="kw">return NULL; } ... }; class="macro">#ifndef RECORD_CLASS class="macro">#define RECORD_CLASS OptCacheRecord class="macro">#endif OptCacheDataAdapter<RECORD_CLASS> _defaultOptCacheAdapter; enum OPT_CACHE_SELECTORS { SELECTOR_NONE, class=class="str">"cmt">// none SELECTOR_INDEX, class=class="str">"cmt">// ordinal number class=class="str">"cmt">/* all the next require a field as parameter */ SELECTOR_SCALAR, class=class="str">"cmt">// scalar(field) SELECTOR_QUANTS, class=class="str">"cmt">// quants(field) SELECTOR_FILTER class=class="str">"cmt">// filter(field) }; class OLAPEngineOptCache: class="kw">public OLAPEngine<OPT_CACHE_SELECTORS,OPT_CACHE_RECORD_FIELDS> { class="kw">protected: class="kw">virtual Selector<OPT_CACHE_RECORD_FIELDS> *createSelector(class="kw">const OPT_CACHE_SELECTORS selector, class="kw">const OPT_CACHE_RECORD_FIELDS field) class="kw">override { class="kw">const class="type">int standard = adapter.getFieldCount(); class="kw">switch(selector) { case SELECTOR_INDEX: class="kw">return new SerialNumberSelector<OPT_CACHE_RECORD_FIELDS,OptCacheRecord>(FIELD_INDEX); case SELECTOR_SCALAR: class="kw">return new OptCacheSelector(field); case SELECTOR_QUANTS: class="kw">return field != FIELD_NONE ? new QuantizationSelector<OPT_CACHE_RECORD_FIELDS>(field, (class="type">int)field < standard ? quantGranularity : class="num">0) : NULL; } class="kw">return NULL; } class="kw">public: OLAPEngineOptCache(): OLAPEngine() {} OLAPEngineOptCache(DataAdapter *ptr): OLAPEngine(ptr) {} }; OLAPEngineOptCache _defaultEngine;