神经网络变得简单(第 59 部分):控制二分法(DoC)·进阶篇
◍ 卷积层之后的潜变量堆叠
在第四层卷积处理完价格窗口后,网络继续向上堆叠四层潜变量与一层分布式回报输出。第四层把窗口输出直接当步长,window 与 step 都等于前层 window_out,并固定输出 4 个特征,优化器统一用 ADAM、激活走 LReLU。 第五到七层都是 defNeuronBaseOCL 基础全连接,节点数由 LatentCount 控制;第六层把激活换成 TANH 且把 prev_count 同步成 LatentCount,形成编码瓶颈,其余层保持 LReLU。 第八层切到 defNeuronFQF 分位数回报头,count 取 NRewards、window_out 写死 32,这意味着模型在内部把未来奖励拆成 32 个分位来估计,而非单点期望。 OnTick 里只在 IsNewBar 为真时跑逻辑:用 CopyRates 拉 NBarInPattern 根 K 线,并把 Rates 设成时间序列;随后 RSI、CCI、ATR、MACD 与品种对象全部 Refresh,保证指标和报价是最新快照。 历史数据循环里逐根取出 open 与 RSI.Main(b)、CCI.Main(b) 转 float,说明特征拼接发生在 Bar 粒度而非 Tick 粒度,外汇与贵金属品种下这种重算频率仍可能因点差跳变产生过拟合,需自行在 MT5 回测验证。
class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = prev_wout; descr.window_out = class="num">4; descr.optimization = ADAM; descr.activation = LReLU; if(!rtg.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!rtg.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = TANH; descr.optimization = ADAM; if(!rtg.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!rtg.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFQF; descr.count = NRewards; descr.window_out = class="num">32; descr.optimization = ADAM; if(!rtg.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if(!IsNewBar()) class="kw">return; class=class="str">"cmt">//--- class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), NBarInPattern, Rates); if(!ArraySetAsSeries(Rates, true)) class="kw">return; class=class="str">"cmt">//--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); Symb.Refresh(); Symb.RefreshRates(); class=class="str">"cmt">//--- History data class="type">float atr = class="num">0; for(class="type">int b = class="num">0; b < (class="type">int)NBarInPattern; b++) { class="type">float open = (class="type">float)Rates[b].open; class="type">float rsi = (class="type">float)RSI.Main(b); class="type">float cci = (class="type">float)CCI.Main(b);
「把账户持仓压进状态数组」
这段逻辑紧接上一节的指标采样,把当前账户余额、净值和持仓多空分布一并写入状态结构,方便后续给模型或监控模块喂原始特征。 先取两个最基础的账户量:ACCOUNT_BALANCE 和 ACCOUNT_EQUITY,直接落进 sState.account[0] 与 [1]。这两个值实时反映资金面,外汇和贵金属杠杆高,净值回撤往往比余额更敏感。 接着遍历 PositionsTotal(),只筛本符号持仓。多单 volume 与 profit 分别累加进 buy_value / buy_profit,空单同理进 sell_value / sell_profit,最终写进 account[2]~[5]。 position_discount 的计算值得留意:它用持仓利润减去「持仓时长 × 1/(60*60*10) × 利润绝对值」。乘数是 1/36000,相当于把每毫秒的衰减摊薄得很小,倾向用于惩罚长期浮亏仓。开 MT5 把这段粘进 EA,改 multiplyer 分母看 discount 曲线怎么变,就能验证它对持仓时间的惩罚强度。
atr = (class="type">float)ATR.Main(b); class="type">float macd = (class="type">float)MACD.Main(b); class="type">float sign = (class="type">float)MACD.Signal(b); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) class="kw">continue; class=class="str">"cmt">//--- class="type">int shift = b * BarDescr; sState.state[shift] = (class="type">float)(Rates[b].close - open); sState.state[shift + class="num">1] = (class="type">float)(Rates[b].high - open); sState.state[shift + class="num">2] = (class="type">float)(Rates[b].low - open); sState.state[shift + class="num">3] = (class="type">float)(Rates[b].tick_volume / class="num">1000.0f); sState.state[shift + class="num">4] = rsi; sState.state[shift + class="num">5] = cci; sState.state[shift + class="num">6] = atr; sState.state[shift + class="num">7] = macd; sState.state[shift + class="num">8] = sign; } bState.AssignArray(sState.state); class=class="str">"cmt">//--- Account description sState.account[class="num">0] = (class="type">float)AccountInfoDouble(ACCOUNT_BALANCE); sState.account[class="num">1] = (class="type">float)AccountInfoDouble(ACCOUNT_EQUITY); class=class="str">"cmt">//--- class="type">class="kw">double buy_value = class="num">0, sell_value = class="num">0, buy_profit = class="num">0, sell_profit = class="num">0; class="type">class="kw">double position_discount = class="num">0; class="type">class="kw">double multiplyer = class="num">1.0 / (class="num">60.0 * class="num">60.0 * class="num">10.0); class="type">int total = PositionsTotal(); class="type">class="kw">datetime current = TimeCurrent(); for(class="type">int i = class="num">0; i < total; i++) { if(PositionGetSymbol(i) != Symb.Name()) class="kw">continue; class="type">class="kw">double profit = PositionGetDouble(POSITION_PROFIT); class="kw">switch((class="type">int)PositionGetInteger(POSITION_TYPE)) { case POSITION_TYPE_BUY: buy_value += PositionGetDouble(POSITION_VOLUME); buy_profit += profit; break; case POSITION_TYPE_SELL: sell_value += PositionGetDouble(POSITION_VOLUME); sell_profit += profit; break; } position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit); } sState.account[class="num">2] = (class="type">float)buy_value; sState.account[class="num">3] = (class="type">float)sell_value; sState.account[class="num">4] = (class="type">float)buy_profit; sState.account[class="num">5] = (class="type">float)sell_profit; sState.account[class="num">6] = (class="type">float)position_discount;
把账户状态压成神经网络输入向量
这段逻辑干的事,是把当前账户与行情的时间特征拼装成一个特征向量 bState,喂给两层网络(RTG 与 Agent)做推理。注意第 7 个账户字段存的是 Rates[0].time 的 float 值,也就是当前 K 线时间戳,后续时间标签全靠它换算。 时间编码用了 4 个不同周期的正弦/余弦:年用 2023→2024 的秒数做分母,月/周/日分别用 PERIOD_MN1、PERIOD_W1、PERIOD_D1 的 PeriodSeconds 做分母,乘上 2π 后取三角函数。这样网络能隐式学到季节与周期节律,外汇和贵金属受周期影响明显,但高频噪声也可能被放大,属高风险信号。 推理结束后,代码把 Agent 输出 temp 做多空互斥裁剪:若 temp[0](买量)≥ temp[3](卖量),卖量归零;反之买量归零。随后用 Symb.LotsMin()、LotsStep()、StopsLevel() 约束真实下单手数,买量不足 min_lot 或 TP/SL 到点差 stops 以内就平掉已有买仓。 开 MT5 把这段塞进 EA 的 OnTick,重点看 bState.AddArray(AgentResult) 那行——上一次网络动作会作为本次状态的一部分回灌,这是策略产生记忆的关键,改 PERIOD 分母能直接改变周期敏感度。
sState.account[class="num">7] = (class="type">float)Rates[class="num">0].time; class=class="str">"cmt">//--- bState.Add((class="type">float)((sState.account[class="num">0] - PrevBalance) / PrevBalance)); bState.Add((class="type">float)(sState.account[class="num">1] / PrevBalance)); bState.Add((class="type">float)((sState.account[class="num">1] - PrevEquity) / PrevEquity)); bState.Add(sState.account[class="num">2]); bState.Add(sState.account[class="num">3]); bState.Add((class="type">float)(sState.account[class="num">4] / PrevBalance)); bState.Add((class="type">float)(sState.account[class="num">5] / PrevBalance)); bState.Add((class="type">float)(sState.account[class="num">6] / PrevBalance)); class=class="str">"cmt">//--- Time label class="type">class="kw">double x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;); bState.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x)); x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1); bState.Add((class="type">float)MathCos(class="num">2.0 * M_PI * x)); x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1); bState.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x)); x = (class="type">class="kw">double)Rates[class="num">0].time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1); bState.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x)); class=class="str">"cmt">//--- Prev action bState.AddArray(AgentResult); class=class="str">"cmt">//--- Return to go if(!RTG.feedForward(GetPointer(bState))) class="kw">return; RTG.getResults(Result); bState.AddArray(Result); if(!Agent.feedForward(GetPointer(bState), class="num">1, false, (CBufferFloat*)NULL)) class="kw">return; class=class="str">"cmt">//--- PrevBalance = sState.account[class="num">0]; PrevEquity = sState.account[class="num">1]; class=class="str">"cmt">//--- vector<class="type">float> temp; Agent.getResults(temp); class=class="str">"cmt">//--- class="type">class="kw">double min_lot = Symb.LotsMin(); class="type">class="kw">double step_lot = Symb.LotsStep(); class="type">class="kw">double stops = MathMax(Symb.StopsLevel(), class="num">1) * Symb.Point(); if(temp[class="num">0] >= temp[class="num">3]) { temp[class="num">0] -= temp[class="num">3]; temp[class="num">3] = class="num">0; } else { temp[class="num">3] -= temp[class="num">0]; temp[class="num">0] = class="num">0; } AgentResult = temp; class=class="str">"cmt">//--- buy control if(temp[class="num">0] < min_lot || (temp[class="num">1] * MaxTP * Symb.Point()) <= stops || (temp[class="num">2] * MaxSL * Symb.Point()) <= stops) { if(buy_value > class="num">0) CloseByDirection(POSITION_TYPE_BUY); } else { class="type">class="kw">double buy_lot = min_lot + MathRound((class="type">class="kw">double)(temp[class="num">0] - min_lot) / step_lot) * step_lot;
◍ 买卖双向仓位的挂单与平仓逻辑
这段控制块把买入和卖出两条线分开处理,核心是根据模型输出的 temp 数组计算手数、止盈止损,并决定加仓、减仓还是清仓。外汇与贵金属杠杆高,实盘跑之前务必在策略测试器里用历史数据验证这套仓位变更逻辑。 买入侧先用 Symb.NormalizePrice 把 Ask 加减 temp[1]/temp[2] 乘 MaxTP/MaxSL 再乘 Point 后的价格规范化,避免报价精度越界。若 buy_value 大于 0 就移动止盈止损;当模型目标手数 buy_lot 与当前持仓不等时,多则平部分、少则补单。 卖出侧先用 min_lot 与 stops 门槛过滤:若 temp[3] 小于最小手数,或止盈止损距离不足 stops,就直接按方向清仓。否则照买侧镜像算 sell_lot、sell_tp、sell_sl,并做移动与差额处理。 末尾把状态写回经验库:用 BarDescr*(NBarInPattern-1) 取偏移,空仓时按 atr/PrevBalance 记惩罚,否则奖励置 0;若 Base.Add 失败直接 ExpertRemove 退出。OnInit 里则先 LoadTotalBase,失败打错误码并返回 INIT_FAILED,模型权重随后以 float temp 载入。
class="type">class="kw">double buy_tp = Symb.NormalizePrice(Symb.Ask() + temp[class="num">1] * MaxTP * Symb.Point()); class="type">class="kw">double buy_sl = Symb.NormalizePrice(Symb.Ask() - temp[class="num">2] * MaxSL * Symb.Point()); if(buy_value > class="num">0) TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp); if(buy_value != buy_lot) { if(buy_value > buy_lot) ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot); else Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp); } } class=class="str">"cmt">//--- sell control if(temp[class="num">3] < min_lot || (temp[class="num">4] * MaxTP * Symb.Point()) <= stops || (temp[class="num">5] * MaxSL * Symb.Point()) <= stops) { if(sell_value > class="num">0) CloseByDirection(POSITION_TYPE_SELL); } else { class="type">class="kw">double sell_lot = min_lot + MathRound((class="type">class="kw">double)(temp[class="num">3] - min_lot) / step_lot) * step_lot;; class="type">class="kw">double sell_tp = Symb.NormalizePrice(Symb.Bid() - temp[class="num">4] * MaxTP * Symb.Point()); class="type">class="kw">double sell_sl = Symb.NormalizePrice(Symb.Bid() + temp[class="num">5] * MaxSL * Symb.Point()); if(sell_value > class="num">0) TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp); if(sell_value != sell_lot) { if(sell_value > sell_lot) ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot); else Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp); } } class=class="str">"cmt">//--- class="type">int shift=BarDescr*(NBarInPattern-class="num">1); sState.rewards[class="num">0] = bState[shift]; sState.rewards[class="num">1] = bState[shift+class="num">1]-class="num">1.0f; if((buy_value + sell_value) == class="num">0) sState.rewards[class="num">2] -= (class="type">float)(atr / PrevBalance); else sState.rewards[class="num">2] = class="num">0; for(class="type">ulong i = class="num">0; i < NActions; i++) sState.action[i] = AgentResult[i]; if(!Base.Add(sState)) ExpertRemove(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ResetLastError(); if(!LoadTotalBase()) { PrintFormat("Error of load study data: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- load models class="type">float temp;
「模型初始化失败的几个硬卡点」
在 MT5 里跑强化学习类 EA,初始化阶段最容易卡在神经网络维度对不上。下面这段逻辑先尝试加载 Agent 和 RTG 两个网络,若文件不存在就现场建描述数组并 Create,任何一步失败直接 return INIT_FAILED,不会带病启动。 加载或创建完后,代码用 getResults 和 GetLayerOutput(0,...) 反复校验输出节点数:Agent 的动作输出必须等于 NActions,RTG 的奖励输出必须等于 NRewards;Agent 输入层维度需等于 NRewards + BarDescr*NBarInPattern + AccountDescr + TimeDescription + NActions,RTG 输入层则不含 NRewards 那一项。任何一项 Total() 不匹配就打印具体差值并失败退出。 RTG.SetUpdateTarget(1000000) 把目标网络更新步数设到百万级,随后用 EventChartCustom 发一个自定义事件 'Init' 触发图表端训练;若事件创建失败(GetLastError 非 0)同样 INIT_FAILED。外汇与贵金属市场高杠杆、滑点无常,这类 EA 若初始化维度算错,实盘可能直接不交易或乱下单,务必在策略测试器里先跑通再上真金。 OnDeinit 里只做了一件事:Agent.Save(FileName+"Act.nnw",0,0,0,TimeCurrent(),true) 把最新权重落盘。注意 RTG 权重没在这里存,若你改了训练频率要自查是否漏存,否则重启 EA 会重新初始化 RTG。
if(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) || !RTG.Load(FileName + "RTG.nnw", dtStudied, true)) { Print("Init new models"); CArrayObj *agent = new CArrayObj(); CArrayObj *rtg = new CArrayObj(); if(!CreateDescriptions(agent,rtg)) { class="kw">delete agent; class="kw">delete rtg; class="kw">return INIT_FAILED; } if(!Agent.Create(agent) || !RTG.Create(rtg)) { class="kw">delete agent; class="kw">delete rtg; class="kw">return INIT_FAILED; } class="kw">delete agent; class="kw">delete rtg; } class=class="str">"cmt">//--- Agent.getResults(Result); if(Result.Total() != NActions) { PrintFormat("The scope of the agent does not match the actions count(%d <> %d)", NActions, Result.Total()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Agent.GetLayerOutput(class="num">0, Result); if(Result.Total() != (NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions)) { PrintFormat("Input size of Agent doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (NRewards + BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions)); class="kw">return INIT_FAILED; } RTG.getResults(Result); if(Result.Total() != NRewards) { PrintFormat("The scope of the RTG does not match the rewards count(%d <> %d)", NRewards, Result.Total()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- RTG.GetLayerOutput(class="num">0, Result); if(Result.Total() != (BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions)) { PrintFormat("Input size of RTG doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions)); class="kw">return INIT_FAILED; } RTG.SetUpdateTarget(class="num">1000000); if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init")) { PrintFormat("Error of create study event: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- Agent.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
训练循环里的状态拼装细节
训练函数 Train 的核心是嵌套循环:外层按 Iterations 上限跑随机轨迹,内层把单条历史缓冲拆成连续状态喂给智能体。MathRand 两次相乘再除以 32767 平方,是为了把随机起点压到更靠前的区间,避免样本总被截断在行情尾部。 状态向量 State 的拼装值得盯一眼:除价格类特征外,还塞了余额变化率、权益相对余额比、浮动盈亏占比,以及用 2023→2024 全年秒数归一化的时间正弦项。这种时间编码让模型对跨年 seasonal 偏移有概率上的感知,但具体泛化能力要在 MT5 里跑不同年份缓冲才看得出。 内层循环上限用 MathMin(Buffer[tr].Total - 2, i + HistoryBars * 3),意味着单幕最长只取历史窗的 3 倍步数。若 i 算出来为负就回退一次 iter 重抽,保证不越界。外汇与贵金属品种用这套训练,杠杆与滑点会让权益项剧烈跳动,属高风险验证,参数乱调可能过拟合到样本内噪声。 想复现就直接把这段贴进 EA 的 Train 调用链,重点改 HistoryBars 和 Iterations 看样本利用率变化。
RTG.Save(FileName + "RTG.nnw", TimeCurrent(), true); class="kw">delete Result; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Train(class="type">void) { class="type">int total_tr = ArraySize(Buffer); class="type">uint ticks = GetTickCount(); class="type">bool StopFlag = false; for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++) { class="type">int tr = (class="type">int)((MathRand() / class="num">32767.0) * (total_tr - class="num">1)); class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * MathMax(Buffer[tr].Total - class="num">2 * HistoryBars,MathMin(Buffer[tr].Total,class="num">20))); if(i < class="num">0) { iter--; class="kw">continue; } Actions = vector<class="type">float>::Zeros(NActions); Agent.Clear(); RTG.Clear(); for(class="type">int state = i; state < MathMin(Buffer[tr].Total - class="num">2,i + HistoryBars * class="num">3); state++) { class=class="str">"cmt">//--- History data State.AssignArray(Buffer[tr].States[state].state); class=class="str">"cmt">//--- Account description class="type">float PrevBalance = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">0] : Buffer[tr].States[state - class="num">1].account[class="num">0]); class="type">float PrevEquity = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">1] : Buffer[tr].States[state - class="num">1].account[class="num">1]); State.Add((Buffer[tr].States[state].account[class="num">0] - PrevBalance) / PrevBalance); State.Add(Buffer[tr].States[state].account[class="num">1] / PrevBalance); State.Add((Buffer[tr].States[state].account[class="num">1] - PrevEquity) / PrevEquity); State.Add(Buffer[tr].States[state].account[class="num">2]); State.Add(Buffer[tr].States[state].account[class="num">3]); State.Add(Buffer[tr].States[state].account[class="num">4] / PrevBalance); State.Add(Buffer[tr].States[state].account[class="num">5] / PrevBalance); State.Add(Buffer[tr].States[state].account[class="num">6] / PrevBalance); class=class="str">"cmt">//--- Time label class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;); State.Add((class="type">float)MathSin(class="num">2.0 * M_PI * x)); x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);