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.
163 lines
4.1 KiB
163 lines
4.1 KiB
package backtest |
|
|
|
import ( |
|
"fmt" |
|
"sig-pub/pkg/trade" |
|
"sig-pub/pkg/types" |
|
"sig-pub/pkg/types/decimals" |
|
"sig-pub/pkg/utils/conver" |
|
"sig-pub/pkg/utils/lang" |
|
"time" |
|
) |
|
|
|
type BacktestAccount struct { |
|
trade.ITradeAccount |
|
cash float64 |
|
positions map[int64]*trade.Position |
|
trades map[int64]*trade.Trade |
|
closeTrades map[int64]*trade.Trade |
|
simulator *TradeSimulator |
|
|
|
profit float64 |
|
winningTrades int |
|
losingTrades int |
|
fee float64 |
|
} |
|
|
|
func NewBacktestAccount(cash float64, simulator *TradeSimulator) *BacktestAccount { |
|
return &BacktestAccount{ |
|
cash: cash, |
|
positions: make(map[int64]*trade.Position), |
|
trades: make(map[int64]*trade.Trade), |
|
closeTrades: make(map[int64]*trade.Trade), |
|
simulator: simulator, |
|
} |
|
} |
|
|
|
// CurrentEquity 根据当前价格对仓位进行 mark-to-market,返回账户净值 |
|
func (a *BacktestAccount) CurrentEquity(price float64) float64 { |
|
equity := a.cash |
|
for _, p := range a.positions { |
|
switch p.Side { |
|
case types.SideLong: |
|
equity += (price - p.EntryPx) * p.Qty |
|
case types.SideShort: |
|
equity += (p.EntryPx - price) * p.Qty |
|
} |
|
} |
|
return equity |
|
} |
|
|
|
// CurrentExposure 返回当前仓位的名义总敞口(绝对值) |
|
func (a *BacktestAccount) CurrentExposure() float64 { |
|
var sum float64 |
|
for _, p := range a.positions { |
|
sum += (p.Qty * p.EntryPx) |
|
} |
|
return sum |
|
} |
|
|
|
// CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct) |
|
// func (a *BacktestAccount) CanOpen(side types.Side, qty, price float64) bool { |
|
// if qty <= 0 || price <= 0 { |
|
// return false |
|
// } |
|
// equity := a.CurrentEquity(price) |
|
// if equity <= 0 { |
|
// return false |
|
// } |
|
// notional := math.Abs(qty * price) |
|
// // 单仓位限制 |
|
// if a.MaxPosPct > 0 { |
|
// if notional > a.MaxPosPct*equity { |
|
// return false |
|
// } |
|
// } |
|
// // 总敞口限制 |
|
// if a.MaxExposurePct > 0 { |
|
// if a.CurrentExposure(price)+notional > a.MaxExposurePct*equity { |
|
// return false |
|
// } |
|
// } |
|
// return true |
|
// } |
|
|
|
// 获取未平仓交易单 |
|
func (a *BacktestAccount) OpenPositions() map[int64]*trade.Position { |
|
return a.positions |
|
} |
|
|
|
// 获取未平仓交易单数 |
|
func (a *BacktestAccount) CountOpenPositions() int { |
|
return len(a.positions) |
|
} |
|
|
|
// 订单下单 |
|
func (a *BacktestAccount) TradeOrder(ta trade.TradeArg) (ok bool, cause trade.Cause, err error) { |
|
order, ok := a.simulator.ExecuteMarket(ta.Side, ta.Qty, ta.Price, ta.Time) |
|
if !ok { |
|
return |
|
} |
|
|
|
// apply cash/position, short side also need earnest money |
|
cost := order.Price*order.Qty + order.Fee |
|
if cost > a.cash { |
|
ok = false |
|
return |
|
} |
|
a.cash -= cost |
|
// open/add position |
|
pos := &trade.Position{TradeId: order.Id, Side: order.Side, Qty: order.Qty, EntryPx: order.Price, EntryTs: order.Time, PeakPx: order.Price, Fee: order.Fee} |
|
a.positions[order.Id] = pos |
|
a.trades[order.Id] = order |
|
ok = true |
|
return |
|
} |
|
|
|
// 将仓位进行平仓 |
|
func (a *BacktestAccount) ClosePosition(pos *trade.Position, kline types.Kline, cause trade.Cause) (err error) { |
|
closeSide := lang.Ternary(pos.Side == types.SideLong, types.SideShort, types.SideLong) |
|
|
|
closePrice := decimals.MustToFloat64(kline.Close) |
|
t, ok := a.simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, kline.Ts) |
|
if !ok { |
|
err = fmt.Errorf("close position error: %#v", pos) |
|
return |
|
} |
|
t.CloseCause = cause |
|
|
|
// apply cash change |
|
// calc profit |
|
var receive, profit float64 |
|
if pos.Side == types.SideLong { |
|
profit = t.Qty*(t.Price-pos.EntryPx) - t.Fee - pos.Fee |
|
receive = t.Price*pos.Qty - t.Fee |
|
} else { |
|
profit = (pos.EntryPx - t.Price) * t.Qty |
|
receive = pos.EntryPx*pos.Qty + profit - t.Fee |
|
profit = profit - t.Fee - pos.Fee |
|
} |
|
a.cash += receive |
|
a.profit += profit |
|
|
|
if profit > 0 { |
|
a.winningTrades++ |
|
} else { |
|
a.losingTrades++ |
|
} |
|
a.fee += t.Fee |
|
|
|
// remove position |
|
delete(a.positions, pos.TradeId) |
|
a.closeTrades[t.Id] = t |
|
|
|
if trade, ok := a.trades[pos.TradeId]; ok { |
|
trade.Pnl = profit |
|
trade.ClosePrice = closePrice |
|
trade.CloseFee = t.Fee |
|
trade.CloseTs = kline.Ts |
|
trade.CloseCause = cause |
|
trade.HoldTime = conver.TimeDurationFormat(time.Duration(trade.CloseTs-trade.Time)*time.Millisecond, ".") |
|
} |
|
return |
|
}
|
|
|