Strategies

Developing a Strategy is core for using LiuAlgoTrader framework.

Assumptions

This section assumes the audience has:

  1. Carefully read and understood the Concepts section,
  2. Read the Scanners section and has a working setup of the framework with at least one Scanner.

No Liability Disclaimer

Any example, or sample is provided for training purposes only. You may choose to use it, modify it or completely disregard it - LiuAlgoTrader and its authors bare no responsibility to any possible loses from using a scanner, strategy, miner, or any other part of LiuAlgoTrader (on the other hand, they also won’t share your profits, if there are any).

The Basics

  • As with Scanners, each strategy should have it’s own unique name,

  • LiuAlgoTrader supports both long and short strategies,

  • LiuAlgoTrader support two strategy types:

    Name Description
    DAY_TRADING Held positions will be automatically liquidated at the end of the trading day.
    SWING No automatic liquidation.
  • The framework supports trading windows (=time frame for buying) per strategy out of the box, however they are not enforced.

  • A Strategy may act on a single symbol, or can act on a collection of symbols (e.g. SP500)

Configuration

Strategies are declared in the trade_plan Database table, or in the tradeplan.toml configuration file under the section:

[strategies]

tradeplan.toml

Strategies may run concurrently. The order of execution is based on the order in which strategies are presented in the tradeplan file.

Each strategy should have its own entry point in the tradeplan.toml configuration file. With filename pointing to the full path to the python file containing the strategy to be executed, and at least one trading window.

For example:

[strategies]
    [strategies.MyStrategy]
        filename = "/home/liu/strategies/my_strategy.py"

        [[strategies.MyStrategy.schedule]]
            start = 15
            duration = 150

Trading windows are not enforced by the framework, the Strategy developer may elect to ignore them.

Strategies are instantiated inside consumer processes, immediately proceeding the creation of the scanners and producer processes.

A Strategy may have any number of parameters, once instanticated, the Framework will pass all the parameters from the .toml file to the strategy. Exception will be raised if parameters are missing. Always make sure to called super() to ensure proper setting of basic parameters.

trade-plan table

See Concepts section from further details

Developing a Strategy

To inherit from the Strategy based class:

from liualgotrader.strategies.base import Strategy, StrategyType

Creating a strategy involves:

  1. Declaring a class object, inherited from the Strategy base class,
  2. Overwrite both the __init__() and run() functions. __init__() is called during initialization, and is passed the configuration parameters from the tradeplan.toml file.`run()` is called when the framework is ready to execute the scanner.

__init__() function

The __init__() function is called by the framework with a batch_id and configuration parameters declared in the configuration file. Declaring a trading window is a must, and the framework will throw an Exception if they are not present.

The function is expected to call the base-class __init__() function with a unique name, type (e.g. swing or day trading) and trading window. Name and type are normally static in the strategy object and not passed from the configuration file.

Please review the base class for furthe details.

run() and run_all() functions

run() is called per picked symbol, per event. That would normally mean at least once per second, per stock. run_al() is called once, every one minute, with list of all open positions and a DataLoader. The Platform decides which function to call, based on the value returned by should_run_all(). See base.py for further details.

run() Actions

The functions returns a tuple of a boolean and dictionary. The functions may return one of the below combinations:

return False, {}

–> No action will be taken by the framework,

return True, {
    "side": "buy",
    "qty": str(10),
    "type": "limit",
    "limit_price": "4.4",
}

–> Requesting the framework to execute a limit order for quantity 10 and limit price $4.4,

return True, {
     "side": "buy",
     "qty": str(5),
     "type": "market",
}

–> Purchase at market. Similarly for sell side,

return False, {"reject": True}

–> Mark the symbol as rejected by the strategy, ensuring run() will not be called again with the symbol during this trading session.

buy_callback()

Called by the framework post completion of a buy ask. Partial fills won’t trigger the callback, only the final complete will trigger. Partial trades will still be persisted on the database.

sell_callback()

Called by the framework post completion of the sell ask. Partial fills won’t trigger the callback, only the final complete will trigger.

Trading windows

Trading windows are list of time-frames during which a strategy may look for a signal to open positions. The framework does not enforce the windows, but rather provides useful functions on the base class that may be overwritten by the strategy developer.

is_buy_time()

This function, implemented by the Strategy base-class, looks at the configured schedule and will return True if current time is over start minutes from the market open and below start`+`duration time from market open.

is_sell_time()

Returns True if at least 15 minutes passed since the market open, AND the buy window elapsed.

It is up to the Strategy developer if to use these functions or not.

Data Persistence

LiuAlgoTrader framework stores successful operations. This data serves well the analysis tools provided by the framework.

Aside from an Notebooks and Streamlit applications for analysis, the framework automatically calculates gain & loss per per strategy, per buy-sell (or sell-buy) operations. Calculations are done in price change value, percentage and also r units. Once enough data is collected, the framework will feedback probability estimate, in real-time, for a strategy based on Machine-Learning models calculated by the framework.

For this purpose, the strategy developer is ask to fill 4 Global Variables:

from liualgotrader.common.trading_data import (
    buy_indicators,
    sell_indicators,
    stop_prices,
    target_prices,
)
  • stop_prices[symbol] = <selected stop price>
  • target_prices[symbol] = <selected target price>
  • buy_indicators[symbol] is dictionary persisted as JSON including the strategy indicators and reasoning for buying.
  • sell_indicators[symbol] is dictionary persisted as JSON including the strategy indicators and reasoning for selling.

The variables can be left out (some strategies, simply do not have stop prices) though its highly recommended to have them included.

Example

Putting it all together in a generic Strategy:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
from datetime import datetime
from typing import Dict, List, Tuple

import pandas as pd

from liualgotrader.common import config
from liualgotrader.common.data_loader import DataLoader  # type: ignore
from liualgotrader.common.tlog import tlog
#
# common.trading_data includes global variables, cross strategies that may be
# helpful
#
from liualgotrader.common.trading_data import (buy_indicators,
                                               last_used_strategy, open_orders,
                                               sell_indicators, stop_prices,
                                               target_prices)
from liualgotrader.strategies.base import Strategy, StrategyType

#
# TALIB is *NOT* available as part of LiuAlgoTrader distribution,
# though it's highly recommended.
# Dcumentation can be found here https://www.ta-lib.org/
#
# import talib
# from talib import BBANDS, MACD, RSI


class MyStrategy(Strategy):
    def __init__(
        self,
        batch_id: str,
        schedule: List[Dict],
        data_loader: DataLoader = None,
        fractional: bool = False,
        ref_run_id: int = None,
        my_arg1: int = 0,
        my_arg2: bool = False,
    ):
        super().__init__(
            name=type(self).__name__,
            type=StrategyType.SWING,
            batch_id=batch_id,
            ref_run_id=ref_run_id,
            schedule=[],
            data_loader=data_loader,
        )
        self.my_arg1 = my_arg1
        self.my_arg2 = my_arg2

    async def buy_callback(
        self,
        symbol: str,
        price: float,
        qty: float,
        now: datetime = None,
        trade_fee: float = 0.0,
    ) -> None:
        ...

    async def sell_callback(
        self,
        symbol: str,
        price: float,
        qty: float,
        now: datetime = None,
        trade_fee: float = 0.0,
    ) -> None:
        ...

    async def create(self) -> bool:
        """
        This function is called by the framework during the instantiation
        of the strategy. Keep in mind that running on multi-process environment
        it means that this function will be called at least once per spawned process.
        :return:
        """
        await super().create()
        tlog(f"strategy {self.name} created")
        return True

    async def should_run_all(self):
        return False

    async def run(
        self,
        symbol: str,
        shortable: bool,
        position: float,
        now: datetime,
        minute_history: pd.DataFrame,
        portfolio_value: float = None,
        debug: bool = False,
        backtesting: bool = False,
    ) -> Tuple[bool, Dict]:
        current_second_data = minute_history.iloc[-1]
        tlog(f"{symbol} position is {position}")
        tlog(f"{symbol} data: {current_second_data}")

        if await super().is_buy_time(now) and not position:
            #
            # Check for buy signals??
            #

            #
            # Global, cross strategies passed via the framework
            #
            target_prices[symbol] = current_second_data.close * 1.02
            stop_prices[symbol] = current_second_data.close * 0.99

            #
            # indicators *should* be filled
            #
            buy_indicators[symbol] = {"my_indicator": "random"}

            tlog(
                f"[{self.name}] Submitting buy 10 shares of {symbol} at {current_second_data.close}"
            )
            return (
                True,
                {
                    "side": "buy",
                    "qty": str(10),
                    "type": "limit",
                    "limit_price": current_second_data.close,
                },
            )
        elif (
            await super().is_sell_time(now)
            and position > 0
            and last_used_strategy[symbol].name == self.name  # important!
            and (
                current_second_data.close >= target_prices[symbol]
                or current_second_data.close <= stop_prices[symbol]
            )
        ):
            # check if we already have open order
            if open_orders.get(symbol) is not None:
                tlog(f"{self.name}: open order for {symbol} exists, skipping")
                return False, {}

            # Check for liquidation signals
            sell_indicators[symbol] = {
                "my_indicator": "reached target"
                if current_second_data.close >= target_prices[symbol]
                else "reached stop"
            }

            tlog(
                f"[{self.name}] Submitting sell for {position} shares of {symbol} at market w/ {sell_indicators}"
            )
            return (
                True,
                {
                    "side": "sell",
                    "qty": str(position),
                    "type": "market",
                },
            )

        return False, {}

Sample tradeplan.toml configuration file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# This is a TOML configuration file.

# if set to true, allow running outside market open hours
# bypass_market_schedule = true

# set max portfolio_value
# portfolio_value = 25000.0

# set risk factor per trade
# risk = 0.001

# how many minutes, before end of the trading day to enforce liqudation
# market_liquidation_end_time_minutes = 15

# ticket scanners, may have several
# scanners during the day
[scanners]
[scanners.momentum]
# scan for tickers with minimal volume since day start
min_volume = 30000

# minimum daily percentage gap
min_gap = 3.5

# minimum last day dollar volume
min_last_dv = 500000

max_share_price = 20.0
min_share_price = 2.0

# How many minutes from market open, to start running scanner
from_market_open = 15

# recurrence = 5

# max_symbols = 440

# target_strategy_name = "MyStrategy"

# trading strategies, can have several *strategy* blocks
[strategies]
# strategy class name, must implement Strategy class
[strategies.MyStrategy]
filename = "examples/my_strategy.py"

# trading schedules block, trades many have
# several windows within the same day
[[strategies.MyStrategy.schedule]]
duration = 150
start = 15

Behind the scenes

Actually a lot is happening behind the scenes! upon setting up the scanner and producer processes, consumer processes are spawned. The number of processes is either pre-determined by an environment variable, or estimated based on the OS & hardware capabilities.

When the consumer processes are spawned, the existing profile in Alpaca is examined to try and figure if the trader app crashed and strategies should resume. Once existing positions are loaded, they are matched with instantiated strategies, events of picked stocks start to pour and the run() functions are called.

Once a strategy decides to purchase a symbol, the framework will place the buy (or sell) order with Alpaca, start follow it and persist the transactions to the new_trades table. Both partial and complete orders are stored, and tracked.

If an update for a strategy operation is not received within a pre-configured windows of time (default is 60 seconds), the framework will check if the order was completed w/o a notification, and if so, will simulate a complete event. However if the operation did not conclude, it will be cancelled, and open order will be cleared, and the strategy may re-trigger an operating based on signals.

The framework works behind the scenes to free the strategy developer from worrying about broker (Alpaca.Markets) and data provider (Polygon.io) integrations.

Global Vars

While each strategy may keep track of it’s own variables, the framework is tracking global variables. The variables can be access by importing liualgotrader.common.trading_data module.

Parameters include:

Parameter Description
open_orders dictionary, with symbol as index and holdin a tuple Order object w/ “buy” or “sell” side.
open_order_strategy dictionary, with symbol as index and holding reference to the Strategy w/ an open order.
buy_time dictionary, with symbol as index and datetime object of order request time in EST.
minute_history dictionary, with symbol as index and DataFrame with OHLC, Volume, vwap and average, per minute.
positions dictionary of open positions.
last_used_strategy dictionary w/ reference to last strategy with completed order, per symbol.

IMPORTANT NOTE:

The global variables are there for a purpose: suppose you run two strategies in parallel, both run() functions would be hit with symbol events. Let’s assume you hold a position in the symbol. One strategy may identify a sell signal, while the other does not. Depending on the strategy combination and architecture, it might be wise for a strategy to confirm it was actually the one initiating the original buy by consulting last_used_strategy[symbol]

Key-store

The platform implement a key-value store, per strategy (see base.py for implementation details). key-value allows for strategies to persist state across executions. This feature is specifically helpful for swing strategies that run across different batches.

Additional Configurations

LiuAlgoTrader framework has quite a few knobs & levers to configure the behaviour of the platform and the strategies.

The best place to check out all possibilities is here.

Some of the configurations are documented in the tradeplan.toml file, some are in environment variables, and some may be over-written by the strategy developer.