Strategies¶
Developing a Strategy is core for using LiuAlgoTrader framework.
Assumptions¶
This section assumes the audience has:
- Carefully read and understood the Concepts section,
- 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:
- Declaring a class object, inherited from the Strategy base class,
- 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.