SmartTrade is an algorithmic trading platform which allows programmers to easily implement and back-test trading strategies using Python. As SmartTrade provides access to multiple essential libraries such as statsmodel and ta-lib, you can use technical analysis or statistical methods for your strategies. Below we will illustrate an example using 2 common technical indicators: MACD (Moving Average Convergence Divergence) and Stochastics.
First, we login to SmartTrade’s coding interface, and we will be presented with 2 panels: 1) Python coding interface and 2) Backtesting results
We will begin the development process. There are 4 key steps in building a strategy on SmartTrade
- Import & Initialize Method
Import the libraries desired (such as ta-lib) at the top.
import pandas as pd
import talib as ta
import numpy as np
It is recommended to import pandas library at a minimum in order to operate on the timeseries variables which come as DataFrame objects on SmartTrade.
Next specify the list of stocks you would like to backtest in the initialize method. Currently, SmartTrade supports backtesting up to 50 stocks at a time.
def initialize(ctx):
# 設定
ctx.logger.debug("initialize() called")
ctx.configure(
channels={ # 利用チャンネル
"jp.stock": {
"symbols": [
"jp.stock.9984",
"jp.stock.9983",
"jp.stock.7201",
"jp.stock.8377",
"jp.stock.5715",
"jp.stock.7014",
"jp.stock.9810",
"jp.stock.6861",
"jp.stock.2782",
],
"columns": [
"open_price_adj", # 始値(株式分割調整後)
"high_price_adj", # 高値(株式分割調整後)
"low_price_adj", # 安値(株式分割調整後)
"volume_adj", # 出来高
"txn_volume", # 売買代金
"close_price", # 終値
"close_price_adj", # 終値(株式分割調整後)
]
}
}
)
- Preparing the Required Data
Given the selected stocks, time period, the timeseries will be provided in the signal method via the data object. Specifically, you can access the close, open, high, low and volume timeseries for each stock by directly accessing the data object as a dictionary like the follow:
open2 = data["open_price_adj"].fillna(method='pad')
high = data["high_price_adj"].fillna(method='pad')
low = data["low_price_adj"].fillna(method='pad')
close = data["close_price_adj"].fillna(method='pad')
Next, we will apply ta-lib to the above timeseries. Before we do that, we define additional objects to build the signals based on the results from ta-lib. We define the below objects with same DataFrame structures as data:
s1 = pd.DataFrame(data=0, columns=close.columns, index=close.index)
s2 = pd.DataFrame(data=0, columns=close.columns, index=close.index)
sto = pd.DataFrame(data=0, columns=close.columns, index=close.index)
sto2 = pd.DataFrame(data=0, columns=close.columns, index=close.index)
s3 = pd.DataFrame(data=0, columns=close.columns, index=close.index)
result = pd.DataFrame(data=0, columns=close.columns, index=close.index)
After that, we will begin applying ta-lib to the timeseries. To do that, we need to loop through the close objects by symbol as follow. We will use MACD and Stochastics functions as described above:
for (sym, val) in close.items():
macd_talib, signal, s1[sym] = ta.MACD(val.values.astype(np.double),
fastperiod=12,
slowperiod=26,
signalperiod=9)
stok, stod = ta.STOCH(high[sym].values.astype(np.double),
low[sym].values.astype(np.double),
val.values.astype(np.double),
slowk_period=14,
slowd_period=3)
s2[sym] = stok - stod
sto[sym] = stok
sto2[sym] = stod
It is important to note that he close, open, high and low timeseries must be converted to array as double first before they can be used in ta-lib.
- Constructing the Signal Logic
Next we will go through the logic of the strategy. The “MACD & Stochastic Momentum” signal is a strategy that buys and sells based on change in momentum. In other words, it buys when momentum turns positive and sells when it turns negative. The MACD and Stochastic indicators allow us to detect changes in momentum.
MACD
The MACD keeps track of 2 exponential moving averages (usually 12 and 26 period), and detects when the shorter moving average “crosses-over” the longer one. When a cross-over occurs, the MACD histogram crosses the zero line. The idea is that we locate the time when the histogram crosses from negative to positive, which indicates positive momentum, and vice-versa on the negative side. Below is an example illustrating buy signal using the MACD:
We use the below code to keep track on when the cross-over occurs:
test = np.sign(s1[sym].values[1:]) - np.sign(s1[sym].values[:-1])
n = len(test)
for i, row in enumerate(test):
result[sym][i+1] = 0 if isNaN(test[i]) else test[i]
Stochastic
There are 2 sub-indicators within Stochastic – the K line and D line. The trend is said to shift to positive when the K line “crosses-over” the D line. In other words, when K Line > D Line. Here is an example:
Therefore, in our Python code, we keep track of stok – stod timeseries in the s2 object and detect when the s2 changes from negative to positive in order to find the time when K line crosses over the D line positively using the following code:
test = np.sign(s2[sym].values[1:]) - np.sign(s2[sym].values[:-1])
n = len(test)
for i, row in enumerate(test):
s3[sym][i+1] = 0 if isNaN(test[i]) else test[i]
In brief, when s3 equals +1, the crossover is positive, and -1 when the crossover is negative otherwise.
Finally once we constructed the above 2 signals, we provide SmartTrade with when to buy and signal using the below code:
buy_sig = result[(result>=2) & (s2>0) & ((sto>=40) | (sto2>=40))]
sell_sig = result[(result<=-2) | ((s3<0) & ((sto<=40) | (sto2<=40)))]
return {
"buy:sig": buy_sig,
"sell:sig": sell_sig
}
- Handle the Signals & Positions
After creating the signal, we need to actually execute the trades. This is done in the last method called handle_signals.
Inside handle_signals, there are 2 major functions:
- Execute buy and sell signals
This is simply done by looping through the buy:sig and sell:sig objects returned from the above.
buy = current["buy:sig"].dropna()
for (sym, val) in buy.items():
if sym in done_syms:
continue
sec = ctx.getSecurity(sym)
sec.order(sec.unit() * 1, comment="SIGNAL BUY")
sell = current["sell:sig"].dropna()
for (sym, val) in sell.items():
if sym in done_syms:
continue
sec = ctx.getSecurity(sym)
sec.order(sec.unit() * -1, comment="SIGNAL SELL")
- Execute stop-losses
This is a custom logic to cut losses or take profit. SmartTrade provides a position object for the program to check the current return and maximum return to date. Based on that, we create a “trailing-stop” logic as follow:
for (sym, val) in ctx.portfolio.positions.items():
returns = val["returns"]
trailing = val["max_returns"] - returns
if (trailing > 0.02 and returns < 0) or (returns > 0 and trailing > 0.04) or (returns > 0.05 and trailing > 0.02) or (returns > 0.1 and trailing > 0.01):
sec = ctx.getSecurity(sym)
sec.order(-val["amount"], comment="Trailing Stop(%f)" % returns)
done_syms.add(sym)
In brief, the logic cuts losses if greater than 2%, and takes profit depending on how much the trailing return is. The idea is to maximize profit and ride the up trend as long as the trailing stop is not hit.
And we are done with the coding! Next we execute the strategy and below are the backtesting results since 2007:
As shown above, the strategy is able to navigate the bear market with relatively mild losses while generating a positive return bigger than the Nikkei 225. When applied on more stocks, the result would be even more profound.
Since the strategy is public on SmartTrade, feel free to check out the full code and run the strategy yourself below: