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.
 
 

143 lines
3.8 KiB

package backtest
import (
"math"
"sig-pub/api/pb"
"sig-pub/pkg/types"
"sig-pub/pkg/types/decimals"
)
type Account struct {
Cash float64
Positions []*Position
Trades []Trade
MaxPosPct float64 // 最大仓位占比
MaxExposurePct float64 // 最大总敞口占比
MaxLots float64 // 最大手数/数量 (optional)
Simulator *Simulator
}
func NewAccount(cash float64, sim *Simulator) *Account {
return &Account{Cash: cash, Simulator: sim, MaxPosPct: 1.0, MaxExposurePct: 1.0}
}
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 {
if p.Side == pb.Side_BUY {
equity += (price - p.EntryPx) * p.Qty
} else if p.Side == pb.Side_SELL {
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
}
// CanOpen 判断在给定价格下是否可以开仓(基于 MaxPosPct 和 MaxExposurePct)
func (a *Account) CanOpen(side pb.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 pb.Side, qty float64, klineTs int64, kline types.Kline) (t Trade, ok bool) {
// risk check before executing
price := decimals.MustToFloat64(kline.Close)
if !a.CanOpen(side, qty, price) {
return t, false
}
trade := a.Simulator.ExecuteMarket(side, qty, kline, klineTs)
// apply cash/position
if side == pb.Side_BUY {
cost := trade.Price*qty + trade.Fee
if cost > a.Cash {
return trade, false
}
a.Cash -= cost
// open/add position
pos := &Position{Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: klineTs, PeakPx: trade.Price}
a.Positions = append(a.Positions, pos)
} else if side == pb.Side_SELL {
// simplify: allow short by increasing cash
receive := trade.Price*qty - trade.Fee
a.Cash += receive
pos := &Position{Side: side, Qty: qty, EntryPx: trade.Price, EntryTs: klineTs, PeakPx: trade.Price}
a.Positions = append(a.Positions, pos)
}
a.Trades = append(a.Trades, trade)
return trade, true
}
// ClosePosition 根据索引平仓(全部平仓该仓位)
func (a *Account) ClosePosition(index int, kline types.Kline, ts int64, cause string) (t Trade, ok bool) {
if index < 0 || index >= len(a.Positions) {
return t, false
}
pos := a.Positions[index]
if pos == nil {
return t, false
}
// determine close side (opposite)
var closeSide pb.Side
if pos.Side == pb.Side_BUY {
closeSide = pb.Side_SELL
} else {
closeSide = pb.Side_BUY
}
t = a.Simulator.ExecuteMarket(closeSide, pos.Qty, kline, ts)
t.Cause = cause
// apply cash change
if closeSide == pb.Side_SELL {
// selling a long position -> receive cash
receive := t.Price*pos.Qty - t.Fee
a.Cash += receive
} else {
// buying to close a short -> pay cash
cost := t.Price*pos.Qty + t.Fee
a.Cash -= cost
}
// remove position
a.Positions = append(a.Positions[:index], a.Positions[index+1:]...)
a.Trades = append(a.Trades, t)
return t, true
}