Dual Moving Average Crossovers with Filtered Scaling and Risk Controls
Summary
This strategy example uses the relationship between short- and long-period moving averages to identify directional trends. It filters crossover signals using the size of the moving-average difference, recent price momentum, and configurable thresholds tied to the instrument’s tick size. When the filters qualify a trend, it submits orders whose sizes come from a configured sequence, subject to a trade limit.
The implementation also tracks positions and orders, cancels unfinished orders after a set number of ticks, and manages exits using trend reversals, loss thresholds, and optional profit-taking with a drawdown parameter. It includes separate handling for exchange-specific position closing rules. The document is primarily an implementation example: it gives configurable parameters and operational logic, but no backtest results or evidence that the filters improve returns. Performance will depend on instrument, bar interval, execution behavior, and parameter selection.
Key ideas
- The strategy uses short- and long-period moving averages to detect directional crossovers.
- It filters entries by crossover magnitude and momentum before submitting orders.
- Order size follows a configurable sequence and is bounded by a trade limit.
- Position handling includes cancellation, reversal exits, and optional profit and loss controls.
- The source describes implementation mechanics but provides no performance evaluation.
Tags
Full text
# DualMA
# DualMA
strategy example: dual MA decision
short MA cross long MA, when get significant trends, place orders with volume decades
## Source (Apache-2.0)
```python
#!/usr/bin/env python
# encoding: utf-8
import sys
import logging
import logging.config
import time
from talib.abstract import SMA
import numpy as np
from collections import deque
#from gmsdk import StrategyBase, OrderSide_Ask, OrderSide_Bid, PositionEffect_Open, PositionEffect_Close
from gmsdk import *
import threading
eps = 1e-6
class DualMA(StrategyBase):
''' strategy example: dual MA decision
short MA cross long MA, when get significant trends, place orders with volume decades
'''
def __init__(self, *args, **kwargs):
self.logger = logging.getLogger(__name__)
#import pdb; pdb.set_trace()
super(DualMA, self).__init__(*args, **kwargs)
self.tick_size = self.config.getfloat('para', 'tick_size') or 0.2
self.half_tick_size = self.tick_size / 2.0
self.threshold_factor = self.config.getfloat('para', 'threshold_factor') or 1.3
self.significant_diff_factor = self.config.getfloat('para', 'significant_diff_factor') or 4.5
self.stop_lose_threshold_factor = self.config.getfloat('para', 'stop_lose_threshold_factor') or 1.5
self.stop_profit_threshold_factor = self.config.getfloat('para', 'stop_profit_threshold_factor') or 5.5
self.drawdown = self.config.getfloat('para', 'stop_profit_drawdown') or 0.3
self.threshold = self.tick_size * self.threshold_factor
self.significant_diff = self.tick_size * self.significant_diff_factor
self.stop_lose_threshold = self.tick_size * self.stop_lose_threshold_factor
self.stop_profit_threshold = self.tick_size * self.stop_profit_threshold_factor
self.exchange = self.config.get('para', 'trade_exchange')
self.sec_id = self.config.get('para', 'trade_ticker')
self.symbol = ".".join([self.exchange, self.sec_id]) or 'CFFEX.IF1406'
## trade unit list, eg. fib numbers [1,2,3,5,8] or [8, 4.0, 2.0, 1.0]
self.trade_unit = [int(x) for x in self.config.get('para', 'trade_unit_list').split(',')]
self.cancel_ticks = self.config.getint('para', 'cancel_ticks') or 10
self.trade_limit = self.config.getint('para', 'trade_limit')
self.trade_limit = min(self.trade_limit, len(self.trade_unit))
self.positive_stop = self.config.getboolean('para', 'positive_stop') or 0
self.hops = self.config.getint('para', 'hops') or 1
self.momentum_factor = self.config.getfloat('para', 'momentum_factor') or 1.1
self.window_size = self.config.getint('para', 'window_size') or 20
self.short_timeperiod = self.config.getint('para', 'short_timeperiod') or 5
self.long_timeperiod = self.config.getint('para', 'long_timeperiod') or 10
self.life_timeperiod = self.config.getint('para', 'life_timeperiod') or 30
self.bar_type = self.config.getint('para', 'bar_type') or 15
self.close_buffer = deque(maxlen=self.window_size)
# prepare historical bars for MA calculating
last_closes = [bar.close for bar in self.get_last_n_bars(self.symbol, self.bar_type, self.window_size)]
last_closes.reverse()
self.close_buffer.extend(last_closes)
self.orders = []
self.tick_counter = 0
##get positions
# long position
#self.a_p = self.get_position(self.exchange, self.sec_id, OrderSide_Ask) or Position()
# short position
#self.b_p = self.get_position(self.exchange, self.sec_id, OrderSide_Bid) or Position()
self.positions = dict()
ps = self.get_positions()
for p in ps:
#import pdb; pdb.set_trace();
sym = "{0}.{1}_{2}".format(p.exchange, p.sec_id, p.side)
self.positions[sym] = p
self.a_p = self.positions.get("{0}.{1}_{2}".format(self.exchange, self.sec_id, OrderSide_Ask))
self.b_p = self.positions.get("{0}.{1}_{2}".format(self.exchange, self.sec_id, OrderSide_Bid))
self.last_price = last_closes[-1] if len(last_closes) else 0 ## for backtest, make last price is last close
self.trade_count = 0
self.momentum = 0.0
self.long_trends = False
self.short_trends = False
self.moving = False
self.moving_long = False
self.moving_short = False
self.highest_pnl = 0.0
self.analyse_only = self.config.getboolean('para', 'analyse_only') or False
## 以下是行情订阅,包括实时行情或者是回放行情
## 分时数据处理
def on_bar(self, bar):
#self.logger.info( "received bar: %s" % bar.__dict__)
if bar.bar_type == self.bar_type:
# go to handle new bar data
self.close_buffer.append(bar.close)
self.logger.info("received bar: {0}, close: {1} ".format(bar.strtime, round(bar.close,1)))
self.algo_action()
self.tick_counter = 0
else:
self.try_stop_action()
## tick数据处理
def on_tick(self, tick):
## filter none own sec_id
if tick.sec_id != self.sec_id:
return
# self.logger.info( "received tick: %s" % tick.__dict__)
self.tick_counter += 1
if self.tick_counter >= self.cancel_ticks:
# cancel unfinished orders
self.cancel_unfinished_orders()
if (len(tick.bids)*len(tick.asks) == 0):
return
if (not tick.last_price > 0):
return
self.last_price = tick.last_price
# self.micro_structure(tick)
#self.care_positions()
#self.try_stop_action()
def micro_structure(self, tick):
#force = tick.bids[0][0] * tick.bids[0][1] - tick.asks[0][0] * tick.asks[0][1]
#whole_force = (tick.bids[0][0] * tick.bids[0][1] + tick.asks[0][0] * tick.asks[0][1])
# self.momentum = force/whole_force
# mean_price = (tick.bids[0][0]+tick.asks[0][0])/2.0
spread = tick.asks[0][0] - tick.bids[0][0]
mean_volume = (tick.bids[0][1]+tick.asks[0][1])/2.0
# self.momentum = force /mean_price /mean_volume
ask_vol = tick.asks[0][1]
bid_vol = tick.bids[0][1]
if spread > self.tick_size or abs(ask_vol - bid_vol) > mean_volume/3:
if ask_vol > bid_vol:
return OrderSide_Ask
if bid_vol > ask_vol:
return OrderSide_Bid
return 0
## 所有成交回报, 包括订单状态变化,撤单拒绝等,可以忽略,只处理如下面关心的订单状态变更信息
def on_execrpt(self, execution):
## filter none own sec_id
if execution.sec_id != self.sec_id:
return
self.logger.info("received execution: exec_type = {0}, rej_reason = {1} ".format(execution.exec_type, execution.ord_rej_reason_detail))
if execution.exec_type == 15:
self.logger.info('''
received execution filled: sec_id: {0}, side: {1}, filled volume: {2}, filled price: {3}
'''.format(execution.sec_id, execution.side, execution.volume, execution.price))
## 订单被接受
def on_order_new(self, order):
## filter none own sec_id
if order.sec_id != self.sec_id:
return
self.orders.append(order)
## 订单部分成交
def on_order_partially_filled(self, order):
## filter none own sec_id
if order.sec_id != self.sec_id:
return
self.logger.info('''
received order partially filled: sec_id: {0}, side: {1}, pe: {2}, volume: {3}, filled price: {4}, filled: {5}
'''.format(order.sec_id, order.side, order.position_effect, order.volume, order.filled_vwap, order.filled_volume))
## 订单完全成交
def on_order_filled(self, order):
## filter none own sec_id
if order.sec_id != self.sec_id:
return
self.logger.info('''
received order filled: sec_id: {0}, side: {1}, volume: {2}, filled price: {3}, filled: {4}
'''.format(order.sec_id, order.side, order.volume, order.filled_vwap, order.filled_volume))
self.clean_final_orders(order)
def on_order_cancelled(self, order):
## filter none own sec_id
if order.sec_id != self.sec_id:
return
self.logger.info('''
received order cancelled {0}: sec_id: {1}, side: {2}, volume: {3}, price: {4}, filled: {5}
'''.format(order.cl_ord_id, order.sec_id, order.side, order.volume, order.price, order.filled_volume))
self.clean_final_orders(order)
## 订单被拒绝
def on_order_rejected(self, order):
## filter none own sec_id
if order.sec_id != self.sec_id:
return
self.logger.info('''
received order rejected {0}, sec_id: {1}, side: {2}, volume: {3}, price: {4}, filled: {5}, reason: {6}
'''.format(order.cl_ord_id, order.sec_id, order.side, order.volume, order.price, order.filled_volume, order.ord_rej_reason_detail))
#self.trade_count -= 1
self.clean_final_orders(order)
def close_long_positions(self, b_p, ord_price=None):
self.print_positions()
price = ord_price if ord_price else self.last_price - self.hops*self.tick_size
self.logger.info("try to close long ... {0} @ {1}, today's position first".format(b_p.volume, price))
if b_p.exchange in ('SHSE', 'SZSE'): ## stocks
if b_p.available_yesterday:
self.close_long(b_p.exchange, b_p.sec_id, price, b_p.available_yesterday)
else:
if self.exchange == 'SHFE': ## special in SHFE
if b_p.available_today > 0:
self.close_long(b_p.exchange, b_p.sec_id, price, b_p.available_today)
if b_p.available_yesterday > 0:
self.close_long_yesterday(b_p.exchange, b_p.sec_id, price, b_p.available_yesterday)
else:
self.close_long(b_p.exchange, b_p.sec_id, price, b_p.available)
def close_short_positions(self, a_p, ord_price=None):
self.print_positions()
price = ord_price if ord_price else self.last_price + self.hops*self.tick_size
self.logger.info("try to close short ... {0} @ {1}, today's position first".format(a_p.volume, price))
if a_p.exchange in ('SHSE', 'SZSE'): ## stocks, something must be wrong
pass
else:
if self.exchange == 'SHFE': ## special in SHFE
if a_p.available_today > 0:
self.close_short(a_p.exchange, a_p.sec_id, price, a_p.available_today)
if a_p.available_yesterday > 0:
self.close_short_yesterday(a_p.exchange, a_p.sec_id, price, a_p.available_yesterday)
else:
self.close_short(a_p.exchange, a_p.sec_id, price, a_p.available)
def print_positions(self):
if self.b_p:
b_p = self.b_p
self.logger.debug(
'long volume today = {0}/{1}, volume = {2}/{3}'.format(b_p.volume_today, b_p.available_today,
b_p.volume, b_p.available))
if self.a_p:
a_p = self.a_p
self.logger.debug(
'short volume today = {0}/{1}, volume = {2}/{3}'.format(a_p.volume_today, a_p.available_today,
a_p.volume, a_p.available))
## 移除本地管理的已进入完成状态的订单
def clean_final_orders(self, order):
self.logger.info("try to remove finished order {0}, sec_id {1} volume {2} price {3} ".format(order.cl_ord_id, order.sec_id, order.volume, order.price))
for o in self.orders:
if o.cl_ord_id == order.cl_ord_id:
self.orders.remove(o)
## 撤销未完成状态的订单
def cancel_unfinished_orders(self):
if len(self.orders) > 0:
for o in self.orders:
self.logger.info("try to cancel order {0}, sec_id {1} volume {2} price {3} ".format(o.cl_ord_id, o.sec_id, o.volume, o.price))
#self.trade_count -= 1
self.cancel_order(o.cl_ord_id)
## 仓位管理,关注持仓的最高收益,用价差表示
def care_positions(self):
if self.analyse_only:
return
a_p = self.a_p = self.get_position(self.exchange, self.sec_id, OrderSide_Ask)
b_p = self.b_p = self.get_position(self.exchange, self.sec_id, OrderSide_Bid)
self.logger.info('pos long: {0} vwap: {1}, pos short: {2}, vwap: {3}'.format(b_p.volume if b_p else 0.0,
round(b_p.vwap, 2) if b_p else 0.0,
a_p.volume if a_p else 0.0,
round(a_p.vwap, 2) if a_p else 0.0))
if a_p:
self.highest_pnl = max(a_p.vwap - self.last_price, self.highest_pnl)
elif b_p:
self.highest_pnl = max(self.last_price - b_p.vwap, self.highest_pnl)
else:
self.highest_pnl = 0.0
## 出场信号逻辑,停止持仓
def try_stop_action(self):
b_p = self.b_p
a_p = self.a_p
short_trends = self.short_trends
long_trends = self.long_trends
stop_profit_threshold = self.stop_profit_threshold
stop_lose_threshold = self.stop_lose_threshold
significant_diff = self.significant_diff
momentum = self.momentum
moving = self.moving
moving_long = self.moving_long
moving_short = self.moving_short
## 多仓
if b_p and b_p.volume > eps:
b_pnl = self.last_price - b_p.vwap
## 触发止损阈值,或趋势不再继续
if (b_pnl < - stop_lose_threshold ):
self.logger.info("stop lose, close long...")
self.close_long_positions(b_p)
self.trade_count = 0
elif (not long_trends or (moving_short and momentum < - significant_diff)):
self.logger.info("long trends stopped, close long...")
self.close_long_positions(b_p)
self.trade_count = 0
## 触发最大回撤阈值
elif self.threshold < b_pnl <= (1 - self.drawdown) * self.highest_pnl:
self.logger.info("pnl drawdown, stop profit, close long...")
self.close_long_positions(b_p)
self.trade_count = 0
## 触发固定止赢
elif self.positive_stop and b_pnl >= stop_profit_threshold:
self.logger.info("take fixed earning, stop profit, close long...")
self.close_long_positions(b_p)
self.trade_count = 0
## 空仓
if a_p and a_p.volume > eps:
a_pnl = -1.0 * (self.last_price - a_p.vwap)
## 触发止损阈值
if a_pnl < - stop_lose_threshold:
self.logger.info("stop lose or profit, close short...")
self.close_short_positions(a_p)
self.trade_count = 0
## 趋势不再继续
elif (not short_trends or (moving_long and momentum > significant_diff)):
self.logger.info("short trends stopped, close short...")
self.close_short_positions(a_p)
self.trade_count = 0
## 触发最大回撤阈值
elif self.threshold < a_pnl <= (1 - self.drawdown) * self.highest_pnl:
self.logger.info("pnl drawdown, stop profit, close short...")
self.close_short_positions(a_p)
self.trade_count = 0
## 触发固定止赢
elif self.positive_stop and a_pnl >= stop_profit_threshold:
self.logger.info("take fixed earning, stop profit, close short...")
self.close_short_positions(a_p)
self.trade_count = 0
## 趋势信号的进出场判断,只在分时数据到来时计算
def algo_action(self):
# type: () -> object
close = np.asarray(self.close_buffer)
if len(close) < self.life_timeperiod:
self.logger.info('data not enough! len = {0}'.format(len(close)))
return
sma = SMA({'close':close}, timeperiod=self.short_timeperiod)
lma = SMA({'close':close}, timeperiod=self.long_timeperiod) ## make sure last lma is a number
life = SMA({'close':close}, timeperiod=self.life_timeperiod)
s_l_ma_delta = sma[-1] - lma[-1] ## 最新的短长MA差值
last_ma = sma[-1] ## 最新的短MA
#self.logger.info('sma length = {0}'.format(len(sma)))
#if len(sma) < 3:
# return
#momentum = self.momentum = sma[-1] - sma[-2] ## MA冲量
momentum = self.momentum = self.last_price - sma[-1] ## 当前价格相对MA冲量
# #self.logger.info( 'close: ', close)
## 短MA变动趋势,true表示进入趋势,false表示震荡
moving_long = self.moving_long = moving_short = self.moving_short = False
sma_diff = sma[-1] - sma[-2]
sma_diff_1 = sma[-2] - sma[-3]
moving = self.moving = sma_diff * sma_diff_1 > 0
if moving and (sma_diff * momentum > 0 or momentum >= self.momentum_factor* s_l_ma_delta):
moving_long = self.moving_long = True
elif moving and (sma_diff * momentum > 0 or momentum <= self.momentum_factor* s_l_ma_delta):
moving_short = self.moving_short = True
## 均线是多头还是空头排列
long_trends = self.long_trends = (sma[-1] > lma[-1] > life[-1])
short_trends = self.short_trends = (sma[-1] < lma[-1] < life[-1])
self.logger.info('short ma: {0}, long ma: {1}, life line: {2}'.format(round(sma[-1],4), round(lma[-1],4), round(life[-1],4)))
self.logger.info('short long ma delta: {0}, last_ma: {1}, momentum: {2}'.format(round(s_l_ma_delta,4), round(last_ma,4), round(momentum,4)))
self.logger.info('## short ma moving long: {0}, moving short: {1}; Trends is long: {2}, is short: {3}.'.format(moving_long, moving_short, long_trends, short_trends))
if self.analyse_only:
return
## 下单基准价,为了能够成交,所有开平仓都会根据配置的跳数追价,这里先计算基准价格
price_base = max(self.last_price, close[-1]) if long_trends else (min(self.last_price, close[-1]) if short_trends else self.last_price)
self.logger.info("current trade count = {0}, price base = {1} ".format(self.trade_count, round(price_base, 1)))
self.care_positions()
a_p = self.a_p
b_p = self.b_p
# cancel unfinished orders
#self.cancel_unfinished_orders()
threshold = self.threshold
significant_diff = self.significant_diff
stop_lose_threshold = self.stop_lose_threshold
stop_profit_threshold = self.stop_profit_threshold
## 多信号
if long_trends: ## short ma cross up long ma
## 没有多仓
if (a_p is None or a_p.volume < eps):
signal_filter = (moving_long or momentum >= significant_diff) and s_l_ma_delta > threshold and self.trade_count < self.trade_limit
vol = self.trade_unit[self.trade_count] ## get order volume from configured list
if signal_filter and vol > eps:
self.trade_count += 1
ord_price = price_base + self.hops*self.tick_size
self.logger.info("open long ... count {0}, {1} @ {2}".format(self.trade_count, vol, ord_price))
self.open_long(self.exchange, self.sec_id, ord_price, vol)
## 有多仓,且有止赢要求
elif b_p and b_p.volume > eps:
checking_limit = self.trade_count == self.trade_limit ## 最大交易次数限制止赢
b_pnl = self.last_price - b_p.vwap ## 多仓浮赢
if self.positive_stop and (checking_limit or b_pnl >= stop_profit_threshold):
ord_price = price_base - self.hops*self.tick_size
self.logger.info("trade count {0} to limit time[{1}], stop profit, close long @ {2}".format(self.trade_count, self.trade_limit, ord_price))
self.close_long_positions(b_p, ord_price)
self.trade_count = 0
elif moving_short: ## 短线趋势反转,检查是否要riskoff
self.try_stop_action()
## 有空仓
elif a_p and a_p.volume > eps:
self.logger.info("trend changed, stop lose, close short")
self.close_short_positions(a_p)
self.trade_count = 0
else:
pass
## 空信号
elif short_trends : ## short ma cross down long ma
## 没有空仓
if (b_p is None or b_p.volume < eps):
signal_filter = (moving_short or momentum <= -significant_diff) and s_l_ma_delta < -threshold and self.trade_count < self.trade_limit
vol = self.trade_unit[self.trade_count] ## get order volume from configured list
if signal_filter and vol > eps:
ord_price = price_base - self.hops*self.tick_size
self.logger.info("open short ... count {0}, {1} @ {2}".format(self.trade_count, vol, ord_price))
self.trade_count += 1 ## 开仓计数器
self.open_short(self.exchange, self.sec_id, ord_price, vol)
## 有空仓,且有止赢要求
elif a_p and a_p.volume > eps:
checking_limit = self.trade_count == self.trade_limit ## 最大交易次数限制止赢
a_pnl = -1.0 * (self.last_price - a_p.vwap) ## 空仓浮赢
if self.positive_stop and (checking_limit or a_pnl >= stop_profit_threshold):
ord_price = price_base + self.hops*self.tick_size
self.logger.info("trade count {0} to limit time [{1}], stop profit, close short @ {2}".format(self.trade_count, self.trade_limit, ord_price))
self.close_short_positions(a_p, ord_price)
self.trade_count = 0
elif moving_long: ## 短线趋势反转,检查是否要riskoff
self.try_stop_action()
## 有多仓
elif b_p and b_p.volume > eps:
self.logger.info("trend changed, stop lose, close long")
self.close_long_positions(b_p)
self.trade_count = 0
else:
pass
else: ## check if need to stop trading, close all positions
self.logger.info("no trends, here check if need to stop ...")
self.try_stop_action()
if __name__ == '__main__':
ini_file = sys.argv[1] if len(sys.argv) > 1 else 'dual_ma.ini'
logging.config.fileConfig(ini_file)
dm = DualMA(config_file=ini_file)
dm.logger.info("Strategy info : %s" % (dm.__dict__))
dm.logger.info("Strategy dual ma ready, waiting for data ...")
ret = dm.run()
dm.logger.info("DualMA message %s" % (dm.get_strerror(ret)))
```Shown in full with attribution under the source's licence. Licence: Apache-2.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.