完整使用示例
大约 6 分钟
动量策略
注意:本策略仅为使用接口提供示例用法,比如查询行情,下单等,策略本身未经验证,不构成投资建议。
该策略基本思路为"过去一段时间收益较高的资产,在未来仍将延续原有趋势,可能会获得较高的收益",选股所用股票池为纳斯达克100指数成份股。
具体实现过程为:定期运行策略,每次选取股票池中周期内涨幅最高的若干股票,作为本次调仓的目标股票买入持有,对先前持仓中未入选的股票进行平仓。
代码如下:
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;
/**
* 动量策略示例。定期运行策略,每次选取股票池中周期内涨幅最高的若干股票,作为本次调仓的目标股票买入持有,对先前持仓中未入选的股票进行平仓。
*/
public class Nasdaq100 {
private static ClientConfig clientConfig = ClientConfig.DEFAULT_CONFIG;
static {
//从开发者信息页面导出的配置文件tiger_openapi_config.properties、tiger_openapi_token.properties存放路径
clientConfig.configFilePath = "/data/tiger_config";
// clientConfig.secretKey = "xxxxxx";// institutional trader private key
}
/*请求历史行情的天数*/
private static final int HISTORY_DAYS = 100;
/*请求历史的批量大小*/
private static final int BATCH_SIZE = 50;
/*每次请求symbol的个数*/
private static final int REQUEST_SYMBOLS_SIZE = 50;
/*计算动量的时间周期*/
private static final int MOMENTUM_PERIOD = 30;
/*持股个数*/
private static final int HOLDING_NUM = 5;
/*订单检查次数*/
private static final int ORDERS_CHECK_MAX_TIMES = 20;
/* 调仓后隔夜剩余流动性目标占比,剩余流动性占比越高,风控状态越安全,如果隔夜剩余流动性占比过低(比如小于5%), 则存在被强平的风险。*/
private static final double TARGET_OVERNIGHT_LIQUIDATION_RATIO = 0.6;
private List<String> selectedSymbols = new ArrayList<>();
private TigerHttpClient client = TigerHttpClient.getInstance().clientConfig(clientConfig);
private static final Logger logger = Logger.getLogger(Nasdaq100.class.getName());
// 纳斯达克100指数成分股 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");
/**
* 运行策略
*/
public void run() throws InterruptedException {
// 抢占行情权限
grabQuotePerm();
// 选股
screenStocks();
// 调仓
rebalancePortfolio();
}
/**
* 选股。计算周期内池中股票各自的历史涨跌幅,选取涨幅最高几只作为结果
*/
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);
}
/**
* 调仓
*/
public void rebalancePortfolio() throws InterruptedException {
if (selectedSymbols.isEmpty()) {
logger.warning("no selected symbols, strategy exit!");
return;
}
// 平仓未入选股票
closePosition();
// 开仓入选股票
openPosition(getAdjustValue());
}
/**
* 获取调仓金额
*/
private Double getAdjustValue() {
// 综合/模拟账户资产
PrimeAssetItem.Segment asset = getPrimeAsset();
// 调仓后目标隔夜剩余流动性(隔夜剩余流动性 overnight_liquidation = 含贷款价值总权益 equity_with_loan - 隔夜保证金 overnight_margin)
// 隔夜剩余流动性比例 = 隔夜剩余流动性 overnight_liquidation / 含贷款价值总权益 equity_with_loan
double targetOvernightLiquidation = asset.getEquityWithLoan() * TARGET_OVERNIGHT_LIQUIDATION_RATIO;
return asset.getOvernightLiquidation() - targetOvernightLiquidation;
}
/**
* 将持仓中未入选的股票平仓
*/
private void closePosition() throws InterruptedException {
Map<String, Integer> positions = getPositions();
Set<String> needCloseSymbols = positions.keySet();
// 持仓中没有入选的需要平仓
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);
}
}
/**
* 对入选的股票进行开仓
* @Param adjustValue 要调仓的金额
* @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);
}
/**
* 执行订单
* @param orderRequests 订单列表
*/
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 (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);
}
// 已成交订单
JSONArray filledOrders = getOrders(MethodName.FILLED_ORDERS);
logger.info("filledOrders:" + filledOrders.toJSONString());
// 已撤销订单
JSONArray inactiveOrders = getOrders(MethodName.INACTIVE_ORDERS);
logger.info("inactiveOrders:" + inactiveOrders);
}
/**
* 撤单
* @param id 订单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);
}
/**
* 获取不同状态的最近订单列表
*
* @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(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");
}
/**
* 获取综合/模拟账户资产
*/
private PrimeAssetItem.Segment getPrimeAsset() {
PrimeAssetRequest assetRequest =
PrimeAssetRequest.buildPrimeAssetRequest(clientConfig.defaultAccount);
PrimeAssetResponse primeAssetResponse = client.execute(assetRequest);
//查询股票相关资产信息
return primeAssetResponse.getSegment(Category.S);
}
/**
* 获取环球账户资产
*/
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" 股票, "C" 期货
return asset1;
}
/**
* 获取持仓symbol及其对应数量
**/
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;
}
/**
* 抢占行情权限
*/
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());
}
/**
* 获取实时行情
*/
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());
}
// 防止触发限流
sleep(1000);
}
return quote;
}
/**
* 获取历史行情
*
* @param total 行情总天数
* @param batchSize 每次请求的天数
*/
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;
// 防止触发限流
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;
}
/**
* 列表批量分组
*/
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();
}
}