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 } // 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 } 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) } 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)) }