完整使用示例

大约 6 分钟

动量策略

注意:本策略仅为使用接口提供示例用法,比如查询行情,下单等,策略本身未经验证,不构成投资建议。

该策略基本思路为"过去一段时间收益较高的资产,在未来仍将延续原有趋势,可能会获得较高的收益",选股所用股票池为纳斯达克100指数成份股。

具体实现过程为:定期运行策略,每次选取股票池中周期内涨幅最高的若干股票,作为本次调仓的目标股票买入持有,对先前持仓中未入选的股票进行平仓。

代码如下:

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)

# 纳斯达克100指数成分股 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"]

# 恒生科技指数成分股. 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"]

# 持仓股票个数
HOLDING_NUM = 5
# 订单检查次数
ORDERS_CHECK_MAX_TIMES = 10
# 获取行情每次请求symbol个数
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
        # 调仓后隔夜剩余流动性目标占比,剩余流动性占比越高,风控状态越安全,如果隔夜剩余流动性占比过低(比如小于5%), 则存在被强平的风险。
        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):
        """
        调仓。先将本次选股未选中但在持仓中的股票进行平仓,然后将选中的股票按照等股数权重买入
        :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)

        # 如果不是美股,需要获取股票的每手股数,每次下单的股数只能使用每手股数的整数倍
        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)
            # 将下单股数处理为每手股数的整数倍
            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']
        # 调仓后目标隔夜剩余流动性(隔夜剩余流动性 overnight_liquidation = 含贷款价值总权益 equity_with_loan - 隔夜保证金 overnight_margin)
        # 隔夜剩余流动性比例 = 隔夜剩余流动性 overnight_liquidation / 含贷款价值总权益 equity_with_loan
        target_overnight_liquidation = asset.equity_with_loan * self.target_overnight_liquidation_ratio
        # 如果流动性充足,需要买入的金额
        adjust_value = asset.overnight_liquidation - target_overnight_liquidation
        if adjust_value <= 0:
            logger.info('no enough liquidation')
            return
        quote = get_quote(self.selected_symbols)
        # 按持股数量等权重持仓,equal weight
        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])
            # 该检查主要针对非美股。如果目前下单股数不是log_size的整数倍,则不能下单,只能通过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' 撤销前有效
            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

            # 检查一定次数后如果还未成交, 进行一次改单, 修改限价为最新价格
            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)
            # 如果达到最大检查次数还未成交,则进行撤单
            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)

        # 打印已成交订单信息
        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}')

        # 打印未成交订单信息
        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()

上次编辑于: