- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换) - 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制) - 增量同步:支持 -incr/-since 参数按存档日期增量入库 - 策略回测:净值曲线/10指标/基准对比,含基本面过滤 - 用户认证:用户名密码 + 邮箱验证码双登录方式 - 前端:三Tab SPA(实时行情/策略回测/数据管理)
133 lines
3.3 KiB
Go
133 lines
3.3 KiB
Go
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
|
||
}
|