统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
@@ -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=(',',':')))
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user