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.
 
 

177 lines
4.5 KiB

package backtest
import (
"math"
"sig-pub/pkg/types"
"sig-pub/pkg/types/decimals"
"sig-pub/pkg/utils/collect"
"sig-pub/pkg/utils/conver"
"time"
)
type Account struct {
Cash float64
Positions []*Position
Trades []*Trade // 交易订单利润分布,回测结果echart折线显示,评估止盈止损策略效果
CloseTrades []*Trade
MaxPosPct float64 // 最大仓位占比
MaxExposurePct float64 // 最大总敞口占比
MaxLots float64 // 最大手数/数量 (optional)
Simulator *Simulator
Stat *TradeStat // 交易统计
Profit float64
}
func NewAccount(cash float64, sim *Simulator) *Account {
return &Account{
Cash: cash, Simulator: sim, MaxPosPct: 1.0, MaxExposurePct: 1.0,
Stat: &TradeStat{},
}
}
func (a *Account) SetRiskLimits(maxPosPct, maxExposurePct float64) {
if maxPosPct > 0 {
a.MaxPosPct = maxPosPct
}
if maxExposurePct > 0 {
a.MaxExposurePct = maxExposurePct
}
}
// CurrentEquity 根据当前价格对仓位进行 mark-to-market,返回账户净值
func (a *Account) 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 *Account) CurrentExposure(price float64) float64 {
var sum float64
for _, p := range a.Positions {
sum += math.Abs(p.Qty * price)
}
return sum
}
func (a *Account) PositionCost() float64 {
var sum float64
for _, p := range a.Positions {
sum += math.Abs(p.Qty * p.EntryPx)
}
return sum
}
// CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct)
func (a *Account) 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
}
// ApplyMarketOrder 直接用市价下单(简化),qty为基础货币数量
func (a *Account) ApplyMarketOrder(side types.Side, qty float64, price float64, ts int64) (t *Trade, ok bool) {
if !a.CanOpen(side, qty, price) {
return t, false
}
trade, ok := a.Simulator.ExecuteMarket(side, qty, price, ts)
if !ok {
return
}
// apply cash/position, short side also need earnest money
cost := trade.Price*qty + trade.Fee
if cost > a.Cash {
return trade, false
}
a.Cash -= cost
// open/add position
pos := &Position{TradeId: trade.Id, Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: ts, PeakPx: trade.Price, Fee: trade.Fee}
a.Positions = append(a.Positions, pos)
a.Trades = append(a.Trades, trade)
{
a.Stat.TotalTrades++
a.Stat.Fee += trade.Fee
}
return trade, true
}
// ClosePosition 根据索引平仓(全部平仓该仓位)
func (a *Account) ClosePosition(index int, pos *Position, kline types.Kline, ts int64, cause string) (t *Trade, ok bool) {
// determine close side (opposite)
var closeSide types.Side
if pos.Side == types.SideLong {
closeSide = types.SideShort
} else {
closeSide = types.SideLong
}
closePrice := decimals.MustToFloat64(kline.Close)
t, ok = a.Simulator.ExecuteMarket(closeSide, pos.Qty, closePrice, ts)
if !ok {
return t, false
}
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.Stat.WinningTrades++
} else {
a.Stat.LosingTrades++
}
a.Stat.Fee += t.Fee
// remove position
a.Positions = append(a.Positions[:index], a.Positions[index+1:]...)
a.CloseTrades = append(a.CloseTrades, t)
trade, ok := collect.Find(a.Trades, func(t *Trade) bool { return t.Id == pos.TradeId })
if 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.Ts)*time.Millisecond, ".")
}
return t, true
}