Other languages

The strategy wire’s CSV protocol is small enough to implement in an afternoon, in any language that can read stdin and write stdout. The entire conversation:

you   > ALGO/1 protocols=stdio-csv-v1 sdk=my-sdk-0.1        (hello, once)
engine> ALGO/1 use=stdio-csv-v1 run_id=...                  (selection)
engine> init,RUN_ID,START,END,[SYM:MIC:CLASS:TICK:PV:CCY|...],[NAME:VALUE|...]
you   > init_ack,LOOKBACK,[],NEEDS_FILLS,NEEDS_SESSIONS,NAME,DESC,[schema...]
engine> bar,SYMBOL,TS,OPEN,HIGH,LOW,CLOSE,VOLUME[,SERIES_ID]
you   > order,SYMBOL,buy|sell,QTY,market|limit|stop|stop_limit|stop_trailing[,PRICE[,STOP_PRICE]]
you   > noop                                                 (when no order; one response per bar)
engine> fill,CLIENT_ID,SYMBOL,buy|sell,QTY,PRICE,TS,COMMISSION   (if you asked for fills)
engine> shutdown,REASON                                      (then exit 0)
you   > log,LEVEL,MESSAGE                                    (anytime, before your response)

Rules that bite: every line is flushed immediately; exactly one response line per bar – the CSV wire cannot express multiple orders per bar (or attached/bracket orders); fields cannot contain commas; timestamps are RFC 3339 nanoseconds. If you need multi-order responses or brackets, that is what the protobuf wire is for – in Go you get it free from the SDK.

v1’s limits. The v1 order line carries neither a label nor a time in force. Two consequences of that are documented limits of v1, pinned by regression tests and kept as they are (v1 does not change):

  • A CSV v1 order reaches the simulator with TIF 0 and no client id, so it rests until it fills or the data ends, and its end-of-data cancellation (reason shutdown) names no client id either. The same order sent as Day, as the Go constructors send it, expires after its first eligible bar. A v1 strategy therefore trades with resting orders that no time in force ends, and the GTC quarter-end rule (GTC quarter-end cancellation) does not reach them either, since it applies to GTC alone.
  • The Go SDK’s v1 working view keeps a filled CSV v1 order listed as working. The SDK files the order under the label the strategy gave it, but the order line drops the label and the engine’s fill names no client id, so the fill moves ctx.Position and leaves the order in ctx.WorkingOrders(). This affects a Go strategy run with --strategy-protocol csv on a legacy run; under --lifetime v2 the same flag selects stdio-csv-v2, which has no such gap.

TestTcv2V1CSVOrderRestsWithTIFZero and TestTcv2V1DayOrderExpiresAfterOneBar (engine) and TestTcv2V1GoViewStrandsAFilledOrder (sdk/strategy) pin both. stdio-csv-v2 is the fix: its order line carries the label, the attempt and an explicit time in force, and every event names the attempt it concerns (The stdio-csv-v2 grammar).

Complete reference clients live in the repository in nine languages beyond Go: strategies/demo/python/smacross.py (stdlib-only Python), strategies/demo/rust/smacross/ (Rust), strategies/demo/c/smacross/ (C99), strategies/demo/shell/smacross.sh (bash + awk), strategies/demo/cpp/smacross/ (C++17), strategies/demo/ocaml/smacross.ml (OCaml), strategies/demo/r/smacross.R (base R), strategies/demo/haskell/smacross.hs (Haskell, base only), and strategies/demo/julia/smacross.jl (Julia stdlib), each speaking raw stdio-csv-v1 with the indicator math embedded. They implement the same SMA crossover as the Go version, and make crosscheck proves every port with an available toolchain produces byte-identical order/noop streams from the same bar fixture – the recommended pattern for porting: pin a decision contract, replay the same fixture (see Testing your strategy), and diff the decisions.

The stdio-csv-v2 grammar

stdio-csv-v2 is the line protocol of the v2 order lifetime (Lifecycle turns) for a strategy written without the Go SDK (GLE-200 increment I10). It carries what the pb wire carries for that lifetime, line for envelope: every submission is an attempt named by a label and an attempt number and has an explicit time in force, the strategy cancels explicitly, and each decision is answered by a command batch (cancels, then orders, then a completion line) where v1 answers each bar with one order line or noop.

Status. Unit 1 (GLE-437) delivered the codec (internal/wire/csv2_strategy.go), three transcripts (internal/wire/testdata/csv2/) and a Python fixture (below); unit 2 (GLE-438) put the grammar on the engine’s strategy wire: the engine host (The engine host) and the Go SDK’s runner (Go over stdio-csv-v2). A run under --lifetime v2 offers v2; a legacy run does not (next).

The conversation, beside v1’s above:

you   > ALGO/1 protocols=stdio-csv-v2 sdk=my-sdk-0.1         (hello, once)
engine> ALGO/1 use=stdio-csv-v2                              (selection)
engine> init,RUN_ID,START,END,[INSTRUMENTS],[INPUTS],[CAPABILITIES],BATCH_LIMIT
you   > init_ack,<the v1 init_ack fields>,[CAPABILITIES]
engine> warmup,COUNT                      (then exactly COUNT bar/abar lines)
you   > commit,FOR,0                      (one batch for the whole block)
engine> bar,...  or  turn,AT,TURN,BUDGET  (a decision)
you   > cancel,LABEL,ATTEMPT,REQUEST_ID   (zero or more, before the orders)
you   > order,LABEL,ATTEMPT,SYMBOL,SIDE,QTY,TYPE,TIF,PRICE,STOP_PRICE,TRAIL   (zero or more)
you   > commit,FOR,COUNT                  (closes the batch)
engine> update / cancel_response / fill / cancelled   (events; no answer)
engine> shutdown,REASON                   (then exit 0)
you   > log,LEVEL,MESSAGE                 (anytime but inside a warm-up block)

The codec. internal/wire/csv2_strategy.go has a writer and a parser for each line kind (FormatOrderCSV2 and ParseOrderCSV2, and so on; a parser takes the fields after the verb, as the v1 parsers do), EscapeCSV2 and UnescapeCSV2, and the batch writer and reader, FormatCSV2Batch and ReadCSV2Batch; the reader returns a CSV2Batch, the instant followed by the cancel requests and the orders in line order. wire.StdioCSV2 is the protocol token. Every function is safe for concurrent use and changes none of its arguments, and the v1 codec and its vocabularies are unchanged.

Negotiating stdio-csv-v2

A strategy asks for v2 by advertising the token in its hello line (ALGO/1 protocols=stdio-csv-v2 sdk=raw-py), and the engine answers ALGO/1 use=stdio-csv-v2 when v2 is the first of its preferences the strategy offers. The engine offers v2 only on a run that puts the order-lifetime v2 profile in effect (--lifetime v2), where it inserts stdio-csv-v2 immediately before stdio-csv-v1 in its preference list:

--strategy-protocolLegacy run--lifetime v2
auto (the default)stdio-pb-v1, stdio-csv-v1stdio-pb-v1, stdio-csv-v2, stdio-csv-v1
pbstdio-pb-v1stdio-pb-v1
csvstdio-csv-v1stdio-csv-v2, stdio-csv-v1

So pb stays the default wherever a strategy offers it, as a Go strategy does, and --strategy-protocol csv runs a Go strategy on v2 (Go over stdio-csv-v2). A strategy that advertises only stdio-csv-v1 still gets v1 under --lifetime v2, and its Init then fails with v1’s text (Capability negotiation). A legacy run is unchanged: its list has no v2, so a strategy that advertises v2 beside stdio-csv-v1 gets v1, byte for byte, and one that advertises v2 alone is refused at the handshake (wire: no common protocol (...)), as an engine that predates v2 refuses it. The host also refuses a v2 init on a run that does not offer lifetime-v2, before anything is written: engine: <strategy name>: stdio-csv-v2 runs only under order lifetime v2, and this run does not offer lifetime-v2 (GLE-200 I10). A v2 strategy must accept lifetime-v2 on its init_ack (next subsection). There is no v2 replay mode: raw lines piped in without a handshake stay v1 (Raw-CSV replay).

The v2 init and init_ack lines

init,RUN_ID,START,END,[INSTRUMENTS],[INPUTS],[CAPABILITIES],BATCH_LIMIT
init_ack,<the stdio-csv-v1 init_ack fields>,[CAPABILITIES]

Both are the v1 lines with trailing fields, so a port reuses its v1 init parsing. The init line is the v1 line followed by the capability list and the batch limit: seven fields after the verb. The init_ack line is the v1 line (6, 7, 8 or 11 fields after the verb, by v1’s rules) followed by the capability list, which is always the last field.

  • [CAPABILITIES] is a bracket list, the tokens joined by |; [] is no capability, and the brackets are required. The engine offers lifetime-v2, and the strategy accepts it by listing it; the two lists are judged by the law the pb wire applies (Capability negotiation). A writer renders a token only when it is non-empty and free of , | [ ] % CR and LF; a reader requires the brackets and non-empty tokens and leaves the rest to that law.
  • BATCH_LIMIT is a decimal integer in [1, 2147483647]: the most command lines (cancel lines plus order lines) one batch may carry on this run. The engine announces 256 (wire.CSV2DefaultBatchLimit) on every run and reads every batch against it; no flag or run-config key changes it.
  • Both lines keep v1’s rules: no escaping, and a value with a CSV metacharacter is refused, as v1 refuses it. Instrument expiration and exchange time zone are dropped from init silently, as v1 drops them, so a dated futures run starts and its line is the line without them.
  • The v2 init_ack cannot express needs_calendar or needs_market_events, whose data travels only on stdio-pb-v1: an InitAck that sets either is refused, not sent without it. Aligned series are refused as in v1.

From the wait transcript, with a batch limit of 8:

init,lifecycle-wait,2026-06-10T13:29:00.000000000Z,2026-06-10T13:35:00.000000000Z,[MSFT:XNAS:equity:0.01:1:USD],[Mode:wait],[lifetime-v2],8
init_ack,2,[],true,false,CSV v2 lifecycle (py),Submits; observes; cancels and replaces one limit order,[Mode:string:wait:wait;batch],[lifetime-v2]

Identity and request ids

Every v2 command and event line names the attempt first: LABEL (the order’s client_id), then ATTEMPT. The pair is the correlation key.

  • On an order line LABEL is non-empty and ATTEMPT is at least 1. Attempts are 1-based per label: each physical submission under a label takes the next number, so a replacement under the same label is its next attempt.
  • A cancel line addresses one attempt; ATTEMPT 0 addresses the label’s current attempt, the pb wire’s meaning. REQUEST_ID is a non-empty id the strategy chooses (the Go SDK writes <label>/<attempt>/c<n>), and the engine’s cancel_response and the cancelled line that ends the attempt echo it.
  • The engine’s lines carry what the message holds: an empty LABEL, attempt 0, empty strings and zero numbers go through as they are. ORDER_REF is the engine’s durable reference for the attempt (sim-N in a backtest), EXEC_ID the venue’s execution id, the de-duplication key of a partial execution.

The v2 line catalogue

The lines kept from v1 byte for byte, with their v1 layout and rules, are the engine’s bar, abar, corpaction, roll, session_start, session_end and shutdown lines and the strategy’s log,LEVEL,MESSAGE and error,CODE,MESSAGE lines. A log or error line may appear anywhere the strategy may write, inside a batch included, but not inside a warm-up block (Warm-up blocks); it is not a command. v2 does not use v1’s noop and order lines or the engine’s v1 fill, afill and cancel lines: v2’s fill has its own layout, and its cancelled takes the place of v1’s cancel. The v2 lines:

LineFromFields after the verb, in orderMessage
orderstrategyLABEL (client_id), ATTEMPT, SYMBOL, SIDE, QTY (quantity), TYPE, TIF, PRICE, STOP_PRICE, TRAIL (trail_amount)Order
cancelstrategyLABEL, ATTEMPT (0 = the label’s current attempt), REQUEST_IDOrderCancelRequest
commitstrategyFOR (the decision’s instant), COUNT (the batch’s command lines)OrderResponse.for_bar
warmupengineCOUNT (the bar lines that follow)the start of a warm-up BarBatch
turnengineAT, TURN, BUDGETLifecycleTurn
updateengineLABEL, ATTEMPT, STATUS, SYMBOL, QTY (quantity), FILLED (filled_quantity), REMAINING (remaining_quantity), TS (timestamp), ORDER_REF, PARENT (parent_client_id), OWNER (owner_id), BROKER_CODE, REASONOrderUpdate
cancel_responseengineLABEL, ATTEMPT, REQUEST_ID, OUTCOME, STATUS, REMAINING (remaining_quantity), TS (timestamp), ORDER_REF, REASONCancelResponse
fillengineLABEL, ATTEMPT, SYMBOL, SIDE, QTY (quantity), PRICE, TS (timestamp), COMMISSION, PARENT (parent_client_id), EXEC_ID, ORDER_REF, REMAINING (remaining_quantity), RAW_PRICE, ADJUSTED_PRICEFill
cancelledengineLABEL, ATTEMPT, TS (timestamp), REASON, TRIGGERED_BY, REQUEST_ID, ORDER_REF, REMAINING (remaining_quantity)OrderCancel

The fields:

  • Text fields (LABEL, SYMBOL, REQUEST_ID, ORDER_REF, PARENT, OWNER, EXEC_ID, TRIGGERED_BY, BROKER_CODE, REASON on update and cancel_response) are escaped (next subsection) and may be empty on the engine’s lines.
  • ATTEMPT, TURN and BUDGET are decimal uint32; QTY, FILLED and REMAINING decimal int64 (an order’s QTY may hold any value: the engine’s admission judges it). COUNT is in [0, 2147483647] on commit and in [1, 2147483647] on warmup.
  • PRICE, STOP_PRICE, TRAIL, COMMISSION, RAW_PRICE and ADJUSTED_PRICE are finite numbers in decimal or scientific notation (no hexadecimal, no _, no NaN or Inf), 0 when unused, written in the shortest form that reads back (99.5, 0, 1e-05). RAW_PRICE and ADJUSTED_PRICE carry the fill’s raw and adjusted prices as they are; 0 means the same as PRICE.
  • FOR, AT and TS are RFC 3339 instants, written in UTC with nine fractional digits (2026-06-10T13:31:00.000000000Z) and read in any precision and zone. TS may be empty, which means the message carries no timestamp.
  • REMAINING on a fill is the quantity still working after the execution, so 0 marks the fill that ends the attempt; on a cancelled line it is the quantity still working when the attempt was cancelled.

The enumerated fields take lower-case tokens, matched exactly (Buy, GTC and Working are malformed); each token list is in enumeration order, from 0 for STATUS, OUTCOME and REASON and from 1 for the others:

FieldTokens
SIDEbuy, sell
TYPEmarket, limit, stop, stop_limit, stop_trailing
TIFday, gtc, ioc, fok
STATUSunspecified, submitting, submission_unknown, working, partially_filled, pending_cancel, held, inactive, filled, cancelled, rejected, denied, expired
OUTCOMEunspecified, confirmed, pending, refused, unknown
REASON (cancelled)unspecified, oco, tif, explicit, rejected, shutdown, parent_cancelled, gtc_expired, venue

REASON’s tokens oco to gtc_expired are v1’s; v2 adds venue and accepts unspecified, which v1’s parser refuses. The engine’s lines are lenient about content and strict about tokens and numbers.

The lifecycle in lines: accepted and working is update STATUS working; held by the race guard is held, REASON beginning held: (The engine race guard); refused by the engine is denied with a REASON, and rejected by the venue rejected with a REASON and a BROKER_CODE; an unknown submission outcome is submission_unknown; a partial fill is partially_filled with fill lines whose REMAINING is above 0, and a full fill the fill line with REMAINING 0; a cancellation is cancel_response OUTCOME confirmed, then cancelled REASON explicit; an expiry is cancelled REASON tif or gtc_expired, or update STATUS expired.

Escaping and fixed arity

Every field of the v2 command and event lines (order, cancel, commit, warmup, turn, update, cancel_response, fill, cancelled) is percent-escaped: % is written %25, , %2C, LF %0A and CR %0D, in upper-case hexadecimal, and every other byte as it is (|, :, ;, ", spaces and non-ASCII bytes included). To read a line, split it on , and decode every field before interpreting it: each % followed by two hexadecimal digits, in either case, becomes the byte they name, and a % without two hexadecimal digits after it is malformed. A needlessly escaped field (%31 for 1) is therefore accepted on every line kind; a writer escapes every field, so its output is canonical. The catalogue’s rejected update shows it:

update,entry,1,rejected,MSFT,5,1,4,2026-06-10T13:31:00.000000000Z,b-1,,,201,price 99.5%2C outside the band; 10%25 limit

whose REASON reads price 99.5, outside the band; 10% limit.

Arity is fixed per verb: a line with more or fewer fields than its layout is malformed. There are no optional trailing fields and no unknown fields, so an additive change needs a new capability or a new protocol version. The init and init_ack lines and the lines kept from v1 are not escaped and keep v1’s rules.

Command batches

A decision is a bar or abar line outside a warm-up block (FOR is the bar’s close, the line’s TS), a turn line (FOR is its AT), or the end of a warm-up block (below). Every decision gets exactly one batch; no other engine line gets an answer.

A batch is zero or more cancel lines, then zero or more order lines, then exactly one commit,FOR,COUNT line, where COUNT is the number of command lines. It is the pb wire’s answer line for envelope: zero or more OrderCancelRequest envelopes, then one OrderResponse whose for_bar is the decision’s instant. A batch with no commands is the completion line alone, commit,FOR,0, which takes the place of v1’s noop.

  • Every cancel line precedes every order line. The engine applies the batch’s cancels first, then its orders, each in line order, and only once the completion line has been read: a batch is applied whole or not at all.
  • The batch limit is inclusive: a batch may carry exactly BATCH_LIMIT command lines. A batch with more is rejected whole: the engine stops reading at the first command line beyond the limit, before parsing it, and applies nothing.
  • End of input, or a read error, before the completion line yields no batch: nothing of it is applied, and the engine fails the run.
  • Any other defect yields no batch either: a line that is not cancel, order or commit (noop, a v1 order line and an empty verb included), a cancel line after an order line, a malformed command line, or a completion line whose FOR is not the decision’s instant (compared as instants, so any rendering of the same instant matches) or whose COUNT is not the number of command lines read.
  • A writer renders a batch only when every line renders and the batch is within the limit: a strategy that cannot express one order sends nothing rather than half a batch.

An over-bound batch fails the run, as end of input does: it is a programming error, and a rollback line would add state on both sides. A strategy that needs more commands than the limit allows splits them across decisions, as the fixture does under a limit of 1.

Warm-up blocks

warmup,COUNT (COUNT in [1, 2147483647]) starts a warm-up block: exactly COUNT engine lines follow, each a bar or abar line in v1’s layout, with no other engine line among them (no session_start, session_end, corpaction, roll or event line), and COUNT counts those bar lines. The strategy buffers them without a decision and answers the block once, after its last bar, with a batch whose FOR is that bar’s close, as the pb wire answers a warm-up BarBatch with one empty OrderResponse. Several blocks may follow one another, one per symbol or a long warm-up in chunks. A strategy may treat any other line inside a block as a protocol error.

A strategy must write nothing until the block’s last bar. From the warmup line until it has read the block’s last bar line, a strategy writes no line at all, log lines and queued cancels included; once it has read that bar, it answers the block as it answers any decision. The engine writes a whole block before it reads anything, and a block can hold 50,000 bars, far more than a pipe holds (darwin pipes start at 16 KiB). A strategy that writes while the engine is still writing can fill its own output pipe and block on the write, so it stops reading, the engine blocks on the rest of the block, and neither process proceeds.

Time in force and what v2 refuses

The time in force is always explicit: an order line’s TIF is day, gtc, ioc or fok, and there is no token for an unspecified one (TIF 0), the time in force every v1 order carries. The Go runner refuses an order without one by name and points to the constructors that set it (Go over stdio-csv-v2).

A writer refuses, rather than drops, what an order line cannot carry: attached orders (brackets), an OCO group, a signal, the order types MOO, MOC, LOC and STOP_TOUCH, an unspecified type or side, an unspecified or unknown time in force, a non-finite price, an empty label or symbol, and attempt 0. Beyond the order line, v2 does not carry exchange calendars (needs_calendar), declared market events (needs_market_events), aligned series, or the account-state, adopted-state and instruments-resolved pushes: a strategy that needs any of them runs on stdio-pb-v1. The v2 profile refuses continuous futures runs today, so stdio-csv-v2 does too. v1’s own limits are unchanged.

Error classes

Every error the codec returns wraps exactly one of four sentinels (errors.Is), and a parser’s message names its line’s verb, for example wire: malformed stdio-csv-v2 line: order line: bad attempt "0":

SentinelTextReturned when
wire.ErrCSV2Malformedwire: malformed stdio-csv-v2 linea parser meets a wrong arity, a bad escape, an unknown token, a bad number or instant, an empty label, symbol or request id where one is required, or attempt 0 on an order; the v1 parsers refuse the v1 part of init or init_ack; the batch reader meets a cancel after an order, a completion line that does not match, or another verb
wire.ErrCSV2NotExpressiblewire: not expressible in stdio-csv-v2a writer meets a nil message, a value outside a vocabulary, a non-finite number, a field the line cannot carry, a bad capability token, a count or limit out of range, or needs_calendar or needs_market_events; the v1 formatters refuse the v1 part
wire.ErrCSV2BatchLimitwire: stdio-csv-v2 batch over its limita batch carries more command lines than its limit, or the limit is below 1
wire.ErrCSV2Incompletewire: stdio-csv-v2 batch incompletethe input ends, or a read fails, before the completion line; the error also wraps the read error (io.EOF, for one)

A strategy reports its own failures with v1’s error,CODE,MESSAGE line, as the fixture does with protocol_error and capability_unsupported.

A worked v2 exchange

From internal/wire/testdata/csv2/lifecycle_wait.txt (< lines are the strategy’s, > lines the engine’s; the engine lines illustrate the protocol and are not a recording of one run):

> bar,MSFT,2026-06-10T13:31:00.000000000Z,100.25,100.75,100,100.5,1200
< order,entry,1,MSFT,buy,1,limit,gtc,99.5,0,0
< commit,2026-06-10T13:31:00.000000000Z,1
> update,entry,1,working,MSFT,1,0,1,2026-06-10T13:31:00.000000000Z,b-1,,,,
> bar,MSFT,2026-06-10T13:32:00.000000000Z,100.5,101,100.25,100.75,900
< cancel,entry,1,entry/1/c1
< commit,2026-06-10T13:32:00.000000000Z,1
> cancel_response,entry,1,entry/1/c1,confirmed,cancelled,1,2026-06-10T13:32:00.000000000Z,b-1,
> turn,2026-06-10T13:32:00.000000000Z,1,3
< order,entry,2,MSFT,buy,1,limit,gtc,99.75,0,0
< commit,2026-06-10T13:32:00.000000000Z,1
> update,entry,2,working,MSFT,1,0,1,2026-06-10T13:32:00.000000000Z,b-2,,,,
> cancelled,entry,1,2026-06-10T13:32:00.000000000Z,explicit,,entry/1/c1,b-1,1
> fill,entry,2,MSFT,buy,1,99.75,2026-06-10T13:33:00.000000000Z,0.5,,e-1,b-2,0,0,0

The 13:31 bar is a decision: one GTC buy limit, attempt 1 of label entry, committed with COUNT 1. The engine accepts it (working, reference b-1). At 13:32 the strategy cancels attempt 1 with request id entry/1/c1; the engine confirms it, with the attempt’s status and its remaining quantity as evidence, and grants a lifecycle turn at the same instant (turn 1 of a budget of 3). The strategy answers the turn with the replacement, attempt 2, committed for the turn’s instant. The cancelled line ends attempt 1, echoing the request id, and the replacement fills on the next bar with nothing remaining. In lifecycle_batch.txt the 13:32 batch carries both commands:

< cancel,entry,1,entry/1/c1
< order,entry,2,MSFT,buy,1,limit,gtc,99.75,0,0
< commit,2026-06-10T13:32:00.000000000Z,2

There the venue leaves the cancellation pending, so the race guard holds attempt 2 (update STATUS held) until attempt 1’s cancelled line, then releases it (update STATUS working).

The engine host

engine/stdiohost_csv2.go is the engine’s side of v2, behind the StdioHost and LifetimeHost methods the pb wire uses. A batch is read into the same orders and cancel requests the pb path returns, so the lifetime-v2 driver, the race guard, the admission policy and the transport scheduler apply unchanged, on the simulated and the emulated-live path alike (Lifecycle turns).

  • Init. The host writes the v2 init line with the run’s capabilities and the batch limit, 256, reads one init_ack line and applies the capability law, as on pb: the strategy must accept lifetime-v2.
  • Decisions. A bar goes out as its bar line, or its abar line when it is adjusted or continuous (v1’s rule), and a lifecycle turn as its turn line. The host then reads one batch with wire.ReadCSV2Batch at the decision’s instant and returns its cancel requests and its orders, each in line order, as the pb path returns the OrderCancelRequest envelopes and the OrderResponse that follows them. While it reads, log lines are printed on stderr ([<strategy name>] LEVEL: message), and an error line ends the read and fails the run with the strategy’s code and message.
  • Warm-up. The bars go out in blocks of at most 50,000, the size at which the pb wire splits its warm-up BarBatch envelopes. Each block is written whole and flushed once, then answered by one batch at its last bar’s close, read as a decision’s is. Warm-up has no order book and no decision point, so the batch’s cancel requests and orders are discarded with a warning on stderr, as on pb ([<strategy name>] warning: 2 order(s) in a warm-up batch discarded (warmup has no decision point)). A refused batch fails the run at once: no further block is written.
  • Events. Each OrderUpdate, CancelResponse, Fill and OrderCancel the driver delivers is one update, cancel_response, fill or cancelled line, formatted by the codec from the values the pb path converts; nothing is read back. The fill line carries the raw and adjusted prices (v2 has no afill). The corpaction, roll, session_start, session_end and shutdown lines are v1’s.
  • Failures. A batch the codec refuses fails the run with nothing of it applied: a batch over the limit (the host stops reading at the first command line beyond it), end of input or a read error before the commit line, and every malformed batch. The error is engine: <strategy name>: followed by the codec’s, which wraps its sentinel (Error classes). An event the grammar cannot carry (a status, outcome or reason outside its vocabulary, or a non-finite number such as a NaN commission) fails the run too, with nothing written: the host refuses rather than corrupt it.

What v2 does not carry. A strategy that needs the account-state, adopted-state or instruments-resolved push runs on stdio-pb-v1.

  • The account-state push is skipped, as on v1, so ctx.Account() keeps its zero value.
  • The adopted-state push (a live run’s startup reconciliation, when a ledger is configured), the market-events push and the instruments-resolved push are refused with v1’s CSV texts, which name the pb protocol; a declaring strategy (needs_instruments) is refused before its first bar, as on v1. A non-declaring run gets no instruments-resolved push either, so ctx.Instrument returns the symbol alone and a strategy’s point value is 1: the SDK books Position.Realized at one currency unit per point, while the engine’s own accounting uses the instrument’s point value.
  • CancelReject: a lifetime-v2 run answers every cancel request with a cancel_response, and the host refuses a CancelReject.
  • The v1 bar exchange (SendBar, SendBarWithCancels): a v2 decision is answered by a batch, which only the lifetime-v2 loop reads, so a v1 loop that reaches a v2 host is refused before the bar is written. No run configuration routes one there; the refusal guards against a routing defect.

Known limits.

  • Every stdio strategy wire, v2 included, can deadlock under heavy handler logging. The engine reads the strategy’s output only while it waits for an answer, and it writes events (on v2 the update, cancel_response, fill and cancelled lines; on pb the same messages; on v1 its fill, afill and cancel lines) without reading anything back. A batch of 256 orders can produce 256 updates. A strategy whose handlers log more than a pipe holds (16 to 64 KiB on darwin) before the engine next reads blocks on its log write, the engine blocks on its next event, and neither proceeds; cancelling the run interrupts neither write. Today’s strategies log little. The fix, a connection that drains the strategy’s output while it writes, is GLE-446, for every wire.
  • On a live run, an event the grammar cannot carry ends the session where pb would carry it: a broker’s NaN commission, or a status outside v2’s vocabulary, makes the host’s formatter refuse. Settling this is a recorded follow-up, due before stdio-csv-v2 trades at a live venue.

Go over stdio-csv-v2

A Go strategy built on the SDK advertises all three wires, ALGO/1 protocols=stdio-pb-v1,stdio-csv-v2,stdio-csv-v1 sdk=go-0.1.0, so it runs on pb by default and on v2 under --lifetime v2 --strategy-protocol csv; an engine that predates v2 ignores the token. The strategy must require lifetime-v2 (RequiredCapabilities), as on pb under --lifetime v2. sdk/strategy/run_csv2.go drives the wire with the pb runner’s lifetime-v2 dispatch:

  • The init line comes first; a malformed one, a batch limit below 1 included, is answered error,protocol_error,.... A strategy that sets needs_calendar is answered error,csv_unsupported,... with v1’s text, and an init_ack the grammar cannot carry with the codec’s.

  • A warm-up block’s bars enter the history without a decision (no OnBar runs, so nothing is written inside the block), and after the last bar the runner answers the block with a batch of no order: the cancel requests queued before it, if any, which the engine discards.

  • Each bar, late bar and turn is answered by one batch: the cancel requests ctx.Cancel queued since the previous batch, each sent once and in queue order, then the orders OnBar or OnLifecycleTurn returned, each stamped with its label’s next attempt, then the commit line. A late bar is buffered without a decision, and its batch carries the queued cancel requests alone; a strategy without a TurnHandler answers a turn with its queued cancel requests and no order.

  • The working view moves as on pb (The working view under lifetime v2), whichever handlers the strategy implements: an update or cancel_response line moves it before OnOrderUpdate or OnCancelResponse runs, a fill line folds the position and moves it before OnFill, and a cancelled line retires the attempt before OnCancel. A confirmed cancel, a full fill and a venue expiry each clear the attempt, so v1’s stranded filled order (v1’s limits) does not occur on v2.

  • corpaction, roll, session_start, session_end and shutdown are handled as on v1, the corporate-action mirror included.

  • A batch the codec cannot write (an attached order, an OCO group, an MOC, more command lines than the limit) is refused whole with error,csv_unsupported,..., and nothing of it is written. An order without a time in force is refused ahead of it, by name: Run returns

    algolang: order "entry" (MSFT) has no time in force: stdio-csv-v2 needs an explicit TIF on every order; set Order.TIF to Day, GTC, IOC or FOK, or build the order with a constructor (MarketBuy, LimitBuy, StopBuy, StopLimitBuy, TrailingStopBuy or their Sell forms), which sets Day
    

    and the error,csv_unsupported,... line carries the same text, its commas written ;. The constructors set Day, so the refusal meets an order built as a struct literal, or one whose TIF was cleared.

  • An OnBar or OnLifecycleTurn error is error,strategy_error,...; v1’s noop, afill and cancel lines, a second init and an unknown verb are error,protocol_error,...; end of input without a shutdown line, inside a block or not, is algolang: engine closed the stream without shutdown.

The Python lifecycle fixture

strategies/demo/python/lifecycle.py is the grammar’s non-Go fixture: stdlib-only Python that speaks stdio-csv-v2 raw, as smacross.py speaks v1. It submits one limit order (label entry, attempt 1, a GTC buy one below the last close), observes it working, cancels it (request id entry/1/c1), replaces it once with attempt 2, and leaves the replacement to fill. Its Mode input chooses how the replacement goes out:

  • wait (the default): the batch carries the cancel alone, and the replacement goes out at the first decision after the cancelled attempt has ended (a confirmed cancel_response or its cancelled line); in lifecycle_wait.txt that is the lifecycle turn the confirmed cancel grants.
  • batch: one batch carries the cancel and the same-side replacement, and the engine’s race guard holds the replacement until the cancelled attempt has ended (lifecycle_batch.txt).

The fixture reads the batch limit from the init line and honours it: under a limit of 1, batch mode cannot fit both commands, so it sends the cancel alone and the replacement at the next decision, as wait mode does. It also shows the refusals a port should make: an init that does not offer lifetime-v2 (error,capability_unsupported,...), and a bad batch limit, a line other than bar or abar inside a warm-up block or a verb outside the v2 grammar (error,protocol_error,...).

make test runs python3 strategies/demo/python/lifecycle_test.py beside the smacross test. It replays both session transcripts through the strategy, feeding each engine line and comparing the replies with the transcript’s strategy lines byte for byte; replays batch mode under limits of 1 and 2 and wait mode under 1; checks its escaping against every v2 line of the catalogue (lines.txt); and checks the warm-up rule and the refusals. The Go tests parse and format the same transcripts, every line of which is canonical, so the two implementations agree on every line, and internal/wire’s TestTcrPythonFixtureHonoursBatchLimitOne runs the fixture as a process and reads each of its batches back with wire.ReadCSV2Batch at limit 1 (skipped when python3 is missing).

The fixture also runs against a live engine in make test: engine’s TestTch2PythonFixtureRoundTrip spawns it through the engine’s strategy host (skipped when python3 is missing). Wait mode runs on the simulator: the confirmed cancel grants the turn that submits attempt 2, and attempt 2 fills. Batch mode runs on the scripted venue, which leaves the cancellation pending: the race guard holds attempt 2 (held), releases it (working) once attempt 1 has ended, and attempt 2 reaches the venue once and fills. A byte-exact golden, engine/testdata/csv2/scripted_session.golden, pins a whole v2 session on the scripted venue, from init to shutdown: a warm-up block, a cancel and a same-side replacement in one batch, the replacement held, the turn, the release and the fill. Its strategy is an in-process scripted peer rather than the fixture, and go test ./engine -run TestTch2GoldenScriptedSession -update records it again.