Full Usage Examples

About 5 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 increasement, buy these stocks, and sell those are not in the list.

Here is the implementation of the strategy:

package com.tigerbrokers.stock.openapi.demo.example.nasdaq100;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.tigerbrokers.stock.openapi.client.config.ClientConfig;
import com.tigerbrokers.stock.openapi.client.struct.enums.MethodName;
import com.tigerbrokers.stock.openapi.client.https.client.TigerHttpClient;
import com.tigerbrokers.stock.openapi.client.https.domain.contract.item.ContractItem;
import com.tigerbrokers.stock.openapi.client.https.domain.quote.item.KlineItem;
import com.tigerbrokers.stock.openapi.client.https.domain.quote.item.KlinePoint;
import com.tigerbrokers.stock.openapi.client.https.domain.trade.item.PrimeAssetItem;
import com.tigerbrokers.stock.openapi.client.https.request.TigerHttpRequest;
import com.tigerbrokers.stock.openapi.client.https.request.quote.QuoteKlineRequest;
import com.tigerbrokers.stock.openapi.client.https.request.quote.QuoteRealTimeQuoteRequest;
import com.tigerbrokers.stock.openapi.client.https.request.trade.PrimeAssetRequest;
import com.tigerbrokers.stock.openapi.client.https.request.trade.TradeOrderRequest;
import com.tigerbrokers.stock.openapi.client.https.response.TigerHttpResponse;
import com.tigerbrokers.stock.openapi.client.https.response.quote.QuoteKlineResponse;
import com.tigerbrokers.stock.openapi.client.https.response.quote.QuoteRealTimeQuoteResponse;
import com.tigerbrokers.stock.openapi.client.https.response.trade.PrimeAssetResponse;
import com.tigerbrokers.stock.openapi.client.https.response.trade.TradeOrderResponse;
import com.tigerbrokers.stock.openapi.client.struct.enums.ActionType;
import com.tigerbrokers.stock.openapi.client.struct.enums.Category;
import com.tigerbrokers.stock.openapi.client.struct.enums.Currency;
import com.tigerbrokers.stock.openapi.client.struct.enums.KType;
import com.tigerbrokers.stock.openapi.client.struct.enums.Market;
import com.tigerbrokers.stock.openapi.client.struct.enums.SecType;
import com.tigerbrokers.stock.openapi.client.util.builder.AccountParamBuilder;
import com.tigerbrokers.stock.openapi.client.util.builder.TradeParamBuilder;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static java.lang.Thread.sleep;

/**
 * Example of a momentum strategy. Periodic run strategy, each time selects a number of stocks in the stock pool with the highest increase in the cycle, as the target stocks to buy and hold for this position transfer, and close out any stocks not selected in the previous position.
 */
public class Nasdaq100 {

  public static class TigerOpenClientConfig {

    static {
      ClientConfig clientConfig = ClientConfig.DEFAULT_CONFIG;
      clientConfig.configFilePath = "/data/tiger_config";
      // clientConfig.secretKey = "xxxxxx";// institutional trader private key
    }

    public static ClientConfig getDefaultClientConfig() {
      return ClientConfig.DEFAULT_CONFIG;
    }
  }

  /*Number of days to request historical quotes*/
  private static final int HISTORY_DAYS = 100;
  /*Batch size of request history*/
  private static final int BATCH_SIZE = 50;
  /*Number of symbols per request*/
  private static final int REQUEST_SYMBOLS_SIZE = 50;

  /*Calculating the time period of momentum*/
  private static final int MOMENTUM_PERIOD = 30;

  /*Number of shares held*/
  private static final int HOLDING_NUM = 5;

  /*Number of order checks*/
  private static final int ORDERS_CHECK_MAX_TIMES = 20;

  /* The higher the residual liquidity ratio, the safer the risk control status. If the residual liquidity ratio is too low (e.g. less than 5%), there is a risk of being forced to close the position.*/
  private static final double TARGET_OVERNIGHT_LIQUIDATION_RATIO = 0.6;

  private List<String> selectedSymbols = new ArrayList<>();

  private TigerHttpClient client = new TigerHttpClient(TigerOpenClientConfig.getDefaultClientConfig());

  private static final Logger logger = Logger.getLogger(Nasdaq100.class.getName());

  // Components of Nasdaq-100. Up to 2021/12/20
  private List<String> UNIVERSE_NDX =
      Arrays.asList("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");

  /**
   * Run Strategy
   */
  public void run() throws InterruptedException {
    // grab quote permission
    grabQuotePerm();
    // Stock Selection
    screenStocks();
    // Transferring positions
    rebalancePortfolio();
  }

  /**
   * Stock Selection. Calculate the historical ups and downs of each stock in the pool during the period, and select the top ones as the result
   */
  public void screenStocks() throws InterruptedException {
    Map<String, List<KlinePoint>> history = getHistory(UNIVERSE_NDX, HISTORY_DAYS, BATCH_SIZE);
    Map<String, Double> momentum = new HashMap<>();
    history.forEach((symbol, klinePoints) -> {
      int size = klinePoints.size();
      Double priceChange =
          (klinePoints.get(size - 1).getClose() - klinePoints.get(size - MOMENTUM_PERIOD).getClose()) / klinePoints.get(
              size - MOMENTUM_PERIOD).getClose();
      momentum.put(symbol, priceChange);
    });
    Map<String, Double> sortedMap = momentum.entrySet().stream()
        .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
        .limit(HOLDING_NUM)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
    selectedSymbols = new ArrayList<>(sortedMap.keySet());
    logger.info("selected symbols:" + selectedSymbols);
  }

  /**
   * Transferring positions
   */
  public void rebalancePortfolio() throws InterruptedException {
    if (selectedSymbols.isEmpty()) {
      logger.warning("no selected symbols, strategy exit!");
      return;
    }
    // Closeout of unselected stocks
    closePosition();
    // Open position in selected stocks
    openPosition(getAdjustValue());
  }

  /**
   * Get transfer amount
   */
  private Double getAdjustValue() {
    // Prime/Papper Account Assets
    PrimeAssetItem.Segment asset = getPrimeAsset();
    // Target overnight residual liquidity after position transfer(overnight_liquidation = equity_with_loan - overnight_margin)
    // Overnight Residual Liquidity Ratio =  overnight_liquidation / equity_with_loan
    double targetOvernightLiquidation = asset.getEquityWithLoan() * TARGET_OVERNIGHT_LIQUIDATION_RATIO;
    return asset.getOvernightLiquidation() - targetOvernightLiquidation;
  }

  /**
   * Close out any unselected stocks in the position
   */
  private void closePosition() throws InterruptedException {
    Map<String, Integer> positions = getPositions();
    Set<String> needCloseSymbols = positions.keySet();
    // Positions that are not selected need to be closed
    for (String selectedSymbol : selectedSymbols) {
      needCloseSymbols.remove(selectedSymbol);
    }
    if (!needCloseSymbols.isEmpty()) {
      Map<String, Double> latestPrice = getQuote(new ArrayList<>(needCloseSymbols));
      List<TradeOrderRequest> orderRequests = new ArrayList<>();
      needCloseSymbols.forEach(symbol -> {
        ContractItem contract = ContractItem.buildStockContract(symbol, Currency.USD.name());
        TradeOrderRequest request = TradeOrderRequest.buildLimitOrder(contract, ActionType.SELL, positions.get(symbol),
            latestPrice.get(symbol));
        orderRequests.add(request);
      });
      executeOrders(orderRequests);
    }
  }

  /**
   * Open positions on selected stocks
   * @Param adjustValue Amount of position to be transferred
   * @throws InterruptedException
   */
  private void openPosition(Double adjustValue) throws InterruptedException {
    double adjustValuePerStock = adjustValue / HOLDING_NUM;
    if (adjustValue <= 0) {
      logger.info("no enough liquidation");
      return;
    }
    Map<String, Double> latestPrice = getQuote(selectedSymbols);
    List<TradeOrderRequest> orders = new ArrayList<>();
    for (String symbol : selectedSymbols) {
      int quantity = (int) (adjustValuePerStock / latestPrice.get(symbol));
      if (quantity == 0) {
        logger.warning("can not place order with zero quantity" + symbol);
        continue;
      }
      ContractItem contract = ContractItem.buildStockContract(symbol, Currency.USD.name());
      TradeOrderRequest request = TradeOrderRequest.buildLimitOrder(contract, ActionType.BUY, quantity,
          latestPrice.get(symbol));
      orders.add(request);
    }
    executeOrders(orders);
  }

  /**
   * Execute Orders
   * @param orderRequests Order Request List 
   */
  private void executeOrders(List<TradeOrderRequest> orderRequests) throws InterruptedException {
    orderRequests.forEach(order -> {
      TradeOrderResponse response = client.execute(order);
      logger.info("place order: " + response);
    });
    sleep(20000);
    int i = 0;
    while (i <= ORDERS_CHECK_MAX_TIMES) {
      logger.info("check order");
      JSONArray openOrders = getOrders(MethodName.ACTIVE_ORDERS);
      if (openOrders.isEmpty()) {
        logger.info("no open orders.");
        break;
      }
      // If the maximum number of checks is reached and not yet filled, the order will be cancel.
      if (i >= ORDERS_CHECK_MAX_TIMES) {
        for (int k = 0; k < openOrders.size(); ++k) {
          JSONObject order = openOrders.getJSONObject(k);
          cancelOrder(order.getLong("id"));
        }
      }
      i++;
      sleep(20000);
    }

    // Orders Fulfilled
    JSONArray filledOrders = getOrders(MethodName.FILLED_ORDERS);
    logger.info("filledOrders:" + filledOrders.toJSONString());
    // Orders Cancelled
    JSONArray inactiveOrders = getOrders(MethodName.INACTIVE_ORDERS);
    logger.info("inactiveOrders:" + inactiveOrders);
  }

  /**
   * Cancel Order
   * @param id OrderId
   */
  private void cancelOrder(Long id) {
    TigerHttpRequest request = new TigerHttpRequest(MethodName.CANCEL_ORDER);
    String bizContent = TradeParamBuilder.instance()
        .account(TigerOpenClientConfig.getDefaultClientConfig().defaultAccount)
        .id(id)
        .buildJson();
    request.setBizContent(bizContent);
    client.execute(request);
  }

  /**
   * Get the list of recent orders with different status
   *
   * @param apiServiceType
   *    ACTIVE_ORDERS 
   *    INACTIVE_ORDERS 
   *    FILLED_ORDERS 
   */
  private JSONArray getOrders(MethodName apiServiceType) {
    logger.info("getOrders by:" + apiServiceType);
    TigerHttpRequest request = new TigerHttpRequest(apiServiceType);
    String bizContent = AccountParamBuilder.instance()
        .account(TigerOpenClientConfig.getDefaultClientConfig().defaultAccount)
        .startDate(Instant.now().minus(1, ChronoUnit.DAYS).toEpochMilli())
        .endDate(Instant.now().toEpochMilli())
        .secType(SecType.STK)
        .market(Market.US)
        .isBrief(false)
        .buildJson();
    request.setBizContent(bizContent);

    TigerHttpResponse response = client.execute(request);
    return JSON.parseObject(response.getData()).getJSONArray("items");
  }

  /**
   * Get Prime/Paper Account Assets
   */
  private PrimeAssetItem.Segment getPrimeAsset() {
    PrimeAssetRequest assetRequest =
        PrimeAssetRequest.buildPrimeAssetRequest(TigerOpenClientConfig.getDefaultClientConfig().defaultAccount);
    PrimeAssetResponse primeAssetResponse = client.execute(assetRequest);
    //Get Stock Segment Asset
    return primeAssetResponse.getSegment(Category.S);
  }

  /**
   * 
   */
  private JSONObject getGlobalAsset() {
    TigerHttpRequest request = new TigerHttpRequest(MethodName.ASSETS);
    String bizContent = AccountParamBuilder.instance()
        .account(TigerOpenClientConfig.getDefaultClientConfig().defaultAccount)
        .buildJson();
    request.setBizContent(bizContent);

    TigerHttpResponse response = client.execute(request);
    JSONArray assets = JSON.parseObject(response.getData()).getJSONArray("items");
    JSONObject asset1 = assets.getJSONObject(0);
    //Double cashBalance = asset1.getDouble("cashBalance");
    //Double sma = asset1.getDouble("SMA");
    //Double netLiquidation = asset1.getDouble("netLiquidation");
    //JSONArray segments = asset1.getJSONArray("segments");
    //JSONObject segment = segments.getJSONObject(0);
    //String category = segment.getString("category"); // "S" Stock, "C" Future
    return asset1;
  }

  /**
   * Get the position symbol and its corresponding quantity
   **/
  private Map<String, Integer> getPositions() {
    Map<String, Integer> result = new HashMap<>();
    TigerHttpRequest request = new TigerHttpRequest(MethodName.POSITIONS);

    String bizContent = AccountParamBuilder.instance()
        .account(TigerOpenClientConfig.getDefaultClientConfig().defaultAccount)
        .market(Market.US)
        .secType(SecType.STK)
        .buildJson();
    request.setBizContent(bizContent);

    TigerHttpResponse response = client.execute(request);
    if (response.getData() == null || response.getData().isEmpty()) {
      return result;
    }
    JSONArray positions = JSON.parseObject(response.getData()).getJSONArray("items");
    if (positions.isEmpty()) {
      return result;
    }
    for (int i = 0; i < positions.size(); ++i) {
      JSONObject pos = positions.getJSONObject(i);
      result.put(pos.getString("symbol"), pos.getInteger("quantity"));
    }
    return result;
  }

  /**
   * Grap Quote Permission
   */
  private void grabQuotePerm() {
    TigerHttpRequest request = new TigerHttpRequest(MethodName.GRAB_QUOTE_PERMISSION);
    String bizContent = AccountParamBuilder.instance()
        .buildJson();
    request.setBizContent(bizContent);
    TigerHttpResponse response = client.execute(request);
    logger.info("quote permissions: " + response.getData());
  }

  /**
   * Get Realtime Quote
   */
  private Map<String, Double> getQuote(List<String> symbols) throws InterruptedException {
    logger.info("getQuote.");
    Map<String, Double> quote = new HashMap<>();
    Collection<List<String>> partitions = partition(symbols, REQUEST_SYMBOLS_SIZE);
    for (List<String> part : partitions) {
      QuoteRealTimeQuoteResponse response = client.execute(QuoteRealTimeQuoteRequest.newRequest(part));
      if (response.isSuccess()) {
        response.getRealTimeQuoteItems().forEach(item -> quote.put(item.getSymbol(), item.getLatestPrice()));
      } else {
        logger.warning("QuoteRealTimeQuoteRequest:" + response.getMessage());
      }
      // Preventing the triggering of current limiting
      sleep(1000);
    }
    return quote;
  }

  /**
   * Get Historical Quote
   *
   * @param total total days
   * @param batchSize batch request size
   */
  private Map<String, List<KlinePoint>> getHistory(List<String> symbols, int total, int batchSize)
      throws InterruptedException {
    logger.info("getHistory.");
    Map<String, List<KlinePoint>> history = new HashMap<>();
    Collection<List<String>> partitions = partition(symbols, REQUEST_SYMBOLS_SIZE);
    for (List<String> part : partitions) {
      int i = 0;
      long endTime = Instant.now().toEpochMilli();
      while (i < total) {
        QuoteKlineRequest request = QuoteKlineRequest.newRequest(part, KType.day, -1L, endTime);
        request.withLimit(batchSize);
        QuoteKlineResponse response = client.execute(request);
        if (response.isSuccess()) {
          for (KlineItem item : response.getKlineItems()) {
            List<KlinePoint> klinePoints = history.getOrDefault(item.getSymbol(), new ArrayList<>());
            klinePoints.addAll(item.getItems());
            endTime = item.getItems().get(0).getTime();
            history.put(item.getSymbol(), klinePoints);
          }
        } else {
          logger.warning("QuoteKlineRequest:" + response.getMessage());
        }
        i += batchSize;
        // Preventing the triggering of current limiting
        sleep(1000);
      }
    }
    Map<String, List<KlinePoint>> sortedHistory = new HashMap<>();
    history.forEach((symbol, klinePoints) -> {
      klinePoints.sort(Comparator.comparingLong(KlinePoint::getTime));
      sortedHistory.put(symbol, klinePoints);
    });
    return sortedHistory;
  }

  /**
   * list partition
   */
  static <T> Collection<List<T>> partition(List<T> inputList, int size) {
    final AtomicInteger counter = new AtomicInteger(0);
    return inputList.stream()
        .collect(Collectors.groupingBy(s -> counter.getAndIncrement() / size))
        .values();
  }

  public static void main(String[] args) throws InterruptedException {
    Nasdaq100 strategy = new Nasdaq100();
    strategy.run();
  }
}


Last update: