Files
WarrenZhou e28bfec957 Initial commit: 可转债轮动分析系统
- 实时行情:东财push2主源 + 腾讯批量报价备用源(自动切换)
- 历史K线:东财push2his主源 + 腾讯web子域备用源(含熔断机制)
- 增量同步:支持 -incr/-since 参数按存档日期增量入库
- 策略回测:净值曲线/10指标/基准对比,含基本面过滤
- 用户认证:用户名密码 + 邮箱验证码双登录方式
- 前端:三Tab SPA(实时行情/策略回测/数据管理)
2026-08-17 20:23:22 +08:00

133 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}