Skip to main content

Migrating to mesolive-sdk 2.0

mesolive-sdk 2.0 updates the Python SDK to the current MesoLive SignalR API surface. It is a breaking release for Control Hub order and paper-fill workflows because prepared execution now uses stable execution-plan leg ids instead of display leg names.

Client and package surface

New clients are exported from mesolive_sdk:

from mesolive_sdk import MesoLiveHistoricalDataHubClient, MesoLiveRiskGraphHubClient

Existing reconnect callbacks remain available on every hub client:

client.on_reconnecting(lambda: print("reconnecting"))
client.on_reconnected(lambda: print("reconnected"))

Package dependencies changed:

  • orjson==3.9.0 became orjson>=3.10.15,<4.
  • brotli and zstandard are required for compressed payload helpers.
  • mesolive-sdk-examples now depends on plotly for optional HTML report output.

Execution-plan leg identity

Prepared entry, exit, and adjustment results now return orderable legs as OrderLegs: list[ExecPlanLegRef]. In 1.0, user code commonly used display leg names from fields such as SelectedLegs or PositionLegs. In 2.0, display names are not stable identities.

Use ExecPlanLegRef.ExecPlanLegId for order placement, paper fills, fill-state polling, per-leg quote/greek dictionaries, and leg-group operations.

# 1.0 style
leg_names = list(prepared.SelectedLegs.keys())

# 2.0 style
order_legs = [leg for leg in prepared.OrderLegs if not leg.IsMarker]
leg_ids = [leg.ExecPlanLegId for leg in order_legs]
refs_by_id = {leg.ExecPlanLegId: leg for leg in prepared.OrderLegs}

LegGroupOrder.LegNames was replaced by LegGroupOrder.ExecPlanLegIds:

order = models.LegGroupOrder(
ExecPlanLegIds=list(prepared.LegGroups[group_id]),
OrderType=models.OrderType.Limit,
TimeInForce=models.TimeInForce.DAY,
LimitPrice=limit_price,
)

SendOrderArgs.QtyMultiplier is required as a nullable field. For AddPosition/AddLegs orders, pass a positive multiplier; it may override the prepared multiplier only before execution starts and must match the locked multiplier after that. Pass None for exits, removals, and move-leg orders.

Prepare workflow changes

Entry, exit, and adjustment preparation all return OrderLegs, LegGroups, and LegGroupQtys keyed by execution-plan leg id.

  • PreparePositionEntryResult.SelectedLegs was replaced by OrderLegs.
  • PreparePositionExitResult.PositionLegs was replaced by OrderLegs.
  • PreparePositionAdjustmentResult.OrderLegs is now list[ExecPlanLegRef].
  • PreparePositionEntryResult.QtyMultiplier is optional.
  • LegGroupQtys was split into ExecutionPlanLegGroupQtys for prepared execution and PositionLegGroupQtys for persisted positions.

Minimal entry flow:

start = await control.start_prepare_position_entry(entry_args)
status = await control.get_prepare_position_entry_status(
models.GetPreparePositionEntryStatusArgs(JobId=start.JobId)
)
prepared = status.Result
entry_leg_ids = [leg.ExecPlanLegId for leg in prepared.OrderLegs if not leg.IsMarker]

Minimal exit flow:

start = await control.start_prepare_position_exit(
models.PreparePositionExitArgs(
IdempotencyKey=exit_key,
PositionId=position_id,
)
)
status = await control.get_prepare_position_exit_status(
models.GetPreparePositionExitStatusArgs(JobId=start.JobId)
)
prepared = status.Result
exit_leg_ids = [leg.ExecPlanLegId for leg in prepared.OrderLegs if not leg.IsMarker]

Adjustment workflows now use canonical live-leg ids when an operation targets existing legs. AdjustmentSignalEvent.OperationTargets contains server-resolved ids for each adjustment signal operation. Pass those ids into PreparePositionAdjustmentArgs.TargetLiveLegIds for RemoveLegs and MoveLeg; AddLegs and UpdateVars ignore this field.

target = next(
t for t in adjustment_event.OperationTargets
if t.Operation == models.PositionAdjustmentOperation.MoveLeg
)
if target.ResolutionError:
raise RuntimeError(target.ResolutionError)

start = await control.start_prepare_position_adjustment(
models.PreparePositionAdjustmentArgs(
IdempotencyKey=adjust_key,
PositionId=adjustment_event.PositionId,
Operation=models.PositionAdjustmentOperation.MoveLeg,
Condition="Adjustment",
TargetLiveLegIds=target.LiveLegIds,
)
)

Paper fills and fill-state polling

ApplyPaperFillsArgs.PaperLegFills is now a list, and each PaperLegFill identifies one execution-plan leg with ExecPlanLegId. ApplyPaperFillsArgs.QtyMultiplier is still required as a nullable field. For AddPosition/AddLegs paper fills, pass a positive multiplier; it may override the prepared multiplier only before execution starts and must match the locked multiplier after that. Pass None for exits, removals, and move-leg fills.

paper_fills = [
models.PaperLegFill(
ExecPlanLegId=leg.ExecPlanLegId,
Price=fill_price_by_leg_id[leg.ExecPlanLegId],
Qty=None, # None fills the remaining planned quantity.
)
for leg in prepared.OrderLegs
if not leg.IsMarker
]

qty_multiplier = (
prepared.QtyMultiplier
if isinstance(prepared, models.PreparePositionEntryResult)
or (
isinstance(prepared, models.PreparePositionAdjustmentResult)
and prepared.Operation == models.PositionAdjustmentOperation.AddLegs
)
else None
)

await control.apply_paper_fills(
models.ApplyPaperFillsArgs(
IdempotencyKey=fill_key,
BrokerAccountId=prepared.BrokerAccountId,
ExecPlanId=prepared.ExecPlanId,
QtyMultiplier=qty_multiplier,
PaperLegFills=paper_fills,
)
)

Use per-leg PaperLegFill.Qty for partial fills. Preserve the signed direction from ExecPlanLegRef.PlannedQty; when Qty is omitted, the server fills the remaining planned quantity for that execution-plan leg.

from decimal import Decimal

partial_fills = [
models.PaperLegFill(
ExecPlanLegId=leg.ExecPlanLegId,
Price=fill_price_by_leg_id[leg.ExecPlanLegId],
Qty=leg.PlannedQty / Decimal("2"),
)
for leg in prepared.OrderLegs
if not leg.IsMarker
]

Use get_execution_plan_fill_state to poll per-leg and per-group fill progress after paper fills or real broker executions:

state = await control.get_execution_plan_fill_state(
models.GetExecutionPlanFillStateArgs(ExecPlanId=prepared.ExecPlanId)
)

remaining_by_leg = {
leg.ExecPlanLegId: leg.RemainingQty
for leg in state.Legs
}
complete = state.IsComplete

GetExecutionPlanFillStateResult.IsComplete is false until all tradeable execution-plan legs are fully filled. If the server detects broker executions that cannot be attributed to an execution-plan leg, the portal logs a warning and also reports IsComplete=False; the public SDK model does not expose a separate unattributed count.

Read-model and event changes

Several read models gained stable identifiers and filters:

  • LiveExecution.ExecPlanLegId and LiveExecutionAssignment.ExecPlanLegId attribute broker executions and assignments back to prepared legs when possible.
  • GetPositionResult.PositionStrategyDefinition exposes the strategy-definition snapshot captured for a position when available.
  • ListPositionsArgs adds Kind, UnderlyingContractId, and State filters for default-position and underlying-scoped reads.
  • LivePosition adds IsDefaultPosition, Underlying, and UnderlyingContractId.
  • GetEventsSinceArgs.MaxEventsLimit caps Limit at 1000; keep continuation tokens opaque.
  • UpdateStrategyArgs adds optional Description, BacktestId, and MesoSimInstance. BacktestId and MesoSimInstance must be updated together.

Risk Graph and Historical Data payloads

Risk Graph and Historical Data APIs return payload envelopes. Decode them before reading series data:

from mesolive_sdk.risk_graph import decode_risk_graph_data

snapshot = await risk.get_position_risk_graph_snapshot(args)
data = decode_risk_graph_data(snapshot)
from mesolive_sdk.historical_data import decode_metric_payload

result = await historical.get_position_metrics(args)
decoded = decode_metric_payload(result)
series = decoded.data.Series

Historical Data supports None, Gzip, Brotli, and Zstd compression. Risk Graph supports Gzip, Brotli, and Zstd.

Plotly reports in examples

The 2.0 Risk Graph and Historical Data examples can write interactive Plotly HTML reports from decoded payloads:

python -m mesolive_sdk_examples.risk_graph_example position snapshot --position-id 789 --plot-html risk-graph.html --no-json-output
python -m mesolive_sdk_examples.historical_data_example position-metrics --position-id 789 --include PnL,Greeks --plot-html position-metrics.html

Risk Graph stream reports require a finite stream (--max-events or --seconds) and render the last received event. --raw-payload affects terminal output only; plotting still decodes the payload when a report is requested.