Full Example
Momentum Strategy
Note: This strategy only demonstrates API usage, such as querying market data and placing orders. It has not been validated and does not constitute investment advice.
This strategy assumes that stocks with stronger recent returns may continue to outperform. Its sample universe is a fixed list of US stocks and is not a current index-constituent list.
Run the strategy periodically. At each rebalance, select the stocks with the highest returns over the measurement period and close positions 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;
// Fixed sample universe; update it before using the strategy
static const std::vector<utility::string_t> SAMPLE_UNIVERSE = {
U("AAPL"), U("AMZN"), U("AVGO"), U("COST"), U("GOOG"),
U("META"), U("MSFT"), U("NFLX"), U("NVDA"), U("TSLA")
};
// 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;
// Order and bar query time_t parameters use Unix timestamps in milliseconds
time_t now_ms() {
auto now = std::chrono::system_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
}
time_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 < SAMPLE_UNIVERSE.size(); i += REQUEST_SIZE) {
value symbols = value::array();
for (size_t j = i; j < std::min(i + (size_t)REQUEST_SIZE, SAMPLE_UNIVERSE.size()); j++) {
symbols[j - i] = value::string(SAMPLE_UNIVERSE[j]);
}
// Get daily K-line data with enough days to calculate momentum
std::vector<Kline> bars = quote_client_.get_kline(
symbols, U("day"), time_before_days(MOMENTUM_PERIOD + 5),
now_ms(), MOMENTUM_PERIOD + 5
);
// Parse K-line data and calculate momentum for each stock
for (const auto& kline : bars) {
if (!kline.items.empty()) {
auto oldest = std::min_element(
kline.items.begin(), kline.items.end(),
[](const KlineItem& a, const KlineItem& b) { return a.time < b.time; }
);
momentum_map[kline.symbol] = oldest->close;
}
}
// 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 < SAMPLE_UNIVERSE.size(); i += REQUEST_SIZE) {
value symbols = value::array();
for (size_t j = i; j < std::min(i + (size_t)REQUEST_SIZE, SAMPLE_UNIVERSE.size()); j++) {
symbols[j - i] = value::string(SAMPLE_UNIVERSE[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
std::vector<Position> position_list = trade_client_.get_position_list(
config_.account, U("STK"), U("ALL"), U("US")
);
std::map<utility::string_t, long long> positions; // symbol -> quantity
for (const auto& pos : position_list) {
positions[pos.contract.symbol] = pos.position;
}
// 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) {
long long quantity = positions[symbol];
auto price = close_prices.find(symbol);
if (quantity == 0 || price == close_prices.end() || price->second <= 0) continue;
Contract contract = ContractUtil::stock_contract(symbol, U("USD"));
utility::string_t action = (quantity > 0) ? U("SELL") : U("BUY");
// A production strategy may use a marketable limit or price buffer to reduce non-fill risk.
Order order = OrderUtil::limit_order(
config_.account, contract, action,
std::abs(quantity), price->second
);
sell_orders.push_back(order);
std::cout << "Close position: " << action << " " << symbol
<< " quantity=" << std::abs(quantity)
<< " price=" << price->second << std::endl;
}
execute_orders(sell_orders);
}
// Get account asset info, calculate available buying amount
PortfolioAccount portfolio = trade_client_.get_prime_portfolio();
double equity_with_loan = 0;
double overnight_liquidation = 0;
for (const auto& segment : portfolio.segments) {
if (segment.category == U("S")) {
equity_with_loan = segment.equity_with_loan;
overnight_liquidation = segment.overnight_liquidation;
break;
}
}
// 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;
}
if (selected_symbols_.empty()) {
std::cout << "No symbols selected; skipping purchases" << 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_) {
auto price = buy_prices.find(symbol);
if (price == buy_prices.end() || price->second <= 0) continue;
int quantity = static_cast<int>(adjust_value * weight / price->second);
if (quantity <= 0) continue;
Contract contract = ContractUtil::stock_contract(symbol, U("USD"));
Order order = OrderUtil::limit_order(
config_.account, contract, U("BUY"),
quantity, price->second
);
order.time_in_force = U("GTC"); // Good-Til-Cancelled
buy_orders.push_back(order);
std::cout << "Buy: BUY " << symbol
<< " quantity=" << quantity
<< " price=" << price->second << 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_active_orders(
config_.account, SecType::STK, Market::US, U(""),
time_before_days(1), now_ms()
);
// get_active_orders returns every working order on the account, so keep
// only the ids this call submitted
std::vector<int64_t> own_active_ids;
if (open_orders.is_array()) {
for (size_t j = 0; j < open_orders.size(); j++) {
auto& open_order = open_orders[j];
if (!open_order.has_field(U("id"))) {
continue;
}
int64_t order_id = open_order[U("id")].as_number().to_int64();
if (local_orders.count(order_id) > 0) {
own_active_ids.push_back(order_id);
}
}
}
if (own_active_ids.empty()) {
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();
// Skip orders that were not submitted by this call
if (local_orders.count(order_id) == 0) {
continue;
}
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 {
Order order = trade_client_.get_order(order_id);
trade_client_.modify_order(order, 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 (int64_t order_id : own_active_ids) {
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(
config_.account, SecType::STK, Market::US, U(""),
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("your_config_directory_path"));
// Create strategy and run
MomentumStrategy strategy(config);
strategy.run();
return 0;
}Updated 12 days ago
Did this page help you?
