NAV
  • Strategy Getting Started Guide
  • EPAM Systems all rights reserved, 2022

    Strategy Getting Started Guide

    In QuantOffice, a strategy is a special trading program designed to analyze market data and make trading decisions based on this analysis. QuantOffice allows you to design, test and run your trading strategies right from JupyterHub. There you can use THEA IDE to develop and debug your strategies written in Python programming language. Integration with TimeBase allows you to backtest your strategy on an extensive historical dataset before going live.

    This guide includes the following main topics:

    Develop Strategy in JupyterHub

    In this tutorial we will describe how to create a sample intraday strategy with EMA indicators which generates trading signals. Results will be displayed then in reports, price and PnL charts.

    This strategy will be composed of several building blocks:

    This strategy consists of the following files:

    Each level (each class correspondingly) has its own set of members and events:

    You can copy code fragments provided in this tutorial and use them in Jupyter Notebook when creating your strategy.

    Create a new folder for your strategy and add the following files:

    Step 1: Create New Strategy

    Namespaces

    import os, sys
    import traceback
    from quantoffice.quant_office_config import QuantOfficeConfig
    QuantOfficeConfig.add_main_qo_libs()
    
    from System import DateTime, TimeSpan
    from Deltix.EMS.API import *
    from Deltix.EMS.Coordinator import EMSParameters
    from quantoffice.order_executor.order_processor import OrderProcessor
    from Deltix.EMS.Simulator import OrderSimulatorParameters
    from QuantOffice.Execution import SimulationMode
    from QuantOffice.Execution.Utils import ConversionEngine, ConversionEngineParams
    from QuantOffice.Reporting.Model import ReportEngine
    from quantoffice.decorators import input_parameter, event, subscription, EventNames, wrapped, SubscriptionNames,\
        BarKind, BarUOM, BarSize, custom_report, custom_subscription
    from quantoffice.decorators.subscription import TickSubscriptionParams 
    from quantoffice.executor_base import PortfolioExecutorBase
    currentdir = os.path.abspath('')
    parentdir = os.path.dirname(os.path.dirname(currentdir))
    sys.path.append(parentdir + '/Library')
    from jupyter_helper import *
    

    1. Add the required namespaces at the beginning of portfolio_executor.py and create PortfolioExecutor class. It is necessary for the strategy to work properly.

    Intraday subscription

    class PortfolioExecutor(PortfolioExecutorBase):
        """This class contains portfolio level event processing logic."""
        @subscription(SubscriptionNames.Intraday, is_main=True, bar_size=BarSize(BarUOM.Second, 10), bar_kind=BarKind.Price)
        @subscription(SubscriptionNames.Tick, is_main=False, tick_subscription_params=TickSubscriptionParams(subscribe_bbo=True, create_quotes=True))
        def __init__(portfolio, base_portfolio_executor, *args, **kwargs):
            PortfolioExecutorBase.__init__(portfolio, base_portfolio_executor, args=args, kwargs=kwargs)
            pass
    

    2. Define data subscriptions to supply market data from TimeBase to the strategy. Place the subscription annotation at the beginning of the PortfolioExecutor class and pass the subscription type. During strategy execution each strategy subscription reads data from the corresponding TimeBase stream (or streams). Intraday (is sometimes also called Intraday Bars or Bars) subscription is used in the current example to get data either from intraday stream(s) or from Tick stream and supply it to the strategy price bars.

    on_init and on_exit event handlers

        @event(EventNames.OnPortfolioInit)
        def on_init(portfolio):
            PortfolioExecutor.portfolio_executor = portfolio
    
        @event(EventNames.OnPortfolioExit)
        def on_exit(portfolio, exit_state):
            pass
    

    3. Add the on_init and on_exit event handlers to the portfolio_executor.py. This will be used later for initialization and finalization logic.

    Namespaces

    import clr
    import os
    from quantoffice.quant_office_config import QuantOfficeConfig
    QuantOfficeConfig.add_main_qo_libs()
    
    import System
    import Deltix
    from Deltix.EMS.API import *
    from Deltix.EMS.AlgoOrders import *
    from FinAnalysis.TA import *
    from FinAnalysis.Predicates import *
    from FinAnalysis.BurstDetection import *
    from quantoffice.decorators import event, EventNames, custom_subscription, errorlog
    from quantoffice.executor_base import InstrumentExecutorBase
    from quantoffice.quant_office_orders_helper import *
    from System.Drawing import Color, Pen
    from QuantOffice.Execution import LineStyle
    import sys
    currentdir = os.path.abspath('')
    parentdir = os.path.dirname(os.path.dirname(currentdir))
    sys.path.append(parentdir + '/Library')
    from metainfo import *
    from trading import *
    

    4. Add the required namespaces at the beginning of instrument_executor.py.

    Initialization

    class InstrumentExecutor(InstrumentExecutorBase):
        """This class contains instrument level event processing logic."""
    
        def __init__(instr, base_instrument_executor, portfolio_executor, *args, **kwargs):
            InstrumentExecutorBase.__init__(instr, base_instrument_executor, portfolio_executor, args=args, kwargs=kwargs)
            pass
    

    5. Create InstrumentExecutor class and add the file constructor to the instrument_executor.py which will initialize the instrument executor instance.

    Initialize Strategy Runner

    %run ../../Library/import_strategy_runner.ipynb
    
    # specify list of instruments
    symbols = 'BTCUSD'
    
    # specify streams used for backtesting
    price_stream = 'KRAKEN_BBO_ORIG'
    
    # specify bar size
    bar_size=BarSize(BarUOM.Minute, 1)
    
    # specify backtesting period
    start_time = "2020-01-01T00:00:00"
    end_time = "2020-02-01T00:00:00"
    
    # initialize strategy runner and subscriptions
    strategy_runner = PyStrategyRunner()
    subscriptions = []
    subscriptions.append(Subscription(Subscription.TickSubscriptionName, price_stream))
    
    strategy_runner = PyStrategyRunner()
    config = strategy_runner.build_config(symbols, start_time, end_time, subscriptions=subscriptions, bar_size=bar_size, use_tick_for_intraday=True)
    configure_charts(config, draw_symbols_chart=True, draw_portfolio_chart=True, draw_execution_tags=True, reporting_mode = ReportingMode.Jupyter)
    
    print ('Backtesting strategy ...')
    show_progress_bar()
    
    strategy_runner.start(on_progress_handler=on_progress_handler)
    strategy_runner.draw_charts(figure_height=400)
    print ('Completed!')
    

    6. Create backtester in backtesting.ipynb. Here we initialize Strategy Runner with backtesting parameters: list of instruments, price data source and the backtesting period (Note, in this tutorial we will use data that has been loaded in Getting Started Guide).

    7. Add the line that starts the Strategy Runner using the start() method and displays charts and the progress bar.

    8. Now everything is ready to backtest the strategy. Run it in Jupyter Notebook and follow the execution process. Once the backtesting is completed, you will see the progress bar and chart as on the attached picture.

    Step 2: Add EMA Indicators

    on_init event handler

        @event(EventNames.OnInit)
        def on_init(instr):
            instr.EMAN1 = Ema(instr.portfolio_executor.N1)
            instr.line_e_m_a_n1 = instr.create_chart_line(r"EMAN1", LineStyle.Line, Color.Red, 1.0, instr.get_symbol(),
                                                          instr.get_charting().get_price_chart(), 1,
                                                          instr.get_current_time(), "")
            instr.EMAN2 = Ema(instr.portfolio_executor.N2)
            instr.line_e_m_a_n2 = instr.create_chart_line(r"EMAN2", LineStyle.Line, Color.Blue, 1.0, instr.get_symbol(),
                                                          instr.get_charting().get_price_chart(), 1,
                                                          instr.get_current_time(), "")
            instr.is_cross_up = IsCrossUp(1)
            instr.is_cross_down = IsCrossDown(1)
    

    1. Add the on_init event to the instrument_executor.py. This event performs instrument-level initialization once for every trading instrument during the strategy start. Create EMA indicators with user-defined periods and lines for each trading instrument to display indicator values on charts.

    on_bar_close event EMA indicators update

        @event(EventNames.OnBarClose)
        def on_bar_close(instr):
            if instr.get_bars().Current is None:
                return
            current_time = instr.get_current_time()
            current_price = instr.get_current_price()
            instr.EMAN1.Add(current_price, current_time)
            eMAN1_value = instr.EMAN1.Value
            EMAN1_stable = instr.EMAN1.Stable
            if EMAN1_stable:
                instr.line_e_m_a_n1.draw(current_time, eMAN1_value)
            instr.EMAN2.Add(current_price, current_time)
            eMAN2_value = instr.EMAN2.Value
            eMAN2_stable = instr.EMAN2.Stable
            if eMAN2_stable:
                instr.line_e_m_a_n2.draw(current_time, eMAN2_value)
            if EMAN1_stable and eMAN2_stable:
                instr.is_cross_up.Add(eMAN1_value, eMAN2_value, current_time)
            if EMAN1_stable and eMAN2_stable:
                instr.is_cross_down.Add(eMAN1_value, eMAN2_value, current_time)        
    

    2. Indicators are updated with the current bar close price and their values are then drawn in the chart in the on_bar_close event of the instrument_executor.py.

    Declaration of input parameters

        @input_parameter("int", input_parameter_default_value="3", input_parameter_question="N1")
        def set_N1(portfolio, N1):
            portfolio.N1 = N1
    
        @input_parameter("int", input_parameter_default_value="8", input_parameter_question="N2")
        def set_N2(portfolio, N2):
            portfolio.N2 = N2
    

    3. Declare input parameters for two EMA indicators at the bottom of portfolio_executor.py. Input parameters allow to control strategy behavior externally by passing values during strategy start. This lets you run different tests without rebuilding the strategy. An additional field is created in the PortfolioExecutor class for each input parameter of the strategy. Decorator @input_parameter is used to declare input parameters.

    4. Run the strategy in Jupyter Notebook. Both EMA indicators are displayed on the Instrument chart.

    Step 3: Add Order Executor

    In this step we will add Order Executor to the strategy to enable execution of trading orders.

    Declaration of input parameters

        @input_parameter("double", input_parameter_default_value="0.1", input_parameter_question="bet_size")
        def set_bet_size(portfolio, bet_size):
            portfolio.bet_size = bet_size
    
        @input_parameter("Deltix.EMS.Coordinator.EMSParameters", input_parameter_default_value="create_ems_parameters()", input_parameter_question="EMS Parameters")
        def set_ems_parameters(portfolio, ems_parameters):
            portfolio.ems_parameters = ems_parameters
    
        @input_parameter("bool", input_parameter_default_value="true", input_parameter_question="Generate Reports ?")
        def set_generate_reports(portfolio, generate_reports):
            portfolio.generate_reports = generate_reports
    

    1. Add input parameters to control strategy trading and simulation modes. Declare bet_size, ems_parameters and generate_reports input parameters in the portfolio_executor.py.

    on_init event handler

        @event(EventNames.OnPortfolioInit)
        def on_init(portfolio):
            portfolio.order_executor = OrderProcessor(portfolio.base_portfolio_executor)
            portfolio.order_executor.set_parameters(portfolio.ems_parameters)
    

    2. Create and initialize OrderProcessor in the on_init method of the portfolio_executor.py. Pass PortfolioExecutor instance as a parameter into the OrderProcessor constructor. Call order_executor.SetParameters() method before using order processor. This method should be called only once when order processor is created.

    create_ems_parameters method

        @wrapped("create_ems_parameters", return_type="Deltix.EMS.Coordinator.EMSParameters")
        @staticmethod
        def create_ems_parameters(): # -> Deltix.EMS.Coordinator.EMSParameters
            ems_parameters = EMSParameters()
            simulator_params = OrderSimulatorParameters()
            simulator_params.simulationMode = SimulationMode.BBO
            ems_parameters.OrderExecutor.Parameters = simulator_params
            return ems_parameters
    

    3. OrderExecutor requires special parameters to be set before using it is the strategy. The create_ems_parameters() method is defined in the PortfolioExecutorclass and sets the initial values of the OrderExecutor parameters. This method is a standard pattern to define OrderExecutor parameters' initial value. The attached example shows how to create default order processor parameters to run strategy with the order simulator using BBO. Add EMS parameters with the code snippet in the attached example to the strategy in the portfolio_executor.py.

    on_init event handler

        # short reference on Order Execution Interface
        instr.order_executor = instr.portfolio_executor.order_executor      # created in PortfolioExecutor. Reference shared by all symbols
    

    4. Set order_executor object in the on_init event in the instrument_executor.py.

    Order sending logic

        @event(EventNames.OnBarClose)
        def on_bar_close(instr):
    
            ...
    
            if instr.is_cross_up.Stable and instr.is_cross_up.Value == 1:
                size = position_size(instr)
                bet_size = instr.portfolio_executor.bet_size
                qtty_to_buy = bet_size - size
                order = market_order(instr, qtty_to_buy, OrderSide.Buy, OrderTimeInForce.GTC)
                instr.order_executor.send_order(order)
            elif instr.is_cross_down.Stable and instr.is_cross_down.Value == 1:
                size = position_size(instr)
                bet_size = - instr.portfolio_executor.bet_size
                qtty_to_sell = bet_size - size
                order = market_order(instr, abs(qtty_to_sell), OrderSide.Sell, OrderTimeInForce.GTC)
                instr.order_executor.send_order(order)
    

    5. Now we can add the following trading logic to on_bar_close event: if the first EMA indicator crosses the second one up we send the buy order, and vice versa.

    6. Run the strategy in Jupyter Notebook and you will see execution tags for both Instrument and Portfolio charts.

    Step 4: Add Report Engine

    Report Engine is a standard component to collect and process strategy execution statistics for the subsequent analysis. Having the Order Executor initialized, we are ready to add the Report Engine to produce standard reports for our strategy.

    Declaration of input parameters

        @input_parameter("double", input_parameter_default_value="10000", input_parameter_question="Initial Capital (US$):")
        def set_initial_cap(portfolio, initial_cap):
            portfolio.initial_cap = initial_cap
    

    1. Add Initial Capital input parameter to the portfolio_executor.py.

    on_init event handler

        if portfolio.generate_reports:
            portfolio.report_engine = ReportEngine(portfolio.base_portfolio_executor, portfolio.order_executor, portfolio.order_executor)
            portfolio.report_engine.TradeReport.Enable()
            portfolio.report_engine.OrderReport.Enable()
            portfolio.report_engine.ExecutionReport.Enable()
            portfolio.report_engine.PerformanceReport.Enable()
            portfolio.report_engine.ByInstrumentReport.Enable()
            portfolio.report_engine.ByPeriodReport.Enable()
            portfolio.report_engine.DrawdownReport.Enable()
            portfolio.report_engine.AllocationReport.Enable()
            portfolio.report_engine.Start()
    

    2. Create Report Engine object, initialize its parameters and start the reports engine in the on_init event:

    on_exit event handler

        @event(EventNames.OnPortfolioExit)
        def on_exit(portfolio, exit_state):
            if portfolio.generate_reports:
                portfolio.report_engine.InitialCapital = portfolio.initial_cap
                portfolio.report_engine.Finish()
    

    3. Add the code snippet in the example to the on_exit event. This event will be called after the strategy execution to process simulation results and generate report data for the subsequent visualization.

    initialize reporting engine parameters

    config.StrategyParameters.initial_cap = 1000.0
    config.StrategyParameters.generate_reports = True
    

    4. Open the backtesting.ipynb and specify additional StrategyParameters. The attached example shows how to set default configuration parameters for the Report Engine. Execute it before starting the strategy runner.

    show_report event

    from portfolio_executor import PortfolioExecutor
    portfolio = PortfolioExecutor.portfolio_executor
    show_report(portfolio.get_performance_report().round(2), 'Performance Report')
    show_report(portfolio.get_order_report().round(2), 'Order Report')
    show_report(portfolio.get_trade_report().round(2), 'Trade Report')
    show_report(portfolio.get_execution_report().round(2), 'Execution Report')
    

    4. Add the attached code snippet to display the reports when the backtesting is completed. The reports are shown using ipyaggrid:

    5. Run the strategy in Jupyter Notebook and you will see the reports after the backtesting is completed.

    Running Strategy in Live Mode

    Define Live Trading Parameters

    # script to run strategy in live mode
    
    %run ../../Library/import_live_strategy_runner.ipynb
    from portfolio_executor import PortfolioExecutor
    
    # specify stream with market data used for live trading
    exchange = 'KRAKEN'
    # specify bar size
    bar_size = BarSize(BarUOM.Second, 10)
    
    # specify module key (identified of running instance of strategy)
    module_key = 'Tutorial'
    # specify list of instruments
    symbols = 'BTCUSD'
    
    #specify order destination
    broker = 'SIM'
    

    1. We will use an additional Jupyter Notebook with parameters required for live trading to run strategy in the live mode. Create live_trading.ipynb file and define main live trading parameters: price stream, symbols and order destination - refer to the attached code example.

    Intraday and Tick subscriptions

    subscriptions = []
    subscriptions.append(Subscription(Subscription.IntradaySubscriptionName, exchange))
    subscriptions.append(Subscription(Subscription.TickSubscriptionName, exchange))
    

    2. Set stream for Intraday and Tick subscriptions.

    Initialize Strategy Runner

    global strategy_status, live_strategy_runner, stop_button
    strategy_status = widgets.Label(value='Strategy Status: Starting ...')
    live_strategy_runner = PyLiveStrategyRunner()
    config = live_strategy_runner.build_config(symbols, subscriptions=subscriptions)
    config.BarSize = bar_size
    configure_live_order_execution(config.StrategyParameters.ems_parameters,
                                   exchange = exchange, 
                                   broker = broker, 
                                   trader='')
    configure_charts(config, draw_symbols_chart=True, draw_portfolio_chart=True, draw_execution_tags=True, reporting_mode = ReportingMode.Timebase)
    
    config.StrategyParameters.generate_reports = True
    config.StrategyParameters.N1 = 5
    config.StrategyParameters.N2 = 10
    config.StrategyParameters.exchange = exchange
    config.StrategyParameters.bet_size = 0.01
    
    stop_button = widgets.Button(description="Stop!", disabled = True)
    stop_button.on_click(on_stop_clicked)
    display(widgets.HBox([strategy_status, stop_button]))
    controller = live_strategy_runner.start_sync(module_key=module_key, module_status_callback=on_module_status_changed)
    

    3. Initialize the Strategy Runner, set parameters and run the strategy.

    4. Run live_trading.ipynb notebook. The strategy execution status will be displayed in the Jupyter Notebook and the Strategy Server monitor.

    Analyse Backtesting Results

    Based on the strategy configurations, it can display backtesting results in Jupyter Notebook or a Backtest Explorer application. All strategy backtesting results are stored in the QuantOffice Cloud repository. In Backtest Explorer you can access them to analyse your strategy performance over a specific period of time, view stats, performance metrics on portfolio and instrument levels, trades, executions, and more.