Full Usage Examples

About 4 min

Momentum Strategy

Disclaimer:This strategy is only for demonstrative use. We DID NOT test this strategy under real market condition. Please DO NOT implement this strategy for your own investment

The idea of this strategy is that we assume that "an asset with . In this strategy, we select stocks from NASDAQ 100 Index

To implement this idea, run the strategy periodically. Each time, the program will select stocks with top price increment, buy these stocks, and sell those are not in the list.

Here is the implementation of the strategy:

import datetime
import logging
import sys
import time

import pandas as pd

from tigeropen.common.consts import BarPeriod, SecurityType, Market, Currency
from tigeropen.common.util.contract_utils import stock_contract
from tigeropen.common.util.order_utils import limit_order
from tigeropen.quote.quote_client import QuoteClient
from tigeropen.tiger_open_config import get_client_config
from tigeropen.trade.trade_client import TradeClient

client_logger = logging.getLogger('client')
client_logger.setLevel(logging.WARNING)
client_logger.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))

pd.set_option('display.max_columns', 500)
pd.set_option('display.max_rows', 100)
pd.set_option('display.width', 1000)

# Components of Nasdaq-100. Up to 2021/12/20
UNIVERSE_NDX = ["AAPL", "ADBE", "ADI", "ADP", "ADSK", "AEP", "ALGN", "AMAT", "AMD", "AMGN", "AMZN", "ANSS", "ASML",
                "ATVI", "AVGO", "BIDU", "BIIB", "BKNG", "CDNS", "CDW", "CERN", "CHKP", "CHTR", "CMCSA", "COST", "CPRT",
                "CRWD", "CSCO", "CSX", "CTAS", "CTSH", "DLTR", "DOCU", "DXCM", "EA", "EBAY", "EXC", "FAST", "FB",
                "FISV", "FOX", "GILD", "GOOG", "HON", "IDXX", "ILMN", "INCY", "INTC", "INTU", "ISRG",
                "JD", "KDP", "KHC", "KLAC", "LRCX", "LULU", "MAR", "MCHP", "MDLZ", "MELI", "MNST", "MRNA", "MRVL",
                "MSFT", "MTCH", "MU", "NFLX", "NTES", "NVDA", "NXPI", "OKTA", "ORLY", "PAYX", "PCAR", "PDD", "PEP",
                "PTON", "PYPL", "QCOM", "REGN", "ROST", "SBUX", "SGEN", "SIRI", "SNPS", "SPLK", "SWKS", "TCOM", "TEAM",
                "TMUS", "TSLA", "TXN", "VRSK", "VRSN", "VRTX", "WBA", "WDAY", "XEL", "XLNX", "ZM"]

# Components of HSTECH. Up to 2021/12/20
UNIVERSE_HSTECH = ["00241", "00268", "00285", "00522", "00700", "00772", "00780", "00909", "00981", "00992", "01024",
                   "01347", "01810", "01833", "02013", "02018", "02382", "02518", "03690", "03888", "06060", "06618",
                   "06690", "09618", "09626", "09698", "09888", "09961", "09988", "09999"]

# number of position stocks
HOLDING_NUM = 5
# number of order status check
ORDERS_CHECK_MAX_TIMES = 10
# number of symbols per request
REQUEST_SIZE = 50
TARGET_QUANTITY = "target_quantity"
PRE_CLOSE = "pre_close"
LATEST_PRICE = "latest_price"
MARKET_CAPITAL = "market_capital"
SYMBOL = "symbol"
WEIGHT = "weight"
TIME = "time"
CLOSE = "close"
DATE = "date"
LOT_SIZE = "lot_size"

PRIVATE_KEY_PATH = "your private key path"
TIGER_ID = "your tiger id"
ACCOUNT = "your account"
client_config = get_client_config(private_key_path=PRIVATE_KEY_PATH, tiger_id=TIGER_ID, account=ACCOUNT)
quote_client = QuoteClient(client_config, logger=client_logger)
trade_client = TradeClient(client_config, logger=client_logger)


def request(symbols, method, **kwargs):
    """
    :param symbols:
    :param method:
    :param kwargs:
    :return:
    """
    symbols = list(symbols)
    result = pd.DataFrame()
    for i in range(0, len(symbols), REQUEST_SIZE):
        part = symbols[i:i + REQUEST_SIZE]
        quote = method(part, **kwargs)
        result = result.append(quote)
        # for rate limit
        time.sleep(0.5)
    return result


def get_quote(symbols):
    quote = request(symbols, quote_client.get_stock_briefs)
    return quote.set_index(SYMBOL)


def get_trade_meta(symbols):
    metas = request(symbols, quote_client.get_trade_metas)
    return metas.set_index(SYMBOL)


def get_history(symbols, total=200, batch_size=50) -> pd.DataFrame:
    """

    :param symbols:
    :param total:
    :param batch_size:
    :return:
                                               time      open     high       low   close     volume
    date                      symbol
    2021-03-05 00:00:00-05:00 AAPL    1614920400000  120.9800  121.935  117.5700  121.42  153766601
                              ADBE    1614920400000  444.8800  444.950  423.7101  440.83    4614971
                              ADI     1614920400000  149.0000  149.620  143.3900  148.88    4040153
                              ADP     1614920400000  171.8300  179.000  171.5003  178.26    2535893
                              ADSK    1614920400000  270.3300  270.330  255.0200  267.39    1835526
    ...                                         ...       ...      ...       ...     ...        ...
    2021-12-16 00:00:00-05:00 WBA     1639630800000   48.5035   50.150   48.5000   49.26    5551852
                              WDAY    1639630800000  277.3300  278.365  269.2600  272.23    1206784
                              XEL     1639630800000   68.5800   69.570   68.3100   68.95    3774564
                              XLNX    1639630800000  217.3700  218.080  198.5100  199.78    4299386
                              ZM      1639630800000  183.7900  185.720  177.0000  182.40    4224447
    """
    end = int(datetime.datetime.today().timestamp() * 1000)
    history = pd.DataFrame()
    for i in range(0, total, batch_size):
        if i + batch_size <= total:
            limit = batch_size
        else:
            limit = i + batch_size - total
        logger.info(f'query history, end_time:{end}, limit:{limit}')
        part = request(symbols, quote_client.get_bars, period=BarPeriod.DAY, end_time=end, limit=limit)
        part[DATE] = pd.to_datetime(part[TIME], unit='ms').dt.tz_localize('UTC').dt.tz_convert('US/Eastern')
        end = min(part[TIME])
        history = history.append(part)
    history.set_index([DATE, SYMBOL], inplace=True)
    history.sort_index(inplace=True)
    return history


class Strategy:

    def __init__(self):
        self.market = Market.US
        self.currency = Currency.USD
        self.universe = UNIVERSE_NDX
        # self.market = Market.HK
        # self.currency = Currency.HKD
        # self.universe = UNIVERSE_HSTECH

        self.selected_symbols = list()
        self.momentum_period = 30
        self.holding_num = HOLDING_NUM
        # the higher the ratio, the safer the risk status, if below 5%, will be forced closing of positions
        self.target_overnight_liquidation_ratio = 0.6

    def screen_stocks(self):
        """screen stock by price momentum
        :return:
        """
        history = get_history(self.universe)
        close_data = history[CLOSE].unstack()
        momentum = close_data.pct_change(periods=self.momentum_period).iloc[-1]
        self.selected_symbols = momentum.nlargest(self.holding_num).index.values.tolist()
        return self.selected_symbols

    def rebalance_portfolio(self):
        """
        rebalance. close not chosen stocks, open chosen stocks by weight
        :return: 
        """
        position_list = trade_client.get_positions(sec_type=SecurityType.STK, market=self.market)
        positions = dict()
        for pos in position_list:
            positions[pos.contract.symbol] = pos.quantity

        need_close_symbols = set(positions.keys()) - set(self.selected_symbols)

        # if not US market, need to get the lot size, only integer multiples are allowed
        lot_size = get_trade_meta(set(positions.keys()).union(self.selected_symbols))[LOT_SIZE]
        latest_price = get_quote(need_close_symbols)[LATEST_PRICE]
        orders = list()
        for symbol in need_close_symbols:
            contract = stock_contract(symbol, currency=self.currency.name)
            # Process the number of shares ordered as an integer multiple of the number of shares per lot
            quantity = int(positions[symbol] // lot_size[symbol] * lot_size[symbol])
            if quantity == 0:
                logger.warning(f'can not place order with this quantity, symbol:{symbol}, lot_size:{lot_size[symbol]},'
                               f'quantity:{positions[symbol]}')
                continue
            limit_price = latest_price[symbol]
            order = limit_order(account=ACCOUNT,
                                contract=contract,
                                action='SELL' if quantity > 0 else 'BUY',
                                quantity=abs(quantity),
                                limit_price=limit_price)
            orders.append(order)
        self.execute_orders(orders)

        # global account
        # asset = trade_client.get_assets(account=ACCOUNT, segment=True)[0].segments['S']
        # target_overnight_liquidation = asset.equity_with_loan * self.target_overnight_liquidation_ratio
        # adjust_value = asset.sma - target_overnight_liquidation

        # prime/paper account
        asset = trade_client.get_prime_assets(account=ACCOUNT).segments['S']
        # Target overnight residual liquidity after position adjust (overnight_liquidation = total equity with loan value equity_with_loan - overnight margin overnight_margin)
        # Residual overnight liquidity ratio = Residual overnight liquidity overnight_liquidation / Total equity with loan value equity_with_loan
        target_overnight_liquidation = asset.equity_with_loan * self.target_overnight_liquidation_ratio
        # If liquidity is sufficient, the amount to be bought
        adjust_value = asset.overnight_liquidation - target_overnight_liquidation
        if adjust_value <= 0:
            logger.info('no enough liquidation')
            return
        quote = get_quote(self.selected_symbols)
        # Position weighted by number of shares held
        quote[WEIGHT] = 1 / len(self.selected_symbols)
        quote[TARGET_QUANTITY] = (adjust_value * quote[WEIGHT] / quote[LATEST_PRICE]).astype(int)

        orders = list()
        for symbol in quote.index:
            contract = stock_contract(symbol, self.currency.name)
            quantity = int(quote[TARGET_QUANTITY][symbol] // lot_size[symbol] * lot_size[symbol])
            # This check is mainly for non-US stocks. If the number of shares currently ordered is not an integer multiple of log_size, 
            # the order cannot be placed and can only be sold in pieces through the app
            if quantity == 0:
                logger.warning(f'can not place order with this quantity, symbol:{symbol}, lot_size:{lot_size[symbol]},'
                               f'quantity:{quote[TARGET_QUANTITY][symbol]}')
                continue
            order = limit_order(account=ACCOUNT,
                                contract=contract,
                                action='BUY',
                                quantity=quantity,
                                limit_price=quote[LATEST_PRICE][symbol])
            order.time_in_force = 'GTC'  # 'DAY' / 'GTC' Good-Till-Cancel
            orders.append(order)
        self.execute_orders(orders)

    def execute_orders(self, orders):
        local_orders = dict()
        for order in orders:
            try:
                trade_client.place_order(order)
                logger.info(f'place order, {order.action} {order.contract.symbol} {order.quantity} {order.limit_price}')
                local_orders[order.id] = order
            except Exception as e:
                logger.error(f'place order error:{order}')
                logger.error(e, exc_info=True)

        time.sleep(20)
        i = 0
        while i <= ORDERS_CHECK_MAX_TIMES:
            logger.info(f'check {i} times')
            history_open_orders = trade_client.get_open_orders(account=ACCOUNT, sec_type=SecurityType.STK,
                                                               market=self.market,
                                                               start_time=self.get_time_from_now(
                                                                   datetime.timedelta(days=1)),
                                                               end_time=self.get_time_from_now())
            if not history_open_orders:
                break

            # Modify the order if a number of attempts has been reached. Change the limit price to the lastest price.
            if i == ORDERS_CHECK_MAX_TIMES // 2:
                for open_order in history_open_orders:
                    latest_price = get_quote([open_order.contract.symbol])[LATEST_PRICE][open_order.contract.symbol]
                    try:
                        trade_client.modify_order(open_order, limit_price=latest_price)
                        logger.info(f'modify order, id:{open_order.id}, symbol:{open_order.contract.symbol},'
                                    f' old_price:{open_order.limit_price}, new_price:{latest_price}')
                    except Exception as e:
                        logger.error(f'modify order error:{open_order.id}')
                        logger.error(e)
            # Cancel the order if reached the maximum number of attempts
            if i >= ORDERS_CHECK_MAX_TIMES:
                for order in history_open_orders:
                    logger.info(f'the order was not filled, now cancel it: {order}')
                    try:
                        trade_client.cancel_order(ACCOUNT, id=order.id)
                    except Exception as e:
                        logger.error(f'cancel order error: {order}')
                        logger.error(e, exc_info=True)
            i += 1
            time.sleep(10)

        # Print filled orders
        filled_orders = trade_client.get_filled_orders(account=ACCOUNT,
                                                       sec_type=SecurityType.STK,
                                                       market=self.market,
                                                       start_time=self.get_time_from_now(datetime.timedelta(days=1)),
                                                       end_time=self.get_time_from_now())
        order_infos = [(str(order.id) + ':' + order.contract.symbol + ':' + order.action + ':' + str(order.filled)
                        + ':' + str(order.avg_fill_price)) for order in filled_orders]
        logger.info(f'recently filled orders:{order_infos}')

        # Print orders that has not yet been filled
        unfilled_order_ids = set(local_orders.keys()) - set(order.id for order in filled_orders)
        for order_id in unfilled_order_ids:
            order = trade_client.get_order(ACCOUNT, id=order_id)
            logger.info(f'order was cancelled, id:{order.id}, status:{order.status}, reason:{order.reason}')

    @staticmethod
    def get_time_from_now(delta=None):
        if not delta:
            return int(datetime.datetime.now().timestamp()) * 1000
        return int((datetime.datetime.now() - delta).timestamp()) * 1000

    def run(self):
        perms = quote_client.grab_quote_permission()
        logger.info(perms)
        self.screen_stocks()
        self.rebalance_portfolio()


if __name__ == '__main__':
    strategy = Strategy()
    strategy.run()

Last update: