feat: 完成模拟交易闭环、App 与 A 股行情接入
Checks / verify (push) Failing after 9m52s

统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
2026-10-08 22:58:52 +08:00
parent bffdb3858b
commit 92329ddce2
145 changed files with 18593 additions and 587 deletions
+55
View File
@@ -0,0 +1,55 @@
import sys, json
from decimal import Decimal
def cents(value):
n = Decimal(value) * 100
if n != n.to_integral_value() or n <= 0:
raise ValueError('invalid price')
return int(n)
def money(value):
return ('-' if value < 0 else '') + str(abs(value)//100) + '.' + str(abs(value)%100).zfill(2)
def run(data):
p = data['parameters']
bars = data['bars']
cash = int(Decimal(data['initial_cash'])*100)
initial = cash
quantity, cost, peak, drawdown, previous = 0, 0, cash, 0, False
trades, equity = [], []
closes = [cents(b['close']) for b in bars]
for i, bar in enumerate(bars):
# Only preceding completed bars generate a signal. Execute at next open.
if i >= p['long_window']:
short = sum(closes[i-p['short_window']:i]) * p['long_window']
long = sum(closes[i-p['long_window']:i]) * p['short_window']
bullish = short > long
open_price = cents(bar['open'])
if bullish and not previous and quantity == 0:
price = (open_price*(10000+p['slippage_bps'])+9999)//10000
unit = p['lot_size']
units = cash*10000//(price*unit*(10000+p['fee_bps']))
quantity = units*unit
gross = quantity*price
fee = (gross*p['fee_bps']+9999)//10000
if quantity:
cash -= gross+fee
cost = gross+fee
trades.append({'date':bar['date'],'side':'BUY','quantity':str(quantity),'price':money(price),'fee':money(fee),'realized_pnl':None})
elif not bullish and previous and quantity:
price = max(1,open_price*(10000-p['slippage_bps'])//10000)
gross = quantity*price
fee = (gross*p['fee_bps']+9999)//10000
cash += gross-fee
trades.append({'date':bar['date'],'side':'SELL','quantity':str(quantity),'price':money(price),'fee':money(fee),'realized_pnl':money(gross-fee-cost)})
quantity, cost = 0, 0
previous = bullish
value = cash+quantity*closes[i]
peak = max(peak,value)
drawdown = max(drawdown,(peak-value)*10000//peak)
equity.append({'date':bar['date'],'equity':money(value)})
final = cash+quantity*closes[-1]
return {'engine_version':'sma_cross_v1','final_equity':money(final),'cash':money(cash),'remaining_quantity':str(quantity),'total_return_bps':(final-initial)*10000//initial,'max_drawdown_bps':drawdown,'trades':trades,'equity_curve':equity,'valuation':'Final close valuation; open positions are not liquidated.'}
if __name__ == '__main__':
print(json.dumps(run(json.load(sys.stdin)),separators=(',',':')))
+46
View File
@@ -0,0 +1,46 @@
// Package research executes the embedded Python research engine with bounded inputs.
package research
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"errors"
"os"
"os/exec"
"time"
)
//go:embed backtest.py
var script string
type Engine struct{ executable string }
func New(executable string) *Engine { return &Engine{executable} }
func (e *Engine) Execute(ctx context.Context, input json.RawMessage) (json.RawMessage, error) {
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, e.executable, "-I", "-c", script)
// No application credentials or Python startup configuration reach the child.
cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "LANG=en_US.UTF-8"}
cmd.Stdin = bytes.NewReader(input)
var output boundedBuffer
cmd.Stdout = &output
if err := cmd.Run(); err != nil {
return nil, errors.New("Python research execution failed; check Python installation and input data")
}
if !json.Valid(output.Bytes()) {
return nil, errors.New("invalid research result")
}
return json.RawMessage(output.Bytes()), nil
}
type boundedBuffer struct{ bytes.Buffer }
func (b *boundedBuffer) Write(p []byte) (int, error) {
if b.Len()+len(p) > 2<<20 {
return 0, errors.New("research output exceeds limit")
}
return b.Buffer.Write(p)
}
+55
View File
@@ -0,0 +1,55 @@
package research
import (
"context"
"encoding/json"
"git.sundynix.cn/Blizzard/Veyra/internal/model"
"os/exec"
"testing"
)
func TestNoLookaheadAndExactCash(t *testing.T) {
python, e := exec.LookPath("python3")
if e != nil {
t.Fatal("Python 3 is required for research tests")
}
values := []string{"10.00", "9.00", "8.00", "20.00", "21.00", "7.00", "6.00", "5.00"}
bars := make([]model.HistoricalBar, len(values))
for i, p := range values {
bars[i] = model.HistoricalBar{Date: []string{"2025-01-01", "2025-01-02", "2025-01-03", "2025-01-04", "2025-01-05", "2025-01-06", "2025-01-07", "2025-01-08"}[i], Open: p, High: p, Low: p, Close: p}
}
input := model.ResearchInput{BacktestInput: model.BacktestInput{InitialCash: "10000.00", Bars: bars}, Parameters: model.StrategyParameters{ShortWindow: 2, LongWindow: 3, FeeBPS: 10, SlippageBPS: 0, LotSize: 100}}
data, _ := json.Marshal(input)
result, e := New(python).Execute(context.Background(), data)
if e != nil {
t.Fatal(e)
}
var got struct {
FinalEquity string `json:"final_equity"`
Trades []struct{ Date, Side, Quantity, Price, Fee string }
}
if e := json.Unmarshal(result, &got); e != nil {
t.Fatal(e)
}
if len(got.Trades) != 2 || got.Trades[0].Date != "2025-01-05" || got.Trades[0].Side != "BUY" || got.Trades[0].Quantity != "400" || got.Trades[0].Fee != "8.40" || got.Trades[1].Date != "2025-01-07" || got.FinalEquity != "3989.20" {
t.Fatalf("unexpected result: %s", result)
}
// Changing the current close cannot change a trade at that day's open.
input.Bars[4].Close = "1.00"
data, _ = json.Marshal(input)
result, e = New(python).Execute(context.Background(), data)
if e != nil {
t.Fatal(e)
}
var changed struct {
Trades []struct{ Date, Side, Quantity, Price, Fee string }
}
if json.Unmarshal(result, &changed) != nil || len(changed.Trades) == 0 || changed.Trades[0] != got.Trades[0] {
t.Fatal("current close leaked into open execution")
}
}
func TestMissingInterpreter(t *testing.T) {
if _, e := New("/not/an/interpreter").Execute(context.Background(), json.RawMessage(`{}`)); e == nil {
t.Fatal("missing Python must fail")
}
}