def test_floor_with_ten_ones_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) for _i in range(20): floored_atr.update_raw(1.00000, 1.00000, 1.00000) # Act, Assert assert floored_atr.value == 5e-05
def test_floor_with_ten_ones_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) for i in range(20): floored_atr.update(1.00000, 1.00000, 1.00000) # Act # Assert self.assertEqual(5e-05, floored_atr.value)
def __init__( self, symbol: Symbol, bar_spec: BarSpecification, trade_size: Decimal, fast_ema_period: int, slow_ema_period: int, atr_period: int, trail_atr_multiple: float, order_id_tag: str, # Must be unique at 'trader level' ): """ Initialize a new instance of the `EMACrossStopEntryWithTrailingStop` class. Parameters ---------- symbol : Symbol The symbol for the strategy. bar_spec : BarSpecification The bar specification for the strategy. trade_size : Decimal The position size per trade. fast_ema_period : int The period for the fast EMA indicator. slow_ema_period : int The period for the slow EMA indicator. atr_period : int The period for the ATR indicator. trail_atr_multiple : float The ATR multiple for the trailing stop. order_id_tag : str The unique order identifier tag for the strategy. Must be unique amongst all running strategies for a particular trader identifier. """ super().__init__(order_id_tag=order_id_tag) # Custom strategy variables self.symbol = symbol self.bar_type = BarType(symbol, bar_spec) self.trade_size = trade_size self.trail_atr_multiple = trail_atr_multiple self.instrument = None # Initialize in on_start self.tick_size = None # Initialize in on_start self.price_precision = None # Initialize in on_start # Create the indicators for the strategy self.fast_ema = ExponentialMovingAverage(fast_ema_period) self.slow_ema = ExponentialMovingAverage(slow_ema_period) self.atr = AverageTrueRange(atr_period) # Users order management variables self.entry = None self.trailing_stop = None
def test_handle_bar_updates_indicator(self): # Arrange indicator = AverageTrueRange(10) bar = TestDataStubs.bar_5decimal() # Act indicator.handle_bar(bar) # Assert assert indicator.has_inputs assert indicator.value == 2.999999999997449e-05
def test_handle_bar_updates_indicator(self): # Arrange indicator = AverageTrueRange(10) bar = TestStubs.bar_5decimal() # Act indicator.handle_bar(bar) # Assert self.assertTrue(indicator.has_inputs) self.assertEqual(2.999999999997449e-05, indicator.value)
def __init__( self, symbol: Symbol, bar_spec: BarSpecification, trade_size: Decimal, fast_ema_period: int=10, slow_ema_period: int=20, atr_period: int=20, trail_atr_multiple: float=2.0, ): """ Initialize a new instance of the `EMACrossWithTrailingStop` class. Parameters ---------- symbol : Symbol The symbol for the strategy. bar_spec : BarSpecification The bar specification for the strategy. trade_size : Decimal The position size per trade. fast_ema_period : int The period for the fast EMA indicator. slow_ema_period : int The period for the slow EMA indicator. atr_period : int The period for the ATR indicator. trail_atr_multiple : float The ATR multiple for the trailing stop. """ # The order_id_tag should be unique at the 'trader level', here we are # just using the traded instruments symbol as the strategy order id tag. super().__init__(order_id_tag=symbol.code.replace('/', "")) # Custom strategy variables self.symbol = symbol self.bar_type = BarType(symbol, bar_spec) self.trade_size = trade_size self.trail_atr_multiple = trail_atr_multiple self.instrument = None # Initialize in on_start self.tick_size = None # Initialize in on_start # Create the indicators for the strategy self.fast_ema = ExponentialMovingAverage(fast_ema_period) self.slow_ema = ExponentialMovingAverage(slow_ema_period) self.atr = AverageTrueRange(atr_period) # Users order management variables self.entry = None self.trailing_stop = None
def test_floor_with_exponentially_decreasing_high_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) high = 1.00020 low = 1.00000 close = 1.00000 for _i in range(20): high -= (high - low) / 2 floored_atr.update_raw(high, low, close) # Act, Assert assert floored_atr.value == 5e-05
def __init__(self, config: VolatilityMarketMakerConfig): super().__init__(config) # Configuration self.instrument_id = InstrumentId.from_str(config.instrument_id) self.bar_type = BarType.from_str(config.bar_type) self.trade_size = Decimal(config.trade_size) self.atr_multiple = config.atr_multiple # Create the indicators for the strategy self.atr = AverageTrueRange(config.atr_period) self.instrument: Optional[Instrument] = None # Initialized in on_start # Users order management variables self.buy_order: Union[LimitOrder, None] = None self.sell_order: Union[LimitOrder, None] = None
def __init__( self, instrument_id: InstrumentId, bar_spec: BarSpecification, trade_size: Decimal, atr_period: int, atr_multiple: float, order_id_tag: str, # Must be unique at 'trader level' ): """ Initialize a new instance of the `VolatilityMarketMaker` class. Parameters ---------- instrument_id : InstrumentId The instrument identifier for the strategy. bar_spec : BarSpecification The bar specification for the strategy. trade_size : Decimal The position size per trade. atr_period : int The period for the ATR indicator. atr_multiple : float The ATR multiple for bracketing limit orders. order_id_tag : str The unique order identifier tag for the strategy. Must be unique amongst all running strategies for a particular trader identifier. """ super().__init__(order_id_tag=order_id_tag) # Custom strategy variables self.instrument_id = instrument_id self.bar_type = BarType(instrument_id, bar_spec) self.trade_size = trade_size self.atr_multiple = atr_multiple self.instrument = None # Request on start instead self.price_precision = None # Initialized on start # Create the indicators for the strategy self.atr = AverageTrueRange(atr_period) # Users order management variables self.buy_order: Union[LimitOrder, None] = None self.sell_order: Union[LimitOrder, None] = None
def __init__( self, symbol: Symbol, bar_spec: BarSpecification, trade_size: Decimal, atr_period: int = 20, atr_multiple: float = 2.0, ): """ Initialize a new instance of the `VolatilityMarketMaker` class. Parameters ---------- symbol : Symbol The symbol for the strategy. bar_spec : BarSpecification The bar specification for the strategy. trade_size : Decimal The position size per trade. atr_period : int The period for the ATR indicator. atr_multiple : float The ATR multiple for bracketing limit orders. """ # The order_id_tag should be unique at the 'trader level', here we are # just using the traded instruments symbol as the strategy order id tag. super().__init__(order_id_tag=symbol.code.replace('/', "")) # Custom strategy variables self.symbol = symbol self.bar_type = BarType(symbol, bar_spec) self.trade_size = trade_size self.atr_multiple = atr_multiple self.instrument = None # Request on start instead self.price_precision = None # Initialized on start # Create the indicators for the strategy self.atr = AverageTrueRange(atr_period) # Users order management variables self.buy_order: Union[LimitOrder, None] = None self.sell_order: Union[LimitOrder, None] = None
def __init__(self, config: EMACrossWithTrailingStopConfig): super().__init__(config) # Configuration self.instrument_id = InstrumentId.from_str(config.instrument_id) self.bar_type = BarType.from_str(config.bar_type) self.trade_size = Decimal(config.trade_size) self.trail_atr_multiple = config.trail_atr_multiple # Create the indicators for the strategy self.fast_ema = ExponentialMovingAverage(config.fast_ema_period) self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) self.atr = AverageTrueRange(config.atr_period) self.instrument: Optional[Instrument] = None # Initialized in on_start self.tick_size = None # Initialized in on_start # Users order management variables self.entry = None self.trailing_stop = None
def __init__(self, config: EMACrossBracketConfig): super().__init__(config) # Configuration self.instrument_id = InstrumentId.from_str(config.instrument_id) self.bar_type = BarType.from_str(config.bar_type) self.bracket_distance_atr = config.bracket_distance_atr self.trade_size = Decimal(config.trade_size) # Create the indicators for the strategy self.atr = AverageTrueRange(config.atr_period) self.fast_ema = ExponentialMovingAverage(config.fast_ema_period) self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) self.instrument: Optional[Instrument] = None # Initialized in on_start
def __init__(self, symbol: Symbol, bar_spec: BarSpecification, risk_bp: float = 10.0, fast_ema: int = 10, slow_ema: int = 20, atr_period: int = 20, sl_atr_multiple: float = 2.0, extra_id_tag: str = ''): """ Initializes a new instance of the EMACrossPy class. :param symbol: The symbol for the strategy. :param bar_spec: The bar specification for the strategy. :param risk_bp: The risk per trade (basis points). :param fast_ema: The fast EMA period. :param slow_ema: The slow EMA period. :param atr_period: The ATR period. :param sl_atr_multiple: The ATR multiple for stop-loss prices. :param extra_id_tag: An optional extra tag to append to order ids. """ super().__init__(order_id_tag=symbol.code + extra_id_tag, bar_capacity=40) # Custom strategy variables self.symbol = symbol self.bar_type = BarType(symbol, bar_spec) self.precision = 5 # dummy initial value for FX self.risk_bp = risk_bp self.entry_buffer = 0.0 # instrument.tick_size self.SL_buffer = 0.0 # instrument.tick_size * 10 self.SL_atr_multiple = sl_atr_multiple self.position_sizer = None # initialized in on_start() self.quote_currency = None # initialized in on_start() # Create the indicators for the strategy self.fast_ema = ExponentialMovingAverage(fast_ema) self.slow_ema = ExponentialMovingAverage(slow_ema) self.atr = AverageTrueRange(atr_period)
class AverageTrueRangeTests(unittest.TestCase): def setUp(self): # Fixture Setup self.atr = AverageTrueRange(10) def test_name_returns_expected_string(self): # Arrange # Act # Assert self.assertEqual("AverageTrueRange", self.atr.name) def test_str_repr_returns_expected_string(self): # Arrange # Act # Assert self.assertEqual("AverageTrueRange(10, SIMPLE, True, 0.0)", str(self.atr)) self.assertEqual("AverageTrueRange(10, SIMPLE, True, 0.0)", repr(self.atr)) def test_period(self): # Arrange # Act # Assert self.assertEqual(10, self.atr.period) def test_initialized_without_inputs_returns_false(self): # Arrange # Act # Assert self.assertEqual(False, self.atr.initialized) def test_initialized_with_required_inputs_returns_true(self): # Arrange # Act for _i in range(10): self.atr.update_raw(1.00000, 1.00000, 1.00000) # Assert self.assertEqual(True, self.atr.initialized) def test_handle_bar_updates_indicator(self): # Arrange indicator = AverageTrueRange(10) bar = TestStubs.bar_5decimal() # Act indicator.handle_bar(bar) # Assert self.assertTrue(indicator.has_inputs) self.assertEqual(2.999999999997449e-05, indicator.value) def test_value_with_no_inputs_returns_zero(self): # Arrange # Act # Assert self.assertEqual(0.0, self.atr.value) def test_value_with_epsilon_input(self): # Arrange epsilon = sys.float_info.epsilon self.atr.update_raw(epsilon, epsilon, epsilon) # Act # Assert self.assertEqual(0.0, self.atr.value) def test_value_with_one_ones_input(self): # Arrange self.atr.update_raw(1.00000, 1.00000, 1.00000) # Act # Assert self.assertEqual(0.0, self.atr.value) def test_value_with_one_input(self): # Arrange self.atr.update_raw(1.00020, 1.00000, 1.00010) # Act # Assert self.assertAlmostEqual(0.00020, self.atr.value) def test_value_with_three_inputs(self): # Arrange self.atr.update_raw(1.00020, 1.00000, 1.00010) self.atr.update_raw(1.00020, 1.00000, 1.00010) self.atr.update_raw(1.00020, 1.00000, 1.00010) # Act # Assert self.assertAlmostEqual(0.00020, self.atr.value) def test_value_with_close_on_high(self): # Arrange high = 1.00010 low = 1.00000 # Act for _i in range(1000): high += 0.00010 low += 0.00010 close = high self.atr.update_raw(high, low, close) # Assert self.assertAlmostEqual(0.00010, self.atr.value, 2) def test_value_with_close_on_low(self): # Arrange high = 1.00010 low = 1.00000 # Act for _i in range(1000): high -= 0.00010 low -= 0.00010 close = low self.atr.update_raw(high, low, close) # Assert self.assertAlmostEqual(0.00010, self.atr.value) def test_floor_with_ten_ones_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) for _i in range(20): floored_atr.update_raw(1.00000, 1.00000, 1.00000) # Act # Assert self.assertEqual(5e-05, floored_atr.value) def test_floor_with_exponentially_decreasing_high_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) high = 1.00020 low = 1.00000 close = 1.00000 for _i in range(20): high -= (high - low) / 2 floored_atr.update_raw(high, low, close) # Act # Assert self.assertEqual(5e-05, floored_atr.value) def test_reset_successfully_returns_indicator_to_fresh_state(self): # Arrange for _i in range(1000): self.atr.update_raw(1.00010, 1.00000, 1.00005) # Act self.atr.reset() # Assert self.assertFalse(self.atr.initialized) self.assertEqual(0, self.atr.value)
class VolatilityMarketMaker(TradingStrategy): """ A very dumb market maker which brackets the current market based on volatility measured by an ATR indicator. Cancels all orders and flattens all positions on stop. """ def __init__( self, instrument_id: InstrumentId, bar_spec: BarSpecification, trade_size: Decimal, atr_period: int, atr_multiple: float, order_id_tag: str, # Must be unique at 'trader level' ): """ Initialize a new instance of the `VolatilityMarketMaker` class. Parameters ---------- instrument_id : InstrumentId The instrument identifier for the strategy. bar_spec : BarSpecification The bar specification for the strategy. trade_size : Decimal The position size per trade. atr_period : int The period for the ATR indicator. atr_multiple : float The ATR multiple for bracketing limit orders. order_id_tag : str The unique order identifier tag for the strategy. Must be unique amongst all running strategies for a particular trader identifier. """ super().__init__(order_id_tag=order_id_tag) # Custom strategy variables self.instrument_id = instrument_id self.bar_type = BarType(instrument_id, bar_spec) self.trade_size = trade_size self.atr_multiple = atr_multiple self.instrument = None # Request on start instead self.price_precision = None # Initialized on start # Create the indicators for the strategy self.atr = AverageTrueRange(atr_period) # Users order management variables self.buy_order: Union[LimitOrder, None] = None self.sell_order: Union[LimitOrder, None] = None def on_start(self): """Actions to be performed on strategy start.""" self.instrument = self.data.instrument(self.instrument_id) self.price_precision = self.instrument.price_precision # Register the indicators for updating self.register_indicator_for_bars(self.bar_type, self.atr) # Get historical data self.request_bars(self.bar_type) # Subscribe to live data self.subscribe_bars(self.bar_type) self.subscribe_quote_ticks(self.instrument_id) # self.subscribe_order_book(self.instrument_id, level=2, depth=5, interval=5) # For debugging # self.subscribe_trade_ticks(self.instrument_id) # For debugging def on_instrument(self, instrument: Instrument): """ Actions to be performed when the strategy is running and receives an instrument. Parameters ---------- instrument : Instrument The instrument received. """ pass def on_order_book(self, order_book: OrderBook): """ Actions to be performed when the strategy is running and receives an order book. Parameters ---------- order_book : OrderBook The order book received. """ # self.log.info(f"Received {repr(order_book)}") # For debugging (must add a subscription) # self.log.info(str(order_book.asks())) # self.log.info(str(order_book.bids())) pass def on_quote_tick(self, tick: QuoteTick): """ Actions to be performed when the strategy is running and receives a quote tick. Parameters ---------- tick : QuoteTick The quote tick received. """ # self.log.info(f"Received {repr(tick)}") # For debugging (must add a subscription) pass def on_trade_tick(self, tick: TradeTick): """ Actions to be performed when the strategy is running and receives a trade tick. Parameters ---------- tick : TradeTick The tick received. """ # self.log.info(f"Received {repr(tick)}") # For debugging (must add a subscription) pass def on_bar(self, bar_type: BarType, bar: Bar): """ Actions to be performed when the strategy is running and receives a bar. Parameters ---------- bar_type : BarType The bar type received. bar : Bar The bar received. """ self.log.info(f"Received {bar_type} {repr(bar)}") # Check if indicators ready if not self.indicators_initialized(): self.log.info(f"Waiting for indicators to warm up " f"[{self.data.bar_count(self.bar_type)}]...") return # Wait for indicators to warm up... last: QuoteTick = self.data.quote_tick(self.instrument_id) if last is None: self.log.error("No quotes yet.") return # Maintain buy orders if self.buy_order and self.buy_order.is_working: self.cancel_order(self.buy_order) self.create_buy_order(last) # Maintain sell orders if self.sell_order and self.sell_order.is_working: self.cancel_order(self.sell_order) self.create_sell_order(last) def create_buy_order(self, last: QuoteTick): """ A market makers simple buy limit method (example). """ price: Decimal = last.bid - (self.atr.value * self.atr_multiple) order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=Quantity(self.trade_size), price=Price(price, self.price_precision), time_in_force=TimeInForce.GTC, post_only=True, # Default value is True hidden=False, # Default value is False ) self.buy_order = order self.submit_order(order) def create_sell_order(self, last: QuoteTick): """ A market makers simple sell limit method (example). """ price: Decimal = last.ask + (self.atr.value * self.atr_multiple) order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.SELL, quantity=Quantity(self.trade_size), price=Price(price, self.price_precision), time_in_force=TimeInForce.GTC, post_only=True, # Default value is True hidden=False, # Default value is False ) self.sell_order = order self.submit_order(order) def on_data(self, data: GenericData): """ Actions to be performed when the strategy is running and receives generic data. Parameters ---------- data : GenericData The data object received. """ pass def on_event(self, event: Event): """ Actions to be performed when the strategy is running and receives an event. Parameters ---------- event : Event The event received. """ last: QuoteTick = self.data.quote_tick(self.instrument_id) if last is None: self.log.error("No quotes yet.") return # If order filled then replace order at atr multiple distance from the market if isinstance(event, OrderFilled): if event.order_side == OrderSide.BUY: if self.buy_order.is_completed: self.create_buy_order(last) elif event.order_side == OrderSide.SELL: if self.sell_order.is_completed: self.create_sell_order(last) def on_stop(self): """ Actions to be performed when the strategy is stopped. """ self.cancel_all_orders(self.instrument_id) self.flatten_all_positions(self.instrument_id) # Unsubscribe from data self.unsubscribe_bars(self.bar_type) self.unsubscribe_quote_ticks(self.instrument_id) # self.unsubscribe_order_book(self.instrument_id, interval=5) # self.unsubscribe_trade_ticks(self.instrument_id) def on_reset(self): """ Actions to be performed when the strategy is reset. """ # Reset indicators here self.atr.reset() def on_save(self) -> {}: """ Actions to be performed when the strategy is saved. Create and return a state dictionary of values to be saved. Returns ------- dict[str, bytes] The strategy state dictionary. """ return {} def on_load(self, state: {}): """ Actions to be performed when the strategy is loaded. Saved state values will be contained in the give state dictionary. Parameters ---------- state : dict[str, bytes] The strategy state dictionary. """ pass def on_dispose(self): """ Actions to be performed when the strategy is disposed. Cleanup any resources used by the strategy here. """ pass
def setUp(self): # Arrange self.atr = AverageTrueRange(10)
class AverageTrueRangeTests(unittest.TestCase): # Fixture Setup def setUp(self): # Arrange self.atr = AverageTrueRange(10) def test_name(self): # Act # Assert self.assertEqual('AverageTrueRange', self.atr.name) def test_str(self): # Act # Assert self.assertEqual('AverageTrueRange(10, SIMPLE, True, 0.0)', str(self.atr)) def test_repr(self): # Act # Assert self.assertTrue( repr(self.atr).startswith( '<AverageTrueRange(10, SIMPLE, True, 0.0) object at')) self.assertTrue(repr(self.atr).endswith('>')) def test_period(self): # Act # Assert self.assertEqual(10, self.atr.period) def test_initialized_without_inputs_returns_false(self): # Act # Assert self.assertEqual(False, self.atr.initialized) def test_initialized_with_required_inputs_returns_true(self): # Act for i in range(10): self.atr.update(1.00000, 1.00000, 1.00000) # Assert self.assertEqual(True, self.atr.initialized) def test_initialized_with_required_mid_inputs_returns_true(self): # Act for i in range(10): self.atr.update_mid(1.00000) # Assert self.assertEqual(True, self.atr.initialized) def test_value_with_no_inputs_returns_zero(self): # Act # Assert self.assertEqual(0.0, self.atr.value) def test_value_with_epsilon_input(self): # Arrange epsilon = sys.float_info.epsilon self.atr.update(epsilon, epsilon, epsilon) # Act # Assert self.assertEqual(0.0, self.atr.value) def test_value_with_one_ones_input(self): # Arrange self.atr.update(1.00000, 1.00000, 1.00000) # Act # Assert self.assertEqual(0.0, self.atr.value) def test_value_with_one_input(self): # Arrange self.atr.update(1.00020, 1.00000, 1.00010) # Act # Assert self.assertAlmostEqual(0.00020, self.atr.value) def test_value_with_three_inputs(self): # Arrange self.atr.update(1.00020, 1.00000, 1.00010) self.atr.update(1.00020, 1.00000, 1.00010) self.atr.update(1.00020, 1.00000, 1.00010) # Act # Assert self.assertAlmostEqual(0.00020, self.atr.value) def test_value_with_three_mid_inputs(self): # Arrange self.atr.update_mid(1.00000) self.atr.update_mid(1.00020) self.atr.update_mid(1.00040) # Act # Assert self.assertAlmostEqual(0.00013333333333331865, self.atr.value) def test_value_with_close_on_high(self): # Arrange high = 1.00010 low = 1.00000 # Act for i in range(1000): high += 0.00010 low += 0.00010 close = high self.atr.update(high, low, close) # Assert self.assertAlmostEqual(0.00010, self.atr.value, 2) def test_value_with_close_on_low(self): # Arrange high = 1.00010 low = 1.00000 # Act for i in range(1000): high -= 0.00010 low -= 0.00010 close = low self.atr.update(high, low, close) # Assert self.assertAlmostEqual(0.00010, self.atr.value) def test_floor_with_ten_ones_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) for i in range(20): floored_atr.update(1.00000, 1.00000, 1.00000) # Act # Assert self.assertEqual(5e-05, floored_atr.value) def test_floor_with_exponentially_decreasing_high_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) high = 1.00020 low = 1.00000 close = 1.00000 for i in range(20): high -= (high - low) / 2 floored_atr.update(high, low, close) # Act # Assert self.assertEqual(5e-05, floored_atr.value) def test_reset_successfully_returns_indicator_to_fresh_state(self): # Arrange for i in range(1000): self.atr.update(1.00010, 1.00000, 1.00005) # Act self.atr.reset() # Assert self.assertEqual(0.0, self.atr.value) # No assertion errors. def test_with_battery_signal(self): # Arrange battery_signal = BatterySeries.create() output = [] # Act for point in BatterySeries.create(): self.atr.update(point, sys.float_info.epsilon, sys.float_info.epsilon) output.append(self.atr.value) # Assert self.assertEqual(len(battery_signal), len(output))
class EMACrossWithTrailingStop(TradingStrategy): """ A simple moving average cross example strategy with a stop-market entry and trailing stop. When the fast EMA crosses the slow EMA then submits a stop-market order one tick above the current bar for BUY, or one tick below the current bar for SELL. If the entry order is filled then a trailing stop at a specified ATR distance is submitted and managed. Cancels all orders and flattens all positions on stop. Parameters ---------- config : EMACrossWithTrailingStopConfig The configuration for the instance. """ def __init__(self, config: EMACrossWithTrailingStopConfig): super().__init__(config) # Configuration self.instrument_id = InstrumentId.from_str(config.instrument_id) self.bar_type = BarType.from_str(config.bar_type) self.trade_size = Decimal(config.trade_size) self.trail_atr_multiple = config.trail_atr_multiple # Create the indicators for the strategy self.fast_ema = ExponentialMovingAverage(config.fast_ema_period) self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) self.atr = AverageTrueRange(config.atr_period) self.instrument: Optional[Instrument] = None # Initialized in on_start self.tick_size = None # Initialized in on_start # Users order management variables self.entry = None self.trailing_stop = None def on_start(self): """Actions to be performed on strategy start.""" self.instrument = self.cache.instrument(self.instrument_id) if self.instrument is None: self.log.error( f"Could not find instrument for {self.instrument_id}") self.stop() return self.tick_size = self.instrument.price_increment # Register the indicators for updating self.register_indicator_for_bars(self.bar_type, self.fast_ema) self.register_indicator_for_bars(self.bar_type, self.slow_ema) self.register_indicator_for_bars(self.bar_type, self.atr) # Get historical data self.request_bars(self.bar_type) # Subscribe to live data self.subscribe_bars(self.bar_type) def on_instrument(self, instrument: Instrument): """ Actions to be performed when the strategy is running and receives an instrument. Parameters ---------- instrument : Instrument The instrument received. """ pass def on_order_book(self, order_book: OrderBook): """ Actions to be performed when the strategy is running and receives an order book. Parameters ---------- order_book : OrderBook The order book received. """ # self.log.info(f"Received {order_book}") # For debugging (must add a subscription) def on_quote_tick(self, tick: QuoteTick): """ Actions to be performed when the strategy is running and receives a quote tick. Parameters ---------- tick : QuoteTick The quote tick received. """ pass def on_trade_tick(self, tick: TradeTick): """ Actions to be performed when the strategy is running and receives a trade tick. Parameters ---------- tick : TradeTick The tick received. """ pass def on_bar(self, bar: Bar): """ Actions to be performed when the strategy is running and receives a bar. Parameters ---------- bar : Bar The bar received. """ self.log.info(f"Received {repr(bar)}") # Check if indicators ready if not self.indicators_initialized(): self.log.info( f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... if self.portfolio.is_flat(self.instrument_id): if self.fast_ema.value >= self.slow_ema.value: self.entry_buy() self.trailing_stop_sell(bar) else: # fast_ema.value < self.slow_ema.value self.entry_sell() self.trailing_stop_buy(bar) else: self.manage_trailing_stop(bar) def entry_buy(self): """ Users simple buy entry method (example). """ order = self.order_factory.market( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), ) self.submit_order(order) def entry_sell(self): """ Users simple sell entry method (example). """ order = self.order_factory.market( instrument_id=self.instrument_id, order_side=OrderSide.SELL, quantity=self.instrument.make_qty(self.trade_size), ) self.submit_order(order) def trailing_stop_buy(self, last_bar: Bar): """ Users simple trailing stop BUY for (``SHORT`` positions). Parameters ---------- last_bar : Bar The last bar received. """ price: Decimal = last_bar.high + (self.atr.value * self.trail_atr_multiple) order: StopMarketOrder = self.order_factory.stop_market( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), price=self.instrument.make_price(price), reduce_only=True, ) self.trailing_stop = order self.submit_order(order) def trailing_stop_sell(self, last_bar: Bar): """ Users simple trailing stop SELL for (LONG positions). """ price: Decimal = last_bar.low - (self.atr.value * self.trail_atr_multiple) order: StopMarketOrder = self.order_factory.stop_market( instrument_id=self.instrument_id, order_side=OrderSide.SELL, quantity=self.instrument.make_qty(self.trade_size), price=self.instrument.make_price(price), reduce_only=True, ) self.trailing_stop = order self.submit_order(order) def manage_trailing_stop(self, last_bar: Bar): """ Users simple trailing stop management method (example). Parameters ---------- last_bar : Bar The last bar received. """ if not self.trailing_stop: self.log.error("Trailing Stop order was None!") self.flatten_all_positions(self.instrument_id) return if self.trailing_stop.is_sell: new_trailing_price = last_bar.low - (self.atr.value * self.trail_atr_multiple) if new_trailing_price > self.trailing_stop.price: self.cancel_order(self.trailing_stop) self.trailing_stop_sell(last_bar) else: # trailing_stop.is_buy new_trailing_price = last_bar.high + (self.atr.value * self.trail_atr_multiple) if new_trailing_price < self.trailing_stop.price: self.cancel_order(self.trailing_stop) self.trailing_stop_buy(last_bar) def on_data(self, data: Data): """ Actions to be performed when the strategy is running and receives generic data. Parameters ---------- data : Data The data received. """ pass def on_event(self, event: Event): """ Actions to be performed when the strategy is running and receives an event. Parameters ---------- event : Event The event received. """ if isinstance(event, OrderFilled) and self.trailing_stop: if event.client_order_id == self.trailing_stop.client_order_id: last_bar = self.cache.bar(self.bar_type) if event.order_side == OrderSide.BUY: self.trailing_stop_sell(last_bar) elif event.order_side == OrderSide.SELL: self.trailing_stop_buy(last_bar) elif event.client_order_id == self.trailing_stop.client_order_id: self.trailing_stop = None def on_stop(self): """ Actions to be performed when the strategy is stopped. """ self.cancel_all_orders(self.instrument_id) self.flatten_all_positions(self.instrument_id) # Unsubscribe from data self.unsubscribe_bars(self.bar_type) def on_reset(self): """ Actions to be performed when the strategy is reset. """ # Reset indicators here self.fast_ema.reset() self.slow_ema.reset() self.atr.reset() def on_save(self) -> Dict[str, bytes]: """ Actions to be performed when the strategy is saved. Create and return a state dictionary of values to be saved. Returns ------- dict[str, bytes] The strategy state dictionary. """ return {} def on_load(self, state: Dict[str, bytes]): """ Actions to be performed when the strategy is loaded. Saved state values will be contained in the give state dictionary. Parameters ---------- state : dict[str, bytes] The strategy state dictionary. """ pass def on_dispose(self): """ Actions to be performed when the strategy is disposed. Cleanup any resources used by the strategy here. """ pass
class VolatilityMarketMaker(TradingStrategy): """ A very dumb market maker which brackets the current market based on volatility measured by an ATR indicator. Cancels all orders and flattens all positions on stop. Parameters ---------- config : VolatilityMarketMakerConfig The configuration for the instance. """ def __init__(self, config: VolatilityMarketMakerConfig): super().__init__(config) # Configuration self.instrument_id = InstrumentId.from_str(config.instrument_id) self.bar_type = BarType.from_str(config.bar_type) self.trade_size = Decimal(config.trade_size) self.atr_multiple = config.atr_multiple # Create the indicators for the strategy self.atr = AverageTrueRange(config.atr_period) self.instrument: Optional[Instrument] = None # Initialized in on_start # Users order management variables self.buy_order: Union[LimitOrder, None] = None self.sell_order: Union[LimitOrder, None] = None def on_start(self): """Actions to be performed on strategy start.""" self.instrument = self.cache.instrument(self.instrument_id) if self.instrument is None: self.log.error(f"Could not find instrument for {self.instrument_id}") self.stop() return # Register the indicators for updating self.register_indicator_for_bars(self.bar_type, self.atr) # Get historical data self.request_bars(self.bar_type) # Subscribe to live data self.subscribe_bars(self.bar_type) self.subscribe_quote_ticks(self.instrument_id) # self.subscribe_trade_ticks(self.instrument_id) # self.subscribe_order_book_deltas(self.instrument_id) # self.subscribe_ticker(self.instrument_id) # For debugging # self.subscribe_order_book_deltas(self.instrument_id, depth=20) # For debugging # self.subscribe_order_book_snapshots(self.instrument_id, depth=20) # For debugging def on_instrument(self, instrument: Instrument): """ Actions to be performed when the strategy is running and receives an instrument. Parameters ---------- instrument : Instrument The instrument received. """ pass def on_order_book(self, order_book: OrderBook): """ Actions to be performed when the strategy is running and receives an order book. Parameters ---------- order_book : OrderBook The order book received. """ # self.log.info(str(order_book)) # For debugging (must add a subscription) pass def on_order_book_delta(self, delta: OrderBookDelta): """ Actions to be performed when the strategy is running and receives an order book delta. Parameters ---------- delta : OrderBookDelta The order book delta received. """ # self.log.info(str(delta), LogColor.GREEN) # For debugging (must add a subscription) pass def on_quote_tick(self, tick: QuoteTick): """ Actions to be performed when the strategy is running and receives a quote tick. Parameters ---------- tick : QuoteTick The quote tick received. """ # self.log.info(f"Received {repr(tick)}") # For debugging (must add a subscription) pass def on_trade_tick(self, tick: TradeTick): """ Actions to be performed when the strategy is running and receives a trade tick. Parameters ---------- tick : TradeTick The tick received. """ # self.log.info(f"Received {repr(tick)}") # For debugging (must add a subscription) pass def on_bar(self, bar: Bar): """ Actions to be performed when the strategy is running and receives a bar. Parameters ---------- bar : Bar The bar received. """ self.log.info(f"Received {repr(bar)}") # Check if indicators ready if not self.indicators_initialized(): self.log.info( f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... last: QuoteTick = self.cache.quote_tick(self.instrument_id) if last is None: self.log.info("No quotes yet...") return # Maintain buy orders if self.buy_order and self.buy_order.is_open: self.cancel_order(self.buy_order) self.create_buy_order(last) # Maintain sell orders if self.sell_order and self.sell_order.is_open: self.cancel_order(self.sell_order) self.create_sell_order(last) def create_buy_order(self, last: QuoteTick): """ Market maker simple buy limit method (example). """ price: Decimal = last.bid - (self.atr.value * self.atr_multiple) order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), price=self.instrument.make_price(price), time_in_force=TimeInForce.GTC, post_only=True, # default value is True # display_qty=self.instrument.make_qty(self.trade_size / 2), # iceberg ) self.buy_order = order self.submit_order(order) def create_sell_order(self, last: QuoteTick): """ Market maker simple sell limit method (example). """ price: Decimal = last.ask + (self.atr.value * self.atr_multiple) order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.SELL, quantity=self.instrument.make_qty(self.trade_size), price=self.instrument.make_price(price), time_in_force=TimeInForce.GTC, post_only=True, # default value is True # display_qty=self.instrument.make_qty(self.trade_size / 2), # iceberg ) self.sell_order = order self.submit_order(order) def on_data(self, data: Data): """ Actions to be performed when the strategy is running and receives generic data. Parameters ---------- data : Data The data received. """ pass def on_event(self, event: Event): """ Actions to be performed when the strategy is running and receives an event. Parameters ---------- event : Event The event received. """ last: QuoteTick = self.cache.quote_tick(self.instrument_id) if last is None: self.log.info("No quotes yet...") return # If order filled then replace order at atr multiple distance from the market if isinstance(event, OrderFilled): if event.order_side == OrderSide.BUY: if self.buy_order.is_closed: self.create_buy_order(last) elif event.order_side == OrderSide.SELL: if self.sell_order.is_closed: self.create_sell_order(last) def on_stop(self): """ Actions to be performed when the strategy is stopped. """ self.cancel_all_orders(self.instrument_id) self.flatten_all_positions(self.instrument_id) # Unsubscribe from data self.unsubscribe_bars(self.bar_type) self.unsubscribe_quote_ticks(self.instrument_id) def on_reset(self): """ Actions to be performed when the strategy is reset. """ # Reset indicators here self.atr.reset() def on_save(self) -> Dict[str, bytes]: """ Actions to be performed when the strategy is saved. Create and return a state dictionary of values to be saved. Returns ------- dict[str, bytes] The strategy state dictionary. """ return {} def on_load(self, state: Dict[str, bytes]): """ Actions to be performed when the strategy is loaded. Saved state values will be contained in the give state dictionary. Parameters ---------- state : dict[str, bytes] The strategy state dictionary. """ pass def on_dispose(self): """ Actions to be performed when the strategy is disposed. Cleanup any resources used by the strategy here. """ pass
def setUp(self): # Fixture Setup self.atr = AverageTrueRange(10)
class TestAverageTrueRange: def setup(self): # Fixture Setup self.atr = AverageTrueRange(10) def test_name_returns_expected_string(self): # Arrange, Act, Assert assert self.atr.name == "AverageTrueRange" def test_str_repr_returns_expected_string(self): # Arrange, Act, Assert assert str(self.atr) == "AverageTrueRange(10, SIMPLE, True, 0.0)" assert repr(self.atr) == "AverageTrueRange(10, SIMPLE, True, 0.0)" def test_period(self): # Arrange, Act, Assert assert self.atr.period == 10 def test_initialized_without_inputs_returns_false(self): # Arrange, Act, Assert assert self.atr.initialized is False def test_initialized_with_required_inputs_returns_true(self): # Arrange, Act for _i in range(10): self.atr.update_raw(1.00000, 1.00000, 1.00000) # Assert assert self.atr.initialized is True def test_handle_bar_updates_indicator(self): # Arrange indicator = AverageTrueRange(10) bar = TestDataStubs.bar_5decimal() # Act indicator.handle_bar(bar) # Assert assert indicator.has_inputs assert indicator.value == 2.999999999997449e-05 def test_value_with_no_inputs_returns_zero(self): # Arrange, Act, Assert assert self.atr.value == 0.0 def test_value_with_epsilon_input(self): # Arrange epsilon = sys.float_info.epsilon self.atr.update_raw(epsilon, epsilon, epsilon) # Act, Assert assert self.atr.value == 0.0 def test_value_with_one_ones_input(self): # Arrange self.atr.update_raw(1.00000, 1.00000, 1.00000) # Act, Assert assert self.atr.value == 0.0 def test_value_with_one_input(self): # Arrange self.atr.update_raw(1.00020, 1.00000, 1.00010) # Act, Assert assert self.atr.value == pytest.approx(0.00020) def test_value_with_three_inputs(self): # Arrange self.atr.update_raw(1.00020, 1.00000, 1.00010) self.atr.update_raw(1.00020, 1.00000, 1.00010) self.atr.update_raw(1.00020, 1.00000, 1.00010) # Act, Assert assert self.atr.value == pytest.approx(0.00020) def test_value_with_close_on_high(self): # Arrange high = 1.00010 low = 1.00000 # Act for _i in range(1000): high += 0.00010 low += 0.00010 close = high self.atr.update_raw(high, low, close) # Assert assert self.atr.value == pytest.approx(0.00010, 2) def test_value_with_close_on_low(self): # Arrange high = 1.00010 low = 1.00000 # Act for _i in range(1000): high -= 0.00010 low -= 0.00010 close = low self.atr.update_raw(high, low, close) # Assert assert self.atr.value == pytest.approx(0.00010) def test_floor_with_ten_ones_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) for _i in range(20): floored_atr.update_raw(1.00000, 1.00000, 1.00000) # Act, Assert assert floored_atr.value == 5e-05 def test_floor_with_exponentially_decreasing_high_inputs(self): # Arrange floor = 0.00005 floored_atr = AverageTrueRange(10, value_floor=floor) high = 1.00020 low = 1.00000 close = 1.00000 for _i in range(20): high -= (high - low) / 2 floored_atr.update_raw(high, low, close) # Act, Assert assert floored_atr.value == 5e-05 def test_reset_successfully_returns_indicator_to_fresh_state(self): # Arrange for _i in range(1000): self.atr.update_raw(1.00010, 1.00000, 1.00005) # Act self.atr.reset() # Assert assert not self.atr.initialized assert self.atr.value == 0
class EMACrossStopEntryTrail(TradingStrategy): """ A simple moving average cross example strategy with a stop market entry and trailing stop. When the fast EMA crosses the slow EMA then submits a stop market order one tick above the current bar for BUY, or one tick below the current bar for SELL. If the entry order is filled then a trailing stop at a specified ATR distance is submitted and managed. Cancels all orders and flattens all positions on stop. """ def __init__( self, symbol: Symbol, bar_spec: BarSpecification, trade_size: Decimal, fast_ema_period: int, slow_ema_period: int, atr_period: int, trail_atr_multiple: float, order_id_tag: str, # Must be unique at 'trader level' ): """ Initialize a new instance of the `EMACrossStopEntryWithTrailingStop` class. Parameters ---------- symbol : Symbol The symbol for the strategy. bar_spec : BarSpecification The bar specification for the strategy. trade_size : Decimal The position size per trade. fast_ema_period : int The period for the fast EMA indicator. slow_ema_period : int The period for the slow EMA indicator. atr_period : int The period for the ATR indicator. trail_atr_multiple : float The ATR multiple for the trailing stop. order_id_tag : str The unique order identifier tag for the strategy. Must be unique amongst all running strategies for a particular trader identifier. """ super().__init__(order_id_tag=order_id_tag) # Custom strategy variables self.symbol = symbol self.bar_type = BarType(symbol, bar_spec) self.trade_size = trade_size self.trail_atr_multiple = trail_atr_multiple self.instrument = None # Initialize in on_start self.tick_size = None # Initialize in on_start self.price_precision = None # Initialize in on_start # Create the indicators for the strategy self.fast_ema = ExponentialMovingAverage(fast_ema_period) self.slow_ema = ExponentialMovingAverage(slow_ema_period) self.atr = AverageTrueRange(atr_period) # Users order management variables self.entry = None self.trailing_stop = None def on_start(self): """Actions to be performed on strategy start.""" self.instrument = self.data.instrument(self.symbol) if self.instrument is None: self.log.error(f"Could not find instrument for {self.symbol}") self.stop() return self.tick_size = self.instrument.tick_size self.price_precision = self.instrument.price_precision # Register the indicators for updating self.register_indicator_for_bars(self.bar_type, self.fast_ema) self.register_indicator_for_bars(self.bar_type, self.slow_ema) self.register_indicator_for_bars(self.bar_type, self.atr) # Get historical data self.request_bars(self.bar_type) # Subscribe to live data self.subscribe_bars(self.bar_type) def on_instrument(self, instrument: Instrument): """ Actions to be performed when the strategy is running and receives an instrument. Parameters ---------- instrument : Instrument The instrument received. """ pass def on_order_book(self, order_book: OrderBook): """ Actions to be performed when the strategy is running and receives an order book. Parameters ---------- order_book : OrderBook The order book received. """ # self.log.info(f"Received {order_book}") # For debugging (must add a subscription) def on_quote_tick(self, tick: QuoteTick): """ Actions to be performed when the strategy is running and receives a quote tick. Parameters ---------- tick : QuoteTick The quote tick received. """ pass def on_trade_tick(self, tick: TradeTick): """ Actions to be performed when the strategy is running and receives a trade tick. Parameters ---------- tick : TradeTick The tick received. """ pass def on_bar(self, bar_type: BarType, bar: Bar): """ Actions to be performed when the strategy is running and receives a bar. Parameters ---------- bar_type : BarType The bar type received. bar : Bar The bar received. """ self.log.info(f"Received {bar_type} {repr(bar)}") # Check if indicators ready if not self.indicators_initialized(): self.log.info(f"Waiting for indicators to warm up " f"[{self.data.bar_count(self.bar_type)}]...") return # Wait for indicators to warm up... if self.portfolio.is_flat(self.symbol): if self.entry is not None: self.cancel_order(self.entry) # BUY LOGIC if self.fast_ema.value >= self.slow_ema.value: self.entry_buy(bar) else: # fast_ema.value < self.slow_ema.value self.entry_sell(bar) else: self.manage_trailing_stop(bar) def entry_buy(self, last_bar: Bar): """ Users simple buy entry method (example). """ order: StopMarketOrder = self.order_factory.stop_market( symbol=self.symbol, order_side=OrderSide.BUY, quantity=Quantity(self.trade_size), price=Price(last_bar.high + (self.tick_size * 2)), ) self.entry = order self.submit_order(order) def entry_sell(self, last_bar: Bar): """ Users simple sell entry method (example). Parameters ---------- last_bar : Bar The last bar received. """ order: StopMarketOrder = self.order_factory.stop_market( symbol=self.symbol, order_side=OrderSide.SELL, quantity=Quantity(self.trade_size), price=Price(last_bar.low - (self.tick_size * 2)), ) self.entry = order self.submit_order(order) def trailing_stop_buy(self, last_bar: Bar): """ Users simple trailing stop BUY for (SHORT positions). Parameters ---------- last_bar : Bar The last bar received. """ # Round price to nearest 0.5 (for XBT/USD) price = round((last_bar.high + (self.atr.value * self.trail_atr_multiple)) * 2) / 2 order: StopMarketOrder = self.order_factory.stop_market( symbol=self.symbol, order_side=OrderSide.BUY, quantity=Quantity(self.trade_size), price=Price(price, self.instrument.price_precision), reduce_only=True, ) self.trailing_stop = order self.submit_order(order) def trailing_stop_sell(self, last_bar: Bar): """ Users simple trailing stop SELL for (LONG positions). """ # Round price to nearest 0.5 (for XBT/USD) price = round((last_bar.low - (self.atr.value * self.trail_atr_multiple)) * 2) / 2 order: StopMarketOrder = self.order_factory.stop_market( symbol=self.symbol, order_side=OrderSide.SELL, quantity=Quantity(self.trade_size), price=Price(price, self.instrument.price_precision), reduce_only=True, ) self.trailing_stop = order self.submit_order(order) def manage_trailing_stop(self, last_bar: Bar): """ Users simple trailing stop management method (example). Parameters ---------- last_bar : Bar The last bar received. """ self.log.info("Managing trailing stop...") if not self.trailing_stop: self.log.error("Trailing Stop order was None!") self.flatten_all_positions(self.symbol) return if self.trailing_stop.is_sell: new_trailing_price = round( (last_bar.low - (self.atr.value * self.trail_atr_multiple)) * 2) / 2 if new_trailing_price > self.trailing_stop.price: self.log.info( f"Moving SELL trailing stop to {new_trailing_price}.") self.cancel_order(self.trailing_stop) self.trailing_stop_sell(last_bar) else: # trailing_stop.is_buy new_trailing_price = round( (last_bar.high + (self.atr.value * self.trail_atr_multiple)) * 2) / 2 if new_trailing_price < self.trailing_stop.price: self.log.info( f"Moving BUY trailing stop to {new_trailing_price}.") self.cancel_order(self.trailing_stop) self.trailing_stop_buy(last_bar) def on_data(self, data): """ Actions to be performed when the strategy is running and receives a data object. Parameters ---------- data : object The data object received. """ pass def on_event(self, event): """ Actions to be performed when the strategy is running and receives an event. Parameters ---------- event : Event The event received. """ if isinstance(event, OrderFilled): if self.entry: if event.cl_ord_id == self.entry.cl_ord_id: last_bar = self.data.bar(self.bar_type) if event.order_side == OrderSide.BUY: self.trailing_stop_sell(last_bar) elif event.order_side == OrderSide.SELL: self.trailing_stop_buy(last_bar) if self.trailing_stop: if event.cl_ord_id == self.trailing_stop.cl_ord_id: self.trailing_stop = None def on_stop(self): """ Actions to be performed when the strategy is stopped. """ self.cancel_all_orders(self.symbol) self.flatten_all_positions(self.symbol) # Unsubscribe from data self.unsubscribe_bars(self.bar_type) def on_reset(self): """ Actions to be performed when the strategy is reset. """ # Reset indicators here self.fast_ema.reset() self.slow_ema.reset() self.atr.reset() def on_save(self) -> {}: """ Actions to be performed when the strategy is saved. Create and return a state dictionary of values to be saved. Returns ------- dict[str, bytes] The strategy state dictionary. """ return {} def on_load(self, state: {}): """ Actions to be performed when the strategy is loaded. Saved state values will be contained in the give state dictionary. Parameters ---------- state : dict[str, bytes] The strategy state dictionary. """ pass def on_dispose(self): """ Actions to be performed when the strategy is disposed. Cleanup any resources used by the strategy here. """ pass