中文

Complete Usage Example

Momentum Strategy

Note: This strategy is only provided as an example for using the API, such as querying market data, placing orders, etc. The strategy itself has not been verified and does not constitute investment advice.

The basic idea of this strategy is that assets with high returns over a past period will continue their original trend and may achieve high returns in the future. The stock pool used for stock selection is the Nasdaq-100 index constituents.

The specific implementation process is: 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 for this rebalancing, and close positions for previously held stocks that were not selected.

The code is as follows:

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 = "./src/main/resources";
    // 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 {
    // Grab quote permissions
    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/simulation 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);
    return JSON.parseObject(response.getData()).getJSONArray("items");
  }

  /** Get prime/simulation 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.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;
  }

  /** Grab quote permissions */
  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 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();
  }
}