平仓
平仓就是提交一笔与持仓方向相反的普通订单。平台没有专门的平仓接口,委托下单的请求参数里也没有开仓、平仓标记:多头持仓用 Sell 订单卖出,空头持仓用 Buy 订单买入回补。持仓数量的正负号是判断方向的唯一依据。
前置条件
- 拥有交易权限的真实账户或模拟账户。
- OAuth client ID,参见快速开始。
- 已安装对应语言的 SDK。下面的示例同时用到
TradeContext(持仓、下单)和QuoteContext(交易时段、最新价)。
操作步骤
1. 查询持仓
用要平仓的标的调用股票持仓,读取 available_quantity,而不是 quantity。available_quantity 已经扣除了挂单冻结和当日交收规则占用的数量,是此刻真正可以平掉的量。
预期结果: 得到该标的的 StockPosition。如果没有该标的,或者 available_quantity 为 0,说明没有可平仓位,应直接停止。
2. 用正负号决定方向和数量
available_quantity | 持仓方向 | 订单 side | 订单数量 |
|---|---|---|---|
大于 0 | 多头 | Sell | available_quantity |
小于 0 | 空头 | Buy | abs(available_quantity) |
预期结果: 得到订单方向和一个正数数量。股票、ETF、窝轮、期权合约都适用同一规则。
3. 选择委托类型并提交
盘中使用市价单,保证仓位立即平掉。美股盘前、盘后不接受市价单,需要改为以最新价提交限价单,并允许订单在盘外时段执行。
| 市场 | 时段 | order_type | submitted_price | outside_rth |
|---|---|---|---|---|
| 港股 / A 股 / 新加坡 | 任意 | MO | 不需要 | 不需要 |
| 美股 | 盘中 | MO | 不需要 | 不需要 |
| 美股 | 盘前 / 盘后 | LO | 最新价 last_done | ANY_TIME |
| 美股 | 夜盘 | LO | 最新价 last_done | OVERNIGHT |
用交易时段取得当天的盘中区间,再和当前美东时间比较即可判断时段。如果 last_done 取不到或为 0,应中止操作,不要提交一笔没有价格的限价单。
务必传入唯一的 client_request_id。平仓请求正是超时后最容易被重试的请求,幂等键可以避免一次重试把仓位平两遍。
预期结果: 委托下单返回 order_id。可以通过当日订单或交易推送跟踪该订单。
完整示例
from datetime import datetime
from uuid import uuid4
from zoneinfo import ZoneInfo
from longbridge.openapi import (
Config, OAuthBuilder, QuoteContext, TradeContext,
Market, TradeSession, OrderType, OrderSide, OutsideRTH, TimeInForceType,
)
oauth = OAuthBuilder("your-client-id").build(lambda url: print("Visit:", url))
config = Config.from_oauth(oauth)
quote_ctx = QuoteContext(config)
trade_ctx = TradeContext(config)
def us_in_regular_session() -> bool:
now = datetime.now(ZoneInfo("America/New_York")).time()
for market in quote_ctx.trading_session():
if market.market != Market.US:
continue
for session in market.trade_sessions:
if session.trade_session == TradeSession.Intraday:
return session.begin_time <= now < session.end_time
return False
def close_position(symbol: str) -> str:
# Step 1: read the position
resp = trade_ctx.stock_positions([symbol])
position = next(
(p for ch in resp.channels for p in ch.positions if p.symbol == symbol),
None,
)
if position is None or position.available_quantity == 0:
raise RuntimeError(f"{symbol}: nothing to close")
# Step 2: opposite side, absolute quantity
side = OrderSide.Sell if position.available_quantity > 0 else OrderSide.Buy
quantity = abs(position.available_quantity)
# Step 3: market order in regular hours, limit order outside US regular hours
order_type, price, outside_rth = OrderType.MO, None, None
if symbol.endswith(".US") and not us_in_regular_session():
quote = quote_ctx.quote([symbol])[0]
if quote.last_done == 0:
raise RuntimeError(f"{symbol}: no last price, cannot close outside regular hours")
order_type, price, outside_rth = OrderType.LO, quote.last_done, OutsideRTH.AnyTime
resp = trade_ctx.submit_order(
symbol,
order_type,
side,
quantity,
TimeInForceType.Day,
submitted_price=price,
outside_rth=outside_rth,
client_request_id=f"close-{symbol}-{uuid4().hex}",
remark="close position",
)
return resp.order_id
print(close_position("TSLA.US"))const {
Config, OAuth, QuoteContext, TradeContext,
Market, TradeSession, OrderType, OrderSide, OutsideRTH, TimeInForceType,
} = require('longbridge')
async function usInRegularSession(quoteCtx) {
const [hour, minute] = new Date()
.toLocaleTimeString('en-US', { timeZone: 'America/New_York', hour12: false })
.split(':')
.map(Number)
const now = hour * 60 + minute
for (const market of await quoteCtx.tradingSession()) {
if (market.market !== Market.US) continue
for (const session of market.tradeSessions) {
if (session.tradeSession === TradeSession.Intraday) {
const begin = session.beginTime.hour * 60 + session.beginTime.minute
const end = session.endTime.hour * 60 + session.endTime.minute
return now >= begin && now < end
}
}
}
return false
}
async function closePosition(quoteCtx, tradeCtx, symbol) {
// Step 1: read the position
const resp = await tradeCtx.stockPositions([symbol])
const position = resp.channels
.flatMap((ch) => ch.positions)
.find((p) => p.symbol === symbol)
if (!position || position.availableQuantity.isZero()) {
throw new Error(`${symbol}: nothing to close`)
}
// Step 2: opposite side, absolute quantity
const side = position.availableQuantity.isPositive() ? OrderSide.Sell : OrderSide.Buy
const quantity = position.availableQuantity.abs()
// Step 3: market order in regular hours, limit order outside US regular hours
let orderType = OrderType.MO
let submittedPrice
let outsideRth
if (symbol.endsWith('.US') && !(await usInRegularSession(quoteCtx))) {
const [quote] = await quoteCtx.quote([symbol])
if (quote.lastDone.isZero()) {
throw new Error(`${symbol}: no last price, cannot close outside regular hours`)
}
orderType = OrderType.LO
submittedPrice = quote.lastDone
outsideRth = OutsideRTH.AnyTime
}
const order = await tradeCtx.submitOrder({
symbol,
orderType,
side,
submittedQuantity: quantity,
timeInForce: TimeInForceType.Day,
submittedPrice,
outsideRth,
clientRequestId: `close-${symbol}-${Date.now()}`,
remark: 'close position',
})
return order.orderId
}
async function main() {
const oauth = await OAuth.build('your-client-id', (_, url) => {
console.log('Open this URL to authorize: ' + url)
})
const config = Config.fromOAuth(oauth)
const quoteCtx = QuoteContext.new(config)
const tradeCtx = TradeContext.new(config)
console.log(await closePosition(quoteCtx, tradeCtx, 'TSLA.US'))
}
main().catch(console.error)// Cargo.toml: longbridge, tokio, rust_decimal, time, chrono, chrono-tz, uuid (v4), anyhow
use std::sync::Arc;
use chrono::Timelike;
use longbridge::{
oauth::OAuthBuilder,
quote::{QuoteContext, TradeSession},
trade::{
GetStockPositionsOptions, OrderSide, OrderType, OutsideRTH, SubmitOrderOptions,
TimeInForceType, TradeContext,
},
Config, Market,
};
use rust_decimal::Decimal;
async fn us_in_regular_session(quote_ctx: &QuoteContext) -> anyhow::Result<bool> {
let now = chrono::Utc::now().with_timezone(&chrono_tz::America::New_York);
let now = time::Time::from_hms(now.hour() as u8, now.minute() as u8, 0)?;
for market in quote_ctx.trading_session().await? {
if market.market != Market::US {
continue;
}
if let Some(session) = market
.trade_sessions
.iter()
.find(|s| s.trade_session == TradeSession::Intraday)
{
return Ok(session.begin_time <= now && now < session.end_time);
}
}
Ok(false)
}
async fn close_position(
quote_ctx: &QuoteContext,
trade_ctx: &TradeContext,
symbol: &str,
) -> anyhow::Result<String> {
// Step 1: read the position
let positions = trade_ctx
.stock_positions(GetStockPositionsOptions::new().symbols([symbol]))
.await?;
let available = positions
.channels
.iter()
.flat_map(|ch| ch.positions.iter())
.find(|p| p.symbol == symbol)
.map(|p| p.available_quantity)
.unwrap_or_default();
if available.is_zero() {
anyhow::bail!("{symbol}: nothing to close");
}
// Step 2: opposite side, absolute quantity
let side = if available.is_sign_positive() { OrderSide::Sell } else { OrderSide::Buy };
let quantity = available.abs();
// Step 3: market order in regular hours, limit order outside US regular hours
let mut opts = SubmitOrderOptions::new(symbol, OrderType::MO, side, quantity, TimeInForceType::Day);
if symbol.ends_with(".US") && !us_in_regular_session(quote_ctx).await? {
let quote = quote_ctx.quote([symbol]).await?.remove(0);
if quote.last_done == Decimal::ZERO {
anyhow::bail!("{symbol}: no last price, cannot close outside regular hours");
}
opts = SubmitOrderOptions::new(symbol, OrderType::LO, side, quantity, TimeInForceType::Day)
.submitted_price(quote.last_done)
.outside_rth(OutsideRTH::AnyTime);
}
let opts = opts
.client_request_id(format!("close-{symbol}-{}", uuid::Uuid::new_v4()))
.remark("close position");
Ok(trade_ctx.submit_order(opts).await?.order_id)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let oauth = OAuthBuilder::new("your-client-id")
.build(|url| println!("Open this URL to authorize: {url}"))
.await?;
let config = Arc::new(Config::from_oauth(oauth));
let (quote_ctx, _) = QuoteContext::new(config.clone());
let (trade_ctx, _) = TradeContext::new(config);
let order_id = close_position("e_ctx, &trade_ctx, "TSLA.US").await?;
println!("order_id: {order_id}");
Ok(())
}package main
import (
"context"
"fmt"
"log"
"strings"
"time"
openapi "github.com/longbridge/openapi-go"
"github.com/longbridge/openapi-go/config"
"github.com/longbridge/openapi-go/oauth"
"github.com/longbridge/openapi-go/quote"
"github.com/longbridge/openapi-go/trade"
"github.com/shopspring/decimal"
)
func usInRegularSession(ctx context.Context, qctx *quote.QuoteContext) (bool, error) {
loc, err := time.LoadLocation("America/New_York")
if err != nil {
return false, err
}
now := time.Now().In(loc)
hhmm := int32(now.Hour()*100 + now.Minute())
sessions, err := qctx.TradingSession(ctx)
if err != nil {
return false, err
}
for _, market := range sessions {
if market.Market != openapi.MarketUS {
continue
}
for _, s := range market.TradeSession {
if s.TradeSession == quote.TradeSessionNormal {
return hhmm >= s.BegTime && hhmm < s.EndTime, nil
}
}
}
return false, nil
}
func closePosition(ctx context.Context, qctx *quote.QuoteContext, tctx *trade.TradeContext, symbol string) (string, error) {
// Step 1: read the position
channels, err := tctx.StockPositions(ctx, []string{symbol})
if err != nil {
return "", err
}
available := decimal.Zero
for _, ch := range channels {
for _, p := range ch.Positions {
if p.Symbol == symbol {
if available, err = decimal.NewFromString(p.AvailableQuantity); err != nil {
return "", err
}
}
}
}
if available.IsZero() {
return "", fmt.Errorf("%s: nothing to close", symbol)
}
// Step 2: opposite side, absolute quantity
side := trade.OrderSideSell
if available.IsNegative() {
side = trade.OrderSideBuy
}
quantity := available.Abs()
// Step 3: market order in regular hours, limit order outside US regular hours
order := &trade.SubmitOrder{
Symbol: symbol,
OrderType: trade.OrderTypeMO,
Side: side,
SubmittedQuantity: uint64(quantity.IntPart()),
TimeInForce: trade.TimeTypeDay,
Remark: "close position",
}
if strings.HasSuffix(symbol, ".US") {
regular, err := usInRegularSession(ctx, qctx)
if err != nil {
return "", err
}
if !regular {
quotes, err := qctx.Quote(ctx, []string{symbol})
if err != nil {
return "", err
}
if len(quotes) == 0 || quotes[0].LastDone == nil || quotes[0].LastDone.IsZero() {
return "", fmt.Errorf("%s: no last price, cannot close outside regular hours", symbol)
}
order.OrderType = trade.OrderTypeLO
order.SubmittedPrice = *quotes[0].LastDone
order.OutsideRTH = trade.OutsideRTHAny
}
}
return tctx.SubmitOrder(ctx, order)
}
func main() {
ctx := context.Background()
o := oauth.New("your-client-id").
OnOpenURL(func(url string) { fmt.Println("Open this URL to authorize:", url) })
if err := o.Build(ctx); err != nil {
log.Fatal(err)
}
conf, err := config.New(config.WithOAuthClient(o))
if err != nil {
log.Fatal(err)
}
qctx, err := quote.NewFromCfg(conf)
if err != nil {
log.Fatal(err)
}
defer qctx.Close()
tctx, err := trade.NewFromCfg(conf)
if err != nil {
log.Fatal(err)
}
defer tctx.Close()
orderID, err := closePosition(ctx, qctx, tctx, "TSLA.US")
if err != nil {
log.Fatal(err)
}
fmt.Println("order_id:", orderID)
}import com.longbridge.*;
import com.longbridge.quote.*;
import com.longbridge.trade.*;
import java.math.BigDecimal;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.UUID;
class Main {
static boolean usInRegularSession(QuoteContext quoteCtx) throws Exception {
LocalTime now = ZonedDateTime.now(ZoneId.of("America/New_York")).toLocalTime();
for (MarketTradingSession market : quoteCtx.getTradingSession().get()) {
if (market.getMarket() != Market.US) continue;
for (TradingSessionInfo session : market.getTradeSessions()) {
if (session.getTradeSession() == TradeSession.Intraday) {
return !now.isBefore(session.getBeginTime()) && now.isBefore(session.getEndTime());
}
}
}
return false;
}
static String closePosition(QuoteContext quoteCtx, TradeContext tradeCtx, String symbol) throws Exception {
// Step 1: read the position
StockPositionsResponse resp = tradeCtx
.getStockPositions(new GetStockPositionsOptions().setSymbols(new String[] { symbol }))
.get();
StockPosition position = Arrays.stream(resp.getChannels())
.flatMap(ch -> Arrays.stream(ch.getPositions()))
.filter(p -> p.getSymbol().equals(symbol))
.findFirst()
.orElse(null);
if (position == null || position.getAvailableQuantity().signum() == 0) {
throw new IllegalStateException(symbol + ": nothing to close");
}
// Step 2: opposite side, absolute quantity
OrderSide side = position.getAvailableQuantity().signum() > 0 ? OrderSide.Sell : OrderSide.Buy;
BigDecimal quantity = position.getAvailableQuantity().abs();
// Step 3: market order in regular hours, limit order outside US regular hours
SubmitOrderOptions opts = new SubmitOrderOptions(symbol, OrderType.MO, side, quantity, TimeInForceType.Day);
if (symbol.endsWith(".US") && !usInRegularSession(quoteCtx)) {
SecurityQuote quote = quoteCtx.getQuote(new String[] { symbol }).get()[0];
if (quote.getLastDone().signum() == 0) {
throw new IllegalStateException(symbol + ": no last price, cannot close outside regular hours");
}
opts = new SubmitOrderOptions(symbol, OrderType.LO, side, quantity, TimeInForceType.Day)
.setSubmittedPrice(quote.getLastDone())
.setOutsideRth(OutsideRTH.AnyTime);
}
opts.setClientRequestId("close-" + symbol + "-" + UUID.randomUUID())
.setRemark("close position");
return tradeCtx.submitOrder(opts).get().orderId;
}
public static void main(String[] args) throws Exception {
try (OAuth oauth = new OAuthBuilder("your-client-id")
.build(url -> System.out.println("Open this URL to authorize: " + url)).get();
Config config = Config.fromOAuth(oauth);
QuoteContext quoteCtx = QuoteContext.create(config);
TradeContext tradeCtx = TradeContext.create(config)) {
System.out.println("order_id: " + closePosition(quoteCtx, tradeCtx, "TSLA.US"));
}
}
}预期结果
函数返回平仓订单的 order_id。订单成交后,股票持仓中该标的的 available_quantity 变为 0,成交记录出现在当日成交中。
注意事项
- 没有「仅平仓」保护。 数量超过持仓时,多出的部分会变成反向开仓(融资账户会直接做空)。数量上限一定要取自刚刚查询到的
available_quantity,不要使用缓存的持仓数。 - 提交前重新查询持仓。 挂单、部分成交、当日买入都会改变
available_quantity。 - 可以用预估接口交叉校验。 预估最大购买数量支持
side=Sell,返回账户最多可卖出的数量。 - 部分平仓。 用
available_quantity乘以想平的比例,再向下取整到标的基础信息中的每手股数。港股碎股需要使用order_type=ODD。 - 组合期权持仓。 通过组合期权下单以相反的
side和相同的腿比例平仓。 - 幂等。 超时后用同一个
client_request_id重试,服务端在 10 分钟内会返回原订单,而不是再创建一笔。