Full Example

Momentum Strategy

Note: This strategy is provided only as an example of how to use the API interfaces (e.g., querying market data, placing orders). The strategy itself has not been verified and does not constitute investment advice.

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

The specific implementation: Run the strategy periodically. Each time, select the stocks with the highest returns over the period from the stock pool as target stocks for the current rebalancing. Close positions for previously held stocks that are no longer selected.

Code:

#include <iostream>
#include <vector>
#include <map>
#include <set>
#include <string>
#include <algorithm>
#include <numeric>
#include <cmath>
#include <thread>
#include <chrono>
#include <ctime>

#include "tigerapi/quote_client.h"
#include "tigerapi/trade_client.h"
#include "tigerapi/client_config.h"
#include "tigerapi/contract_util.h"
#include "tigerapi/order_util.h"

using namespace TIGER_API;
using namespace web::json;

// Nasdaq-100 index components
static const std::vector<utility::string_t> UNIVERSE_NDX = {
    U("AAPL"), U("ADBE"), U("ADI"), U("ADP"), U("ADSK"), U("AEP"), U("ALGN"), U("AMAT"), U("AMD"), U("AMGN"),
    U("AMZN"), U("ANSS"), U("ASML"), U("ATVI"), U("AVGO"), U("BIDU"), U("BIIB"), U("BKNG"), U("CDNS"), U("CDW"),
    U("CERN"), U("CHKP"), U("CHTR"), U("CMCSA"), U("COST"), U("CPRT"), U("CRWD"), U("CSCO"), U("CSX"), U("CTAS"),
    U("CTSH"), U("DLTR"), U("DOCU"), U("DXCM"), U("EA"), U("EBAY"), U("EXC"), U("FAST"), U("FB"), U("FISV"),
    U("FOX"), U("GILD"), U("GOOG"), U("HON"), U("IDXX"), U("ILMN"), U("INCY"), U("INTC"), U("INTU"), U("ISRG"),
    U("JD"), U("KDP"), U("KHC"), U("KLAC"), U("LRCX"), U("LULU"), U("MAR"), U("MCHP"), U("MDLZ"), U("MELI"),
    U("MNST"), U("MRNA"), U("MRVL"), U("MSFT"), U("MTCH"), U("MU"), U("NFLX"), U("NTES"), U("NVDA"), U("NXPI"),
    U("OKTA"), U("ORLY"), U("PAYX"), U("PCAR"), U("PDD"), U("PEP"), U("PTON"), U("PYPL"), U("QCOM"), U("REGN"),
    U("ROST"), U("SBUX"), U("SGEN"), U("SIRI"), U("SNPS"), U("SPLK"), U("SWKS"), U("TCOM"), U("TEAM"), U("TMUS"),
    U("TSLA"), U("TXN"), U("VRSK"), U("VRSN"), U("VRTX"), U("WBA"), U("WDAY"), U("XEL"), U("XLNX"), U("ZM")
};

// Number of stocks to hold
static const int HOLDING_NUM = 5;
// Maximum order check attempts
static const int ORDERS_CHECK_MAX_TIMES = 10;
// Number of symbols per request for market data
static const int REQUEST_SIZE = 50;
// Momentum calculation period (days)
static const int MOMENTUM_PERIOD = 30;

// Get current timestamp in milliseconds
int64_t now_ms() {
    auto now = std::chrono::system_clock::now();
    return std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
}

// Get timestamp in milliseconds for a specified number of days ago
int64_t time_before_days(int days) {
    auto now = std::chrono::system_clock::now();
    auto target = now - std::chrono::hours(24 * days);
    return std::chrono::duration_cast<std::chrono::milliseconds>(target.time_since_epoch()).count();
}

/**
 * Momentum Strategy Class
 */
class MomentumStrategy {
public:
    MomentumStrategy(ClientConfig& config)
        : quote_client_(config), trade_client_(config), config_(config) {}

    void run() {
        // Query market data permissions
        value perms = quote_client_.grab_quote_permission();
        std::cout << "Quote permissions: " << perms.serialize() << std::endl;

        // 1. Screen stocks by momentum
        screen_stocks();

        // 2. Rebalance portfolio
        rebalance_portfolio();
    }

private:
    QuoteClient quote_client_;
    TradeClient trade_client_;
    ClientConfig& config_;
    std::vector<utility::string_t> selected_symbols_;

    /**
     * Screen stocks by momentum
     * Select stocks with the highest returns over the period
     */
    void screen_stocks() {
        std::cout << "=== Start screening stocks ===" << std::endl;

        // Store momentum (return rate) for each stock
        std::map<utility::string_t, double> momentum_map;

        // Fetch historical K-line data in batches
        for (size_t i = 0; i < UNIVERSE_NDX.size(); i += REQUEST_SIZE) {
            value symbols = value::array();
            for (size_t j = i; j < std::min(i + (size_t)REQUEST_SIZE, UNIVERSE_NDX.size()); j++) {
                symbols[j - i] = value::string(UNIVERSE_NDX[j]);
            }

            // Get daily K-line data with enough days to calculate momentum
            value bars = quote_client_.get_bars(symbols, U("day"), MOMENTUM_PERIOD + 5);

            // Parse K-line data and calculate momentum for each stock
            if (bars.is_array()) {
                for (size_t k = 0; k < bars.size(); k++) {
                    auto& bar = bars[k];
                    if (bar.has_field(U("symbol")) && bar.has_field(U("close"))) {
                        utility::string_t symbol = bar[U("symbol")].as_string();
                        // Record the earliest closing price (for calculating period return)
                        if (momentum_map.find(symbol) == momentum_map.end()) {
                            momentum_map[symbol] = bar[U("close")].as_double();
                        }
                    }
                }
            }

            // Rate limit control
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }

        // Get latest prices and calculate returns
        std::vector<std::pair<utility::string_t, double>> momentum_list;
        for (size_t i = 0; i < UNIVERSE_NDX.size(); i += REQUEST_SIZE) {
            value symbols = value::array();
            for (size_t j = i; j < std::min(i + (size_t)REQUEST_SIZE, UNIVERSE_NDX.size()); j++) {
                symbols[j - i] = value::string(UNIVERSE_NDX[j]);
            }
            value briefs = quote_client_.get_brief(symbols);
            if (briefs.is_array()) {
                for (size_t k = 0; k < briefs.size(); k++) {
                    auto& item = briefs[k];
                    if (item.has_field(U("symbol")) && item.has_field(U("latestPrice"))) {
                        utility::string_t sym = item[U("symbol")].as_string();
                        double latest = item[U("latestPrice")].as_double();
                        if (momentum_map.count(sym) && momentum_map[sym] > 0) {
                            double change = (latest - momentum_map[sym]) / momentum_map[sym];
                            momentum_list.push_back({sym, change});
                        }
                    }
                }
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }

        // Sort by return in descending order, select top HOLDING_NUM stocks
        std::sort(momentum_list.begin(), momentum_list.end(),
                  [](const auto& a, const auto& b) { return a.second > b.second; });

        selected_symbols_.clear();
        for (int i = 0; i < std::min(HOLDING_NUM, (int)momentum_list.size()); i++) {
            selected_symbols_.push_back(momentum_list[i].first);
            std::cout << "Selected: " << momentum_list[i].first
                      << " momentum: " << momentum_list[i].second << std::endl;
        }
    }

    /**
     * Rebalance portfolio
     * Close positions for stocks not selected, then buy selected stocks with equal weight
     */
    void rebalance_portfolio() {
        std::cout << "=== Start rebalancing ===" << std::endl;

        // Get current positions
        value positions_json = trade_client_.get_positions(U("STK"), U("US"));
        std::map<utility::string_t, int> positions;  // symbol -> quantity
        if (positions_json.is_array()) {
            for (size_t i = 0; i < positions_json.size(); i++) {
                auto& pos = positions_json[i];
                if (pos.has_field(U("symbol")) && pos.has_field(U("quantity"))) {
                    positions[pos[U("symbol")].as_string()] = pos[U("quantity")].as_integer();
                }
            }
        }

        // Find stocks to close: in positions but not in selected list
        std::set<utility::string_t> selected_set(selected_symbols_.begin(), selected_symbols_.end());
        std::vector<utility::string_t> need_close_symbols;
        for (auto& [symbol, qty] : positions) {
            if (selected_set.find(symbol) == selected_set.end()) {
                need_close_symbols.push_back(symbol);
            }
        }

        // Close unselected positions
        if (!need_close_symbols.empty()) {
            // Get latest prices for limit orders
            value close_symbols_arr = value::array();
            for (size_t i = 0; i < need_close_symbols.size(); i++) {
                close_symbols_arr[i] = value::string(need_close_symbols[i]);
            }
            value close_briefs = quote_client_.get_brief(close_symbols_arr);
            std::map<utility::string_t, double> close_prices;
            if (close_briefs.is_array()) {
                for (size_t i = 0; i < close_briefs.size(); i++) {
                    auto& item = close_briefs[i];
                    if (item.has_field(U("symbol")) && item.has_field(U("latestPrice"))) {
                        close_prices[item[U("symbol")].as_string()] = item[U("latestPrice")].as_double();
                    }
                }
            }

            std::vector<Order> sell_orders;
            for (auto& symbol : need_close_symbols) {
                int quantity = positions[symbol];
                if (quantity == 0) continue;

                Contract contract = ContractUtil::stock_contract(symbol, U("USD"));
                utility::string_t action = (quantity > 0) ? U("SELL") : U("BUY");
                Order order = OrderUtil::limit_order(
                    config_.account, contract, action,
                    std::abs(quantity), close_prices[symbol]
                );
                sell_orders.push_back(order);
                std::cout << "Close position: " << action << " " << symbol
                          << " quantity=" << std::abs(quantity)
                          << " price=" << close_prices[symbol] << std::endl;
            }
            execute_orders(sell_orders);
        }

        // Get account asset info, calculate available buying amount
        value asset_json = trade_client_.get_prime_assets();
        double equity_with_loan = 0;
        double overnight_liquidation = 0;
        if (asset_json.has_field(U("equityWithLoan"))) {
            equity_with_loan = asset_json[U("equityWithLoan")].as_double();
        }
        if (asset_json.has_field(U("overnightLiquidation"))) {
            overnight_liquidation = asset_json[U("overnightLiquidation")].as_double();
        }

        // Target overnight remaining liquidity after rebalancing
        // Overnight remaining liquidity ratio = overnight remaining liquidity / equity with loan
        double target_ratio = 0.6;
        double target_overnight_liquidation = equity_with_loan * target_ratio;
        double adjust_value = overnight_liquidation - target_overnight_liquidation;
        if (adjust_value <= 0) {
            std::cout << "Insufficient liquidity, cannot buy" << std::endl;
            return;
        }

        // Get latest prices for selected stocks
        value selected_arr = value::array();
        for (size_t i = 0; i < selected_symbols_.size(); i++) {
            selected_arr[i] = value::string(selected_symbols_[i]);
        }
        value buy_briefs = quote_client_.get_brief(selected_arr);
        std::map<utility::string_t, double> buy_prices;
        if (buy_briefs.is_array()) {
            for (size_t i = 0; i < buy_briefs.size(); i++) {
                auto& item = buy_briefs[i];
                if (item.has_field(U("symbol")) && item.has_field(U("latestPrice"))) {
                    buy_prices[item[U("symbol")].as_string()] = item[U("latestPrice")].as_double();
                }
            }
        }

        // Equal weight position allocation
        double weight = 1.0 / selected_symbols_.size();
        std::vector<Order> buy_orders;
        for (auto& symbol : selected_symbols_) {
            double price = buy_prices[symbol];
            if (price <= 0) continue;
            int quantity = static_cast<int>(adjust_value * weight / price);
            if (quantity <= 0) continue;

            Contract contract = ContractUtil::stock_contract(symbol, U("USD"));
            Order order = OrderUtil::limit_order(
                config_.account, contract, U("BUY"),
                quantity, price
            );
            order.time_in_force = U("GTC");  // Good-Til-Cancelled
            buy_orders.push_back(order);
            std::cout << "Buy: BUY " << symbol
                      << " quantity=" << quantity
                      << " price=" << price << std::endl;
        }
        execute_orders(buy_orders);
    }

    /**
     * Execute order list: place orders, check fills, modify orders, cancel orders
     */
    void execute_orders(std::vector<Order>& orders) {
        std::map<int64_t, Order> local_orders;
        for (auto& order : orders) {
            try {
                value result = trade_client_.place_order(order);
                std::cout << "Order placed: " << order.contract.symbol
                          << " id=" << order.id << std::endl;
                local_orders[order.id] = order;
            } catch (const std::exception& e) {
                std::cerr << "Order failed: " << order.contract.symbol
                          << " error=" << e.what() << std::endl;
            }
        }

        // Wait for order fills
        std::this_thread::sleep_for(std::chrono::seconds(20));

        for (int i = 0; i <= ORDERS_CHECK_MAX_TIMES; i++) {
            std::cout << "Checking order status, attempt " << i << std::endl;

            value open_orders = trade_client_.get_open_orders(
                U("STK"), U("US"),
                time_before_days(1), now_ms()
            );

            if (!open_orders.is_array() || open_orders.size() == 0) {
                std::cout << "All orders filled" << std::endl;
                break;
            }

            // After half of max checks, modify unfilled orders with latest price
            if (i == ORDERS_CHECK_MAX_TIMES / 2) {
                for (size_t j = 0; j < open_orders.size(); j++) {
                    auto& open_order = open_orders[j];
                    if (open_order.has_field(U("symbol")) && open_order.has_field(U("id"))) {
                        utility::string_t sym = open_order[U("symbol")].as_string();
                        int64_t order_id = open_order[U("id")].as_number().to_int64();

                        value sym_arr = value::array();
                        sym_arr[0] = value::string(sym);
                        value brief = quote_client_.get_brief(sym_arr);
                        if (brief.is_array() && brief.size() > 0 &&
                            brief[0].has_field(U("latestPrice"))) {
                            double new_price = brief[0][U("latestPrice")].as_double();
                            try {
                                trade_client_.modify_order(order_id, new_price);
                                std::cout << "Order modified: id=" << order_id
                                          << " symbol=" << sym
                                          << " new_price=" << new_price << std::endl;
                            } catch (const std::exception& e) {
                                std::cerr << "Modify failed: id=" << order_id
                                          << " error=" << e.what() << std::endl;
                            }
                        }
                    }
                }
            }

            // If max checks reached and still unfilled, cancel orders
            if (i >= ORDERS_CHECK_MAX_TIMES) {
                for (size_t j = 0; j < open_orders.size(); j++) {
                    auto& open_order = open_orders[j];
                    if (open_order.has_field(U("id"))) {
                        int64_t order_id = open_order[U("id")].as_number().to_int64();
                        try {
                            trade_client_.cancel_order(order_id);
                            std::cout << "Order cancelled: id=" << order_id << std::endl;
                        } catch (const std::exception& e) {
                            std::cerr << "Cancel failed: id=" << order_id
                                      << " error=" << e.what() << std::endl;
                        }
                    }
                }
            }

            std::this_thread::sleep_for(std::chrono::seconds(10));
        }

        // Print filled orders
        value filled = trade_client_.get_filled_orders(
            U("STK"), U("US"),
            time_before_days(1), now_ms()
        );
        if (filled.is_array()) {
            std::cout << "Filled orders:" << std::endl;
            for (size_t i = 0; i < filled.size(); i++) {
                std::cout << "  " << filled[i].serialize() << std::endl;
            }
        }
    }
};

int main() {
    // Initialize configuration
    ClientConfig config(false, U("/path/to/your/properties/"));

    // Create strategy and run
    MomentumStrategy strategy(config);
    strategy.run();

    return 0;
}