Paper trading readiness에서 API/worker/CLI가 같은 protobuf 계약으로 paper state를 시작하고 조회할 수 있어야 한다. Headless 운영 경로를 먼저 닫기 위해 contract, worker runtime, API forwarding, CLI scenario, client parser map과 검증 artifact를 함께 반영한다.
208 lines
5.3 KiB
Go
208 lines
5.3 KiB
Go
package operator
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// OutputFormat selects how runner events are rendered to stdout.
|
|
type OutputFormat string
|
|
|
|
const (
|
|
// OutputText emits grep-friendly key=value lines (the default).
|
|
OutputText OutputFormat = "text"
|
|
// OutputJSONL emits one JSON object per line.
|
|
OutputJSONL OutputFormat = "jsonl"
|
|
)
|
|
|
|
// noCount marks a step event that has no market result count (for example a
|
|
// hello handshake or a transport failure).
|
|
const noCount = -1
|
|
|
|
// StepEvent is one machine-readable record describing a single executed step.
|
|
type StepEvent struct {
|
|
Scenario string
|
|
Step string
|
|
Action string
|
|
Status string
|
|
Count int
|
|
ErrorCode string
|
|
ErrorMessage string
|
|
|
|
// Backtest-specific output fields
|
|
RunID string
|
|
RunStatus string
|
|
StartingCash string
|
|
EndingEquity string
|
|
TotalReturn string
|
|
TradeCount int
|
|
|
|
// Import-specific output fields
|
|
Provider string
|
|
InstrumentCount int
|
|
BarCount int
|
|
|
|
// Paper-trading-specific output fields. They are only rendered when
|
|
// AccountID is set, so non-paper steps never emit them.
|
|
AccountID string
|
|
Cash string
|
|
PositionCount int
|
|
FillCount int
|
|
}
|
|
|
|
// RunSummary is the final record describing a whole scenario run.
|
|
type RunSummary struct {
|
|
Scenario string
|
|
Status string
|
|
Steps int
|
|
Passed int
|
|
ExitCode int
|
|
}
|
|
|
|
// Writer renders runner events to an io.Writer in the selected format. stdout
|
|
// carries these machine-readable lines; scripts grep them for status keys.
|
|
type Writer struct {
|
|
w io.Writer
|
|
format OutputFormat
|
|
}
|
|
|
|
// NewWriter returns a Writer for the given format, defaulting to text.
|
|
func NewWriter(w io.Writer, format OutputFormat) *Writer {
|
|
if format == "" {
|
|
format = OutputText
|
|
}
|
|
return &Writer{w: w, format: format}
|
|
}
|
|
|
|
// WriteStep renders one executed step.
|
|
func (o *Writer) WriteStep(ev StepEvent) {
|
|
if o.format == OutputJSONL {
|
|
fields := map[string]any{
|
|
"type": "step",
|
|
"scenario": ev.Scenario,
|
|
"step": ev.Step,
|
|
"action": ev.Action,
|
|
"status": ev.Status,
|
|
}
|
|
if ev.Count >= 0 {
|
|
fields["count"] = ev.Count
|
|
}
|
|
if ev.ErrorCode != "" {
|
|
fields["error_code"] = ev.ErrorCode
|
|
}
|
|
if ev.ErrorMessage != "" {
|
|
fields["error_message"] = ev.ErrorMessage
|
|
}
|
|
if ev.RunID != "" {
|
|
fields["run_id"] = ev.RunID
|
|
}
|
|
if ev.RunStatus != "" {
|
|
fields["run_status"] = ev.RunStatus
|
|
}
|
|
if ev.StartingCash != "" {
|
|
fields["starting_cash"] = ev.StartingCash
|
|
}
|
|
if ev.EndingEquity != "" {
|
|
fields["ending_equity"] = ev.EndingEquity
|
|
}
|
|
if ev.TotalReturn != "" {
|
|
fields["total_return"] = ev.TotalReturn
|
|
}
|
|
if ev.TradeCount > 0 {
|
|
fields["trade_count"] = ev.TradeCount
|
|
}
|
|
if ev.Provider != "" {
|
|
fields["provider"] = ev.Provider
|
|
}
|
|
if ev.InstrumentCount >= 0 {
|
|
fields["instrument_count"] = ev.InstrumentCount
|
|
}
|
|
if ev.BarCount >= 0 {
|
|
fields["bar_count"] = ev.BarCount
|
|
}
|
|
if ev.AccountID != "" {
|
|
fields["account_id"] = ev.AccountID
|
|
if ev.Cash != "" {
|
|
fields["cash"] = ev.Cash
|
|
}
|
|
fields["position_count"] = ev.PositionCount
|
|
fields["fill_count"] = ev.FillCount
|
|
}
|
|
o.writeJSON(fields)
|
|
return
|
|
}
|
|
|
|
line := fmt.Sprintf("scenario=%s step=%s action=%s status=%s", ev.Scenario, ev.Step, ev.Action, ev.Status)
|
|
if ev.Count >= 0 {
|
|
line += fmt.Sprintf(" count=%d", ev.Count)
|
|
}
|
|
if ev.ErrorCode != "" {
|
|
line += fmt.Sprintf(" error.code=%s", ev.ErrorCode)
|
|
}
|
|
if ev.ErrorMessage != "" {
|
|
line += fmt.Sprintf(" error.message=%q", ev.ErrorMessage)
|
|
}
|
|
if ev.RunID != "" {
|
|
line += fmt.Sprintf(" run.id=%s", ev.RunID)
|
|
}
|
|
if ev.RunStatus != "" {
|
|
line += fmt.Sprintf(" run.status=%s", ev.RunStatus)
|
|
}
|
|
if ev.StartingCash != "" {
|
|
line += fmt.Sprintf(" starting_cash=%s", ev.StartingCash)
|
|
}
|
|
if ev.EndingEquity != "" {
|
|
line += fmt.Sprintf(" ending_equity=%s", ev.EndingEquity)
|
|
}
|
|
if ev.TotalReturn != "" {
|
|
line += fmt.Sprintf(" total_return=%s", ev.TotalReturn)
|
|
}
|
|
if ev.TradeCount > 0 {
|
|
line += fmt.Sprintf(" trade_count=%d", ev.TradeCount)
|
|
}
|
|
if ev.Provider != "" {
|
|
line += fmt.Sprintf(" provider=%s", ev.Provider)
|
|
}
|
|
if ev.InstrumentCount >= 0 {
|
|
line += fmt.Sprintf(" instrument_count=%d", ev.InstrumentCount)
|
|
}
|
|
if ev.BarCount >= 0 {
|
|
line += fmt.Sprintf(" bar_count=%d", ev.BarCount)
|
|
}
|
|
if ev.AccountID != "" {
|
|
line += fmt.Sprintf(" account_id=%s", ev.AccountID)
|
|
if ev.Cash != "" {
|
|
line += fmt.Sprintf(" cash=%s", ev.Cash)
|
|
}
|
|
line += fmt.Sprintf(" position_count=%d fill_count=%d", ev.PositionCount, ev.FillCount)
|
|
}
|
|
fmt.Fprintln(o.w, line)
|
|
}
|
|
|
|
// WriteSummary renders the final run summary.
|
|
func (o *Writer) WriteSummary(s RunSummary) {
|
|
if o.format == OutputJSONL {
|
|
o.writeJSON(map[string]any{
|
|
"type": "summary",
|
|
"scenario": s.Scenario,
|
|
"status": s.Status,
|
|
"steps": s.Steps,
|
|
"passed": s.Passed,
|
|
"exit_code": s.ExitCode,
|
|
})
|
|
return
|
|
}
|
|
fmt.Fprintf(o.w, "scenario=%s status=%s steps=%d passed=%d exit_code=%d\n", s.Scenario, s.Status, s.Steps, s.Passed, s.ExitCode)
|
|
}
|
|
|
|
func (o *Writer) writeJSON(fields map[string]any) {
|
|
data, err := json.Marshal(fields)
|
|
if err != nil {
|
|
// Marshalling a map of strings/ints cannot realistically fail; fall back
|
|
// to a minimal line so a step result is never silently dropped.
|
|
fmt.Fprintf(o.w, "{\"type\":\"error\",\"error_message\":%q}\n", err.Error())
|
|
return
|
|
}
|
|
fmt.Fprintln(o.w, string(data))
|
|
}
|