Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
This commit is contained in:
132
internal/backtest/account.go
Normal file
132
internal/backtest/account.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package backtest
|
||||
|
||||
// Account 模拟账户:现金 + 持仓,负责调仓成交与逐日市值结算。
|
||||
// 可转债交易规则:面值 100 元/张,1 手 = 10 张,买入须为整手;
|
||||
// T+0、无印花税,仅计单边佣金与可选滑点。
|
||||
type Account struct {
|
||||
initialCash float64
|
||||
cash float64
|
||||
holdings map[string]float64 // code → 持有张数
|
||||
commission float64
|
||||
slippage float64
|
||||
curve []EquityPoint
|
||||
equity float64 // 最近一次结算的净值
|
||||
}
|
||||
|
||||
// NewAccount 创建初始账户。
|
||||
func NewAccount(p Params) *Account {
|
||||
return &Account{
|
||||
initialCash: p.InitialCash,
|
||||
cash: p.InitialCash,
|
||||
holdings: make(map[string]float64),
|
||||
commission: p.Commission,
|
||||
slippage: p.Slippage,
|
||||
equity: p.InitialCash, // 首个交易日即可建仓
|
||||
}
|
||||
}
|
||||
|
||||
// priceFn 返回标的指定日期(或最近)价格。封装 History.CloseOn 签名。
|
||||
type priceFn func(code, date string) (float64, bool)
|
||||
|
||||
// Rebalance 按目标权重调仓:先卖后买,张数向下取整到整手(10 张)。
|
||||
// 已退市/当日停牌标的按最后已知价结算。
|
||||
func (a *Account) Rebalance(date string, targets []Target, price priceFn) {
|
||||
total := a.equity
|
||||
if total <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 目标持仓权重表。
|
||||
want := make(map[string]float64)
|
||||
for _, t := range targets {
|
||||
if t.Weight > 0 {
|
||||
want[t.Code] = t.Weight
|
||||
}
|
||||
}
|
||||
|
||||
// 先卖出:超出目标权重的部分。
|
||||
for code, qty := range a.holdings {
|
||||
p, ok := price(code, date)
|
||||
if !ok || p <= 0 {
|
||||
continue // 无价格信息,暂不处理该持仓
|
||||
}
|
||||
targetQty := lots(total*want[code]/(p*10)) * 10
|
||||
if targetQty < qty {
|
||||
sell := qty - targetQty
|
||||
proceeds := sell * p * (1 - a.slippage)
|
||||
a.cash += proceeds - proceeds*a.commission
|
||||
if targetQty == 0 {
|
||||
delete(a.holdings, code)
|
||||
} else {
|
||||
a.holdings[code] = targetQty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 再买入:未达目标权重的部分。
|
||||
for code, weight := range want {
|
||||
p, ok := price(code, date)
|
||||
if !ok || p <= 0 {
|
||||
continue
|
||||
}
|
||||
targetQty := lots(total*weight/(p*10)) * 10
|
||||
cur := a.holdings[code]
|
||||
if targetQty <= cur {
|
||||
continue
|
||||
}
|
||||
buy := targetQty - cur
|
||||
if avail := a.buyableLots(p); avail < buy/10 {
|
||||
buy = avail * 10
|
||||
}
|
||||
if buy <= 0 {
|
||||
continue
|
||||
}
|
||||
cost := buy * p * (1 + a.slippage)
|
||||
a.cash -= cost + cost*a.commission
|
||||
a.holdings[code] = cur + buy
|
||||
}
|
||||
}
|
||||
|
||||
// buyableLots 返回当前现金可买的手数(整手向下取整)。
|
||||
func (a *Account) buyableLots(price float64) float64 {
|
||||
unitCost := price * 10 * (1 + a.slippage) * (1 + a.commission)
|
||||
if unitCost <= 0 {
|
||||
return 0
|
||||
}
|
||||
return lots(a.cash / unitCost)
|
||||
}
|
||||
|
||||
// lots 向下取整到整数手。
|
||||
func lots(v float64) float64 {
|
||||
return float64(int(v))
|
||||
}
|
||||
|
||||
// MarkToMarket 按收盘价结算当日净值并追加到净值曲线。
|
||||
func (a *Account) MarkToMarket(date string, price priceFn) {
|
||||
value := a.cash
|
||||
for code, qty := range a.holdings {
|
||||
if p, ok := price(code, date); ok {
|
||||
value += qty * p
|
||||
}
|
||||
}
|
||||
a.equity = value
|
||||
init := a.initialCash
|
||||
if init <= 0 {
|
||||
init = 1
|
||||
}
|
||||
a.curve = append(a.curve, EquityPoint{
|
||||
Date: date,
|
||||
Value: value,
|
||||
Return: value/init - 1,
|
||||
})
|
||||
}
|
||||
|
||||
// Equity 返回最近一次结算的净值。
|
||||
func (a *Account) Equity() float64 {
|
||||
return a.equity
|
||||
}
|
||||
|
||||
// Curve 返回累计净值曲线。
|
||||
func (a *Account) Curve() []EquityPoint {
|
||||
return a.curve
|
||||
}
|
||||
Reference in New Issue
Block a user