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.0becameorjson>=3.10.15,<4.brotliandzstandardare required for compressed payload helpers.mesolive-sdk-examplesnow depends onplotlyfor 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.SelectedLegswas replaced byOrderLegs.PreparePositionExitResult.PositionLegswas replaced byOrderLegs.PreparePositionAdjustmentResult.OrderLegsis nowlist[ExecPlanLegRef].PreparePositionEntryResult.QtyMultiplieris optional.LegGroupQtyswas split intoExecutionPlanLegGroupQtysfor prepared execution andPositionLegGroupQtysfor 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.ExecPlanLegIdandLiveExecutionAssignment.ExecPlanLegIdattribute broker executions and assignments back to prepared legs when possible.GetPositionResult.PositionStrategyDefinitionexposes the strategy-definition snapshot captured for a position when available.ListPositionsArgsaddsKind,UnderlyingContractId, andStatefilters for default-position and underlying-scoped reads.LivePositionaddsIsDefaultPosition,Underlying, andUnderlyingContractId.GetEventsSinceArgs.MaxEventsLimitcapsLimitat 1000; keep continuation tokens opaque.UpdateStrategyArgsadds optionalDescription,BacktestId, andMesoSimInstance.BacktestIdandMesoSimInstancemust 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.