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

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
}