Paper trading readiness에서 API/worker/CLI가 같은 protobuf 계약으로 paper state를 시작하고 조회할 수 있어야 한다. Headless 운영 경로를 먼저 닫기 위해 contract, worker runtime, API forwarding, CLI scenario, client parser map과 검증 artifact를 함께 반영한다.
112 lines
3.7 KiB
Go
112 lines
3.7 KiB
Go
package socket
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
|
"git.toki-labs.com/toki/alt/packages/domain/backtest"
|
|
"git.toki-labs.com/toki/alt/packages/domain/market"
|
|
"git.toki-labs.com/toki/alt/services/worker/internal/papertrading"
|
|
)
|
|
|
|
// paper_mapping.go owns paper-trading-specific conversions between the ALT
|
|
// contract and the worker-owned paper runtime. As with backtest mapping, keeping
|
|
// proto<->domain conversion in the worker honours the rail design where the
|
|
// worker owns runtime shapes and the API stays a thin pass-through.
|
|
|
|
// startPaperRequestFromProto validates an inbound StartPaperTradingRequest and
|
|
// converts it into the paper service start request. Validation lives next to the
|
|
// conversion so a malformed request never reaches the runtime.
|
|
func startPaperRequestFromProto(req *altv1.StartPaperTradingRequest) (papertrading.StartRequest, error) {
|
|
if req.GetAccountId() == "" {
|
|
return papertrading.StartRequest{}, fmt.Errorf("account_id is required")
|
|
}
|
|
spec, err := runSpecFromProto(req.GetSpec())
|
|
if err != nil {
|
|
return papertrading.StartRequest{}, err
|
|
}
|
|
cash, err := priceFromProto(req.GetStartingCash())
|
|
if err != nil {
|
|
return papertrading.StartRequest{}, fmt.Errorf("starting_cash: %w", err)
|
|
}
|
|
return papertrading.StartRequest{
|
|
AccountID: backtest.PaperAccountID(req.GetAccountId()),
|
|
Spec: spec,
|
|
StartingCash: cash,
|
|
}, nil
|
|
}
|
|
|
|
// priceFromProto converts a contract Price into the domain Price, requiring a
|
|
// concrete currency and amount so paper cash math never runs on an empty value.
|
|
func priceFromProto(price *altv1.Price) (market.Price, error) {
|
|
if price == nil {
|
|
return market.Price{}, fmt.Errorf("price is required")
|
|
}
|
|
currency, err := currencyFromProto(price.GetCurrency())
|
|
if err != nil {
|
|
return market.Price{}, err
|
|
}
|
|
if price.GetAmount().GetValue() == "" {
|
|
return market.Price{}, fmt.Errorf("amount is required")
|
|
}
|
|
return market.Price{
|
|
Currency: currency,
|
|
Amount: market.Decimal{Value: price.GetAmount().GetValue()},
|
|
}, nil
|
|
}
|
|
|
|
func currencyFromProto(c altv1.Currency) (market.Currency, error) {
|
|
switch c {
|
|
case altv1.Currency_CURRENCY_KRW:
|
|
return market.CurrencyKRW, nil
|
|
case altv1.Currency_CURRENCY_USD:
|
|
return market.CurrencyUSD, nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported currency %q", c.String())
|
|
}
|
|
}
|
|
|
|
// paperStateToProto converts a paper service State into the contract state,
|
|
// reusing the shared backtest run/position/trade/equity payload shapes.
|
|
func paperStateToProto(accountID backtest.PaperAccountID, state papertrading.State) *altv1.PaperTradingState {
|
|
return &altv1.PaperTradingState{
|
|
AccountId: string(accountID),
|
|
Run: runToProto(state.Run),
|
|
Cash: priceToProto(state.Cash),
|
|
Positions: paperPositionsToProto(state.Positions),
|
|
Fills: fillsToProto(state.Fills),
|
|
EquityCurve: equityCurveToProto(state.EquityCurve),
|
|
}
|
|
}
|
|
|
|
func paperPositionsToProto(positions []backtest.Position) []*altv1.BacktestPosition {
|
|
if len(positions) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]*altv1.BacktestPosition, len(positions))
|
|
for i, p := range positions {
|
|
out[i] = &altv1.BacktestPosition{
|
|
InstrumentId: string(p.InstrumentID),
|
|
Quantity: quantityToProto(p.Quantity),
|
|
LastPrice: priceToProto(p.LastPrice),
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func fillsToProto(fills []backtest.Fill) []*altv1.BacktestTrade {
|
|
if len(fills) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]*altv1.BacktestTrade, len(fills))
|
|
for i, f := range fills {
|
|
out[i] = &altv1.BacktestTrade{
|
|
InstrumentId: string(f.InstrumentID),
|
|
Side: string(f.Side),
|
|
Quantity: quantityToProto(f.Quantity),
|
|
Price: priceToProto(f.Price),
|
|
TimestampUnixMs: timeToUnixMs(f.Timestamp),
|
|
}
|
|
}
|
|
return out
|
|
}
|