Complete Usage Example

Momentum Strategy

This unverified strategy demonstrates Market Data requests and order placement. It is not investment advice.

This momentum example assumes that recent outperformers may continue to outperform. It uses Nasdaq-100 constituents as its stock universe.

Each run ranks the stocks by their gain over the configured period, buys and holds the highest-ranked symbols, and closes positions that are no longer selected.

import static java.lang.Thread.sleep;

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.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.MethodName;
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;

/** Momentum strategy example. Run the strategy periodically, each time selecting several stocks with the highest gains during the period from the stock pool as target stocks to buy and hold, and close positions for previously held stocks that were not selected. */
public class Nasdaq100 {

  private static final ClientConfig clientConfig = ClientConfig.DEFAULT_CONFIG;
  private static final TigerHttpClient client;

  static {

    // Path to the configuration file tiger_openapi_config.properties exported from the developer platform. For TBHK license, 
    // the tiger_openapi_token.properties file is also in this directory
    clientConfig.configFilePath = "your_config_directory_path";
    // clientConfig.secretKey = "xxxxxx"; // Required field for institutional account traders: secret key
    client = TigerHttpClient.getInstance().clientConfig(clientConfig);
  }

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

  /*Time period for calculating momentum*/
  private static final int MOMENTUM_PERIOD = 30;

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

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

  /* Target overnight remaining liquidity ratio after rebalancing. The higher the remaining liquidity ratio, the safer the risk control status. If the overnight remaining liquidity ratio is too low (e.g., less than 5%), there is a risk of forced liquidation.*/
  private static final double TARGET_OVERNIGHT_LIQUIDATION_RATIO = 0.6;

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

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

  // Components of Nasdaq-100. Up to 2021/12/20
  private final 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 {
    // Claim market data device access.
    grabQuotePerm();
    // Screen stocks
    screenStocks();
    // Rebalance portfolio
    rebalancePortfolio();
  }

  /** Screen stocks. Calculate historical gains and losses of each stock in the pool during the period, and select the ones with the highest gains as results */
  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);
  }

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

  /** Get rebalancing amount */
  private Double getAdjustValue() {
    // Prime/Paper Trading account assets
    PrimeAssetItem.Segment asset = getPrimeAsset();
    // Target overnight remaining liquidity after rebalancing (overnight remaining liquidity overnight_liquidation = total equity with loan value equity_with_loan - overnight margin overnight_margin)
    // Overnight remaining liquidity ratio = overnight remaining liquidity overnight_liquidation / total equity with loan value equity_with_loan
    double targetOvernightLiquidation =
        asset.getEquityWithLoan() * TARGET_OVERNIGHT_LIQUIDATION_RATIO;
    return asset.getOvernightLiquidation() - targetOvernightLiquidation;
  }

  /** Close positions of unselected stocks in holdings */
  private void closePosition() throws InterruptedException {
    Map<String, Integer> positions = getPositions();
    Set<String> needCloseSymbols = positions.keySet();
    // Holdings 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 for selected stocks
   *
   * @throws InterruptedException @Param adjustValue Amount to rebalance
   */
  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 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 orders are still not filled, cancel orders
      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);
    }

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

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

  /**
   * Get recent order list with different statuses
   *
   * @param apiServiceType ACTIVE_ORDERS unfilled INACTIVE_ORDERS cancelled FILLED_ORDERS filled
   */
  private JSONArray getOrders(MethodName apiServiceType) {
    logger.info("getOrders by:" + apiServiceType);
    TigerHttpRequest request = new TigerHttpRequest(apiServiceType);
    String bizContent =
        AccountParamBuilder.instance()
            .account(clientConfig.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);
    if (response == null || response.getData() == null || response.getData().isEmpty()) {
      return new JSONArray();
    }
    JSONArray items = JSON.parseObject(response.getData()).getJSONArray("items");
    // Return an empty array when items is absent so callers never call isEmpty() on null
    return items == null ? new JSONArray() : items;
  }

  /** Get prime/paper trading account assets */
  private PrimeAssetItem.Segment getPrimeAsset() {
    PrimeAssetRequest assetRequest =
        PrimeAssetRequest.buildPrimeAssetRequest(clientConfig.defaultAccount);
    PrimeAssetResponse primeAssetResponse = client.execute(assetRequest);
    // Query stock-related asset information
    return primeAssetResponse.getSegment(Category.S);
  }

  /** Get global account assets */
  private JSONObject getGlobalAsset() {
    TigerHttpRequest request = new TigerHttpRequest(MethodName.ASSETS);
    String bizContent =
        AccountParamBuilder.instance().account(clientConfig.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" stocks, "C" futures
    return asset1;
  }

  /** Get position symbols and their corresponding quantities */
  private Map<String, Integer> getPositions() {
    Map<String, Integer> result = new HashMap<>();
    TigerHttpRequest request = new TigerHttpRequest(MethodName.POSITIONS);

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

    TigerHttpResponse response = client.execute(request);
    if (response == null || response.getData() == null || response.getData().isEmpty()) {
      return result;
    }
    JSONArray positions = JSON.parseObject(response.getData()).getJSONArray("items");
    if (positions == null || 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;
  }

  /** Claims market data device access for this client. */
  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("market data permissions: " + response.getData());
  }

  /** Get real-time quotes */
  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());
      }
      // Prevent rate limiting
      sleep(1000);
    }
    return quote;
  }

  /**
   * Get historical quotes
   *
   * @param total Total number of days for quotes
   * @param batchSize Number of days per request
   */
  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;
        // Prevent rate 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;
  }

  /** Batch partition list */
  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();
  }
}

Did this page help you?