Algorithmic trading system for algo trading on FutuNiuNiu broker (project is not finished)
How it works- Maintain Connection to Futu OpenD, stream and broadcast real-time data
- Store streamed data to MySQL databases
- API for changing subscribed tickers & data types, get historical data and more
- API for placing orders, modifying orders, cancelling orders
- Install and run FutuOpenD: https://www.futunn.com/download/openAPI?lang=en-US
- Install and run MySQL database: https://www.mysql.com/downloads/
- Set Up system environment variables for the following:
- SANIC_HOST : host for sanic app (e.g. 0.0.0.0)
- SANIC_PORT: port for sanic app (e.g. 8000)
- FUTU_TRADE_PWD: the trade unlock password for your Futu account
- FUTU_HOST: host of your running Futu OpenD
- FUTU_PORT: port of your running Futu OpenD
- ZMQ_PORT: port for ZMQ
- MYSQL_DB: name of the database that you wish to store your data
- MYSQL_HOST: host of your running MySQL service
- MYSQL_USER: user of your running MySQL service
- MYSQL_PWD: password of your user
import FutuAlgo
INIT_DATATYPE = ['K_3M', 'K_5M', 'QUOTE']
INIT_TICKERS = ['HK.00700', 'HK_FUTURE.999010']
futu_hook = FutuAlgo.FutuHook()
futu_hook.subscribe(datatypes=INIT_DATATYPE, tickers=INIT_TICKERS)
futu_hook.run(fill_db=True)
Functions:
- Subscribe to FutuHook and receive price updates
- Trigger events on receiving different data types
- Place orders, modify orders, cancel orders
- APIs for retrieving strategy infos(returns, params, positions, pending orders ...)
import FutuAlgo
class SMACrossover(FutuAlgo.CandlestickStrategy):
def __init__(self, short, long):
super().__init__(name='SMA Crossover ({}, {})'.format(short, long), bars_no=long + 1)
self.short = short
self.long = long
async def on_bar(self, datatype, ticker, df):
df['SMA_short'] = talib.SMA(df['close'], timeperiod=self.short)
df['SMA_long'] = talib.SMA(df['close'], timeperiod=self.long)
sma_short_last = df['SMA_short'].iloc[-2]
sma_short_cur = df['SMA_short'].iloc[-1]
sma_long_last = df['SMA_long'].iloc[-2]
sma_long_cur = df['SMA_long'].iloc[-1]
if (sma_short_last <= sma_long_last) and (sma_short_cur > sma_long_cur) and (self.get_qty(ticker) == 0):
self.buy_limit(ticker=ticker, quantity=self.cal_max_buy_qty(ticker),
price=self.get_price(ticker=ticker))
elif (sma_short_last >= sma_long_last) and (sma_short_cur < sma_long_cur) and (self.get_qty(ticker) > 0):
self.sell_limit(ticker=ticker, quantity=self.get_qty(ticker),
price=self.get_price(ticker=ticker))
async def on_order_update(self, order_id, df):
pass
async def on_orderbook(self, ticker, df):
pass
async def on_other_data(self, datatype, ticker, df):
pass
async def on_quote(self, ticker, df):
pass
async def on_tick(self, ticker, df):
pass
algo = SMACrossover(short=10, long=20)
algo.initialize(initial_capital=100000.0, margin=100000.0, mq_ip='tcp://127.0.0.1:8001',
hook_ip='http://127.0.0.1:8000',
hook_name='FUTU', trading_environment='SIMULATE',
trading_universe=['HK.00700', 'HK.01299'], datatypes=['K_3M'])
algo.run(5000)
Quick back-test module for research purpose: QuickBacktest
import QuickBacktest
class SMA(QuickBacktest.Strategy):
def init(self):
self.data['sma16'] = self.data['adjclose'].rolling(16).mean()
self.data['sma32'] = self.data['adjclose'].rolling(32).mean()
self.data['dif'] = self.data['sma16'] - self.data['sma32']
self.data['pre_dif'] = self.data['dif'].shift(1)
def signal(self):
if self.dif > 0 and self.pre_dif <= 0:
self.long()
elif self.dif < 0 and self.pre_dif >= 0:
self.exit_long()
else:
pass
tickers = ('FB', 'AMZN', 'AAPL', 'GOOG', 'NFLX', 'MDB', 'NET', 'TEAM', 'CRM')
sma = SMA()
result = sma.backtest(tickers=tickers,
capital=1000000,
start_date="2015-01-01",
end_date="2020-07-31",
buy_at_open=True,
bid_ask_spread=0.0,
fee_mode='FIXED:0',
data_params={'interval': '1d', 'range': '10y'})
result.portfolio_report(benchmark="^IXIC", allocations=[0.25,0.25,0.25,0.25])
result.ticker_report('FB', benchmark='^IXIC')
result.ticker_plot('FB')
Use Algo class to back-test Strategies. This is useful when you have strategies that uses multiple data types and data source.
You need to fill your MySQL database with historical data first, as it retrieve data from your MySQL database through FutuHook. (This is to avoid wasting download quota of your Futu account)
You can do so by using FutuHook api: /db/fill.
import FutuAlgo
class SMACrossover(FutuAlgo.Backtest):
def __init__(self, short, long):
super().__init__(name='SMA Crossover ({}, {})'.format(short, long), bars_no=long+1)
self.short = short
self.long = long
async def on_bar(self, datatype, ticker, df):
df['SMA_short'] = talib.SMA(df['close'], timeperiod=self.short)
df['SMA_long'] = talib.SMA(df['close'], timeperiod=self.long)
sma_short_last = df['SMA_short'].iloc[-2]
sma_short_cur = df['SMA_short'].iloc[-1]
sma_long_last = df['SMA_long'].iloc[-2]
sma_long_cur = df['SMA_long'].iloc[-1]
if (sma_short_last <= sma_long_last) and (sma_short_cur > sma_long_cur) and (self.get_qty(ticker) == 0):
self.buy_limit(ticker=ticker, quantity=self.cal_max_buy_qty(ticker),
price=self.get_price(ticker=ticker))
elif (sma_short_last >= sma_long_last) and (sma_short_cur < sma_long_cur) and (self.get_qty(ticker) > 0):
self.sell_limit(ticker=ticker, quantity=self.get_qty(ticker),
price=self.get_price(ticker=ticker))
async def on_order_update(self, order_id, df):
pass
async def on_orderbook(self, ticker, df):
pass
async def on_other_data(self, datatype, ticker, df):
pass
async def on_quote(self, ticker, df):
pass
async def on_tick(self, ticker, df):
pass
if __name__ == '__main__':
algo = SMACrossover(short=16,long=32)
algo.initialize(initial_capital=200000.0, margin=200000.0, mq_ip='tcp://127.0.0.1:8001',
hook_ip='http://127.0.0.1:8000',
hook_name='FUTU', trading_environment='BACKTEST',
trading_universe=['HK.00700', 'HK.54544554','HK.00388'], datatypes=['K_DAY'], spread=0)
algo.backtest(start_date = '2020-04-01', end_date = '2020-05-01')
Generated by Quantstats.
# Use tencent 0700 as benchmark. This will open a webbrowser showing the full report.
algo.report(benchmark='0700.HK')
# Plot exit-entry point of this ticker
algo.plot_ticker_trades('K_DAY', 'HK.00700')
Functions:
- Sanic webapp that retrieves strategies' performances and positions
- Render and serve dashboard webpages
- Pause, Resume, Tune your Algos
import FutuAlgo
if __name__ == '__main__':
app = FutuAlgo.WebApp()
app.run(port=8522, hook_ip='http://127.0.0.1:8000')
- Cash is deducted after order is filled. If you place a new order before previous one is filled, you might end up with negative cash balance.