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.
142 lines
3.8 KiB
142 lines
3.8 KiB
package backtest |
|
|
|
import ( |
|
"math" |
|
"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 == types.SideBuy { |
|
equity += (price - p.EntryPx) * p.Qty |
|
} else if p.Side == types.SideSell { |
|
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 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, 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 == types.SideBuy { |
|
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 == types.SideSell { |
|
// 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 types.Side |
|
if pos.Side == types.SideBuy { |
|
closeSide = types.SideSell |
|
} else { |
|
closeSide = types.SideBuy |
|
} |
|
|
|
t = a.Simulator.ExecuteMarket(closeSide, pos.Qty, kline, ts) |
|
t.CloseCause = cause |
|
// apply cash change |
|
if closeSide == types.SideSell { |
|
// 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 |
|
}
|
|
|