Python Strategies
MovingAveragePython is the Python twin of the C# MovingAverage
worker — same strategy rule, same canonical JSON contract, different
engine. It shows the pattern is language-portable even though IDecide
itself is a C# type.
The worker
from virtufin.worker_devkit import WorkerBase
class MovingAverageWorker(WorkerBase):
def __init__(self):
super().__init__(
response_type="movingaveragepython.response",
source="urn:virtufin:worker:movingaveragepython",
)
# ... SMA ring buffer, intent latch, portfolio fold ...
def handle(self, event_data):
# dispatch by event_data["type"]: position -> fold, candle -> step
...
_worker = MovingAverageWorker()
def Process(cloud_event):
return _worker.process(cloud_event)
Python workers ship as source: a module under src/ with a
module-level Process(cloud_event), stdlib only, with
virtufin-workmanager (the devkit) resolved at load time.
Contract parity
Config keys are CloudEvent extension attributes (extension-only;
payload fields ignored) — e.g. timewindowdays (default 20), symbol,
scenarioid (required) — the same convention the C# bridge uses.
Response envelope: at parity with C#. An emitted order publishes
the pubsub-topics spec's actual "Order submitted" event: topic
sc.<scenarioid>.trading.order.submitted, stable ce-type =
com.virtufin.trading.order.submitted (not overridable via
replytopic), ce-subject = order/<order_id>, payload
{"order_id":...,"symbol":...,"side":"buy"|"sell",
"type":"market"|"limit","qty":...,"price":...,"venue":...,
"submitted_at":...} (see
MovingAveragePython/src/moving_average_python.py::_order_submitted_response).
Routing goes through the publishtopic extension attribute -- the
devkit's WorkerBase.response() only accepts id/message kwargs
(built for the generic command/success envelope shape), so the
worker builds this envelope directly, same as the old ad hoc one it
replaces. This worker only ever emits market orders (no limit-price
concept), so type is always "market" and price is always null.
Testing
pytest runs the suite; conftest.py falls back to a minimal
worker_devkit stub when the private PyPI feed is unreachable locally —
CI always installs the real package.