You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
4.7 KiB
175 lines
4.7 KiB
package backtest |
|
|
|
import ( |
|
"fmt" |
|
"sig-pub/pkg/trade" |
|
"sig-pub/pkg/types" |
|
"sig-pub/pkg/utils/collect" |
|
"sig-pub/pkg/zlog" |
|
) |
|
|
|
type BacktestTradeAccount struct { |
|
trader *TradeSimulator // 下单器 |
|
traderOrderId int64 // 订单id递增 |
|
cash float64 // 可用资金 |
|
positions map[string]*trade.Position // 仓位信息,key: symbol, value: qty(不能持有同一交易产品反方向单, 交易中不能改变杠杆) |
|
trades map[int64]*trade.TradeOrder // 所有交易单 |
|
openTrades map[string][]int64 // 未平仓交易单,key: symbol, value: tradeId |
|
closeTrades []int64 // 平仓交易单 |
|
} |
|
|
|
func NewBacktestTradeAccount(cash float64) *BacktestTradeAccount { |
|
return &BacktestTradeAccount{ |
|
trader: NewTradeSimulator(0.0005, 0.0008), |
|
traderOrderId: 1, |
|
cash: cash, |
|
positions: make(map[string]*trade.Position), |
|
} |
|
} |
|
|
|
// Equity 账户净值 |
|
func (a *BacktestTradeAccount) GetCash() float64 { |
|
return a.cash |
|
} |
|
|
|
// Equity 账户净值 |
|
func (a *BacktestTradeAccount) GetEquity() float64 { |
|
return a.cash |
|
} |
|
|
|
// IsSymbolOpen 指定交易对是否有持仓 |
|
func (a *BacktestTradeAccount) IsSymbolOpen(symbol string) bool { |
|
return a.positions[symbol] != nil |
|
} |
|
|
|
func (a *BacktestTradeAccount) GetOpenTradeInsts() (instIds []string) { |
|
for instId := range a.openTrades { |
|
instIds = append(instIds, instId) |
|
} |
|
return |
|
} |
|
|
|
func (a *BacktestTradeAccount) GetOpenTrades(instId string) []*trade.TradeOrder { |
|
var trades []*trade.TradeOrder |
|
for _, tradeId := range a.openTrades[instId] { |
|
trades = append(trades, a.trades[tradeId]) |
|
} |
|
return trades |
|
} |
|
|
|
// GetSymbolOpenPosition 获取指定交易对的仓位信息 |
|
func (a *BacktestTradeAccount) GetSymbolOpenPosition(symbol string) *trade.Position { |
|
return a.positions[symbol] |
|
} |
|
|
|
// MarketOrder 市价下单 |
|
func (a *BacktestTradeAccount) MarketOrder(ticket trade.TradeTicket) (order *trade.TradeOrder, err error) { |
|
instId := ticket.InstId |
|
if pos, ok := a.positions[instId]; ok { |
|
// 检查反方向单 |
|
if pos.Side != ticket.Side { |
|
err = fmt.Errorf("cannot open opposite side position") |
|
return |
|
} |
|
// 同方向杠杆倍数 |
|
if pos.Leverage != ticket.Leverage { |
|
err = fmt.Errorf("cannot change leverage on existing position") |
|
return |
|
} |
|
} |
|
|
|
cost, order := a.trader.ExecuteMarket(a.traderOrderId, instId, ticket) |
|
if cost > a.cash { |
|
err = fmt.Errorf("insufficient cash") |
|
return |
|
} |
|
a.traderOrderId++ |
|
a.cash -= cost |
|
a.trades[order.TradeId] = order |
|
a.openTrades[instId] = append(a.openTrades[instId], order.TradeId) |
|
|
|
// open/add position |
|
pos, ok := a.positions[instId] |
|
if !ok { |
|
pos = &trade.Position{ |
|
InstId: order.InstId, |
|
Side: order.Side, |
|
Qty: order.Qty, |
|
Leverage: order.Leverage, |
|
EntryPx: order.Price, |
|
EntryTs: order.Ctime, |
|
PeakPx: order.Price, |
|
} |
|
a.positions[instId] = pos |
|
} else { |
|
totalQty := pos.Qty + order.Qty |
|
pos.EntryPx = (pos.EntryPx*pos.Qty + order.Price*order.Qty) / totalQty |
|
pos.Qty = totalQty |
|
if pos.Side == types.SideLong && order.Price < pos.PeakPx { |
|
pos.PeakPx = order.Price |
|
} |
|
if pos.Side == types.SideShort && order.Price > pos.PeakPx { |
|
pos.PeakPx = order.Price |
|
} |
|
} |
|
return |
|
} |
|
|
|
// CloseTradeOrder 订单订单平仓 |
|
func (a *BacktestTradeAccount) CloseTradeOrder(ticket trade.TradeTicket) (err error) { |
|
instId := ticket.InstId |
|
// 检查仓位 |
|
pos, ok := a.positions[instId] |
|
if !ok { |
|
err = fmt.Errorf("no open position for symbol: %s", instId) |
|
return |
|
} |
|
if pos.Side == ticket.Side { |
|
err = fmt.Errorf("cannot close position with same side trade") |
|
return |
|
} |
|
if ticket.Qty > pos.Qty { |
|
err = fmt.Errorf("close quantity exceeds position quantity") |
|
return |
|
} |
|
|
|
cost, order := a.trader.ExecuteMarket(a.traderOrderId, instId, ticket) |
|
a.traderOrderId++ |
|
a.cash += cost |
|
a.trades[order.TradeId] = order |
|
a.closeTrades = append(a.closeTrades, order.TradeId) |
|
if len(ticket.TradesId) > 0 { |
|
if trades, ok := a.openTrades[instId]; ok { |
|
// remove open trade order |
|
removes := collect.Remove(&trades, func(tradeId int64) bool { |
|
return collect.In(tradeId, ticket.TradesId...) |
|
}) |
|
a.openTrades[instId] = trades |
|
if removes != len(ticket.TradesId) { |
|
zlog.Warningf("close ticket tradeId not exists: instId=%s, tradeId=%#v", ticket.InstId, ticket.TradesId) |
|
} |
|
} |
|
if len(a.openTrades[instId]) == 0 { |
|
delete(a.openTrades, instId) |
|
} |
|
} |
|
|
|
// update position |
|
pos.Qty -= ticket.Qty |
|
if pos.Qty <= 0 { |
|
delete(a.positions, instId) |
|
if _, ok := a.openTrades[instId]; ok { |
|
zlog.Warningf("close all position trades: %s", instId) |
|
} |
|
} |
|
|
|
// todo 平仓单统计 |
|
|
|
return |
|
} |
|
|
|
// CloseOpsition 仓位平仓 |
|
func (a *BacktestTradeAccount) CloseOpsition(pos *trade.Position) (err error) { |
|
|
|
return |
|
}
|
|
|