Initial commit: 可转债轮动分析系统

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

View 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
}