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):
# The worker type names the worker: source urn:com.virtufin:worker:movingaveragepython,
# errors com.virtufin.movingaveragepython.error on wrk.movingaveragepython.error.
super().__init__(worker_type="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 emits limit orders at the triggering candle's close, so
type is always "limit" and price is that close (since 0.2.2; before,
market orders with price: null, which SimulatedExecutor rejects).
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.