- Implement backtest workflow validation for headless operator mode - Add handoff evidence tracking for backtest runs - Update operator client, runner, scenario, and output modules - Add test files for handoff, backtest runner, and error handling - Add test data for backtest scenarios and validation
163 lines
4.1 KiB
Go
163 lines
4.1 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
|
|
}
|
|
|
|
// 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))
|
|
}
|