Strategy Engine
Declarative strategy intents — define what you want, the engine runs it. Covers the intent model, template presets, validation, parameter derivation, and agent orchestration.
1. Intent model
A StrategyIntent is a declarative specification of an index-tracking strategy. You describe what you want — the ticker to track, the capital to deploy, the risk constraints — and the Strategy Engine activates agents that execute it. You never write agent orchestration logic; the engine derives it from your parameters.
Lifecycle states
| State | Meaning | Allowed transitions |
|---|---|---|
active |
Agents are running. Vaults are allocated, rolls execute at threshold, drift is monitored. | paused, completed, cancelled |
paused |
Execution is suspended. Vaults remain in place but no rolls or rebalances occur. Useful for maintenance windows or manual review. | active, cancelled |
completed |
Strategy reached its natural end — all positions settled, capital returned. Terminal state. | none |
cancelled |
User-initiated cancel. Positions are settled and vaults closed. Terminal state. | none |
error |
Unrecoverable fault — agent crash, on-chain revert, or invariant violation. Requires manual intervention. Terminal state. | none |
owner field set to the
Canton party (derived from the JWT sub claim) that created it. Only the
owner can pause, resume, update, or cancel an intent. Non-owner requests receive
HTTP 403.
What the engine does
When an intent transitions to active, the Strategy Engine:
- Validates all parameters against known-good ranges and cross-field sanity rules.
- Derives operational parameters (expected rolls/year, estimated annual cost).
- Selects the required agent set based on strategy type.
- Allocates vaults on-chain via the PoolVault template.
- Starts the rebalancing loop: monitor price, roll at threshold, enforce constraints.
- Writes runtime state back to the intent (
runtimefield) so the frontend can poll it.
2. Full schema
The canonical TypeScript type for StrategyIntent (from
packages/agent-sdk/src/intents.ts). Every field is shown with its
type and purpose.
// ── Type aliases ──
type StrategyType = 'treasury' | 'hedge' | 'leverage' | 'yield' | 'custom';
type IntentStatus = 'active' | 'paused' | 'completed' | 'cancelled' | 'error';
type RollStrategy = 'standard' | 'shortDated' | 'manual';
type NotificationChannel = 'email' | 'push' | 'inapp' | 'webhook';
// ── Sub-objects ──
interface CollateralSpec {
asset: 'USDC' | 'WETH' | 'WBTC'; // collateral asset
amount: string; // decimal string, e.g. "10000.00"
}
interface IntentConfig {
strikeRatio: number; // 0.01–1.0, e.g. 0.50 = 50% strike
maturityDays: number; // 1–365, option maturity in calendar days
rebalanceThreshold: number; // >= 1.0, rotate when price < strike * threshold
autoRoll: boolean; // auto-roll at maturity / threshold breach
rollStrategy: RollStrategy; // standard | shortDated | manual
}
interface IntentConstraints {
maxAnnualDriftBps: number; // max tolerated annual drift (basis points)
maxSlippagePerRollBps: number; // max slippage per roll (basis points)
maxCapital: string; // hard capital cap (decimal string)
stopLossDriftBps: number; // unwind if drift exceeds this (basis points)
}
interface IntentNotifications {
onRoll: boolean; // notify on each roll
onThresholdBreach: boolean; // notify when rebalance threshold is crossed
onStopLoss: boolean; // notify if stop-loss triggers
channels: NotificationChannel[]; // at least one channel
}
interface IntentRuntime {
vaultIds: string[]; // active on-chain vault contract IDs
currentDriftBps: number; // live annualized drift in basis points
totalRolls: number; // count of rolls executed so far
totalCostBps: number; // cumulative realized slippage in bps
lastRollAt: string; // ISO 8601 timestamp of last roll
lastRollCostBps: number; // slippage cost of last roll in bps
}
// ── Top-level intent ──
interface StrategyIntent {
id: string; // auto-generated: intent-{unixms}-{8hex}
owner: string; // Canton party (from JWT sub)
type: StrategyType; // strategy preset
status: IntentStatus; // lifecycle state
statusReason?: string; // e.g. "Stop-loss triggered: drift exceeded 5%"
collateral: CollateralSpec; // what capital to deploy
ticker: string; // e.g. "BTC/USDC"
config: IntentConfig; // strategy parameters
constraints: IntentConstraints; // risk guardrails
notifications: IntentNotifications; // alert preferences
runtime?: IntentRuntime; // live state (populated by agents)
}
CreateIntentRequest — the StrategyIntent shape minus
id, status, and runtime. Those three are
populated by the engine. Updates use UpdateIntentRequest — a partial
of only the mutable runtime fields.
3. Strategy templates
Five pre-configured templates with backtest-optimal defaults. Each template
provides a complete config + constraints +
notifications triple. Use createFromTemplate(type, overrides)
to merge user values over the template defaults.
| Type | Label | strikeRatio | maturityDays | rebalanceThreshold | autoRoll | rollStrategy | maxAnnualDriftBps | maxSlippagePerRollBps | maxCapital | stopLossDriftBps |
|---|---|---|---|---|---|---|---|---|---|---|
treasury |
Treasury Management | 0.50 | 30 | 1.5 | true | standard | 200 | 10 | 1,000,000,000 | 500 |
hedge |
Portfolio Hedge | 0.30 | 14 | 1.3 | true | shortDated | 500 | 15 | 50,000,000 | 800 |
leverage |
Leveraged Exposure | 0.80 | 7 | 1.2 | true | shortDated | 2000 | 25 | 1,000,000 | 5000 |
yield |
Yield Generation | 0.50 | 60 | 2.0 | true | standard | 300 | 10 | 10,000,000 | 600 |
custom |
Custom Strategy | 0.50 | 30 | 1.5 | false | manual | 1000 | 20 | 1,000,000 | 1000 |
Notification defaults
| Type | onRoll | onThresholdBreach | onStopLoss | channels |
|---|---|---|---|---|
treasury | true | true | true | inapp, webhook |
hedge | true | true | true | inapp, push, webhook |
leverage | true | true | true | inapp, push |
yield | true | false | true | inapp |
custom | false | false | false | inapp |
Template descriptions
treasury— Treasury Management- Conservative yield generation for DAO treasuries. Low drift (200 bps cap), capital preservation focus. Suitable for stablecoin holdings seeking modest yield without meaningful directional risk. Default max capital of 1B reflects DAO scale.
hedge— Portfolio Hedge- Protect existing crypto holdings against drawdowns. Lower strike (0.30 = cheaper protection, lower coverage). Short-dated rolls (14-day maturity, shortDated strategy). For active portfolio managers hedging directional exposure.
leverage— Leveraged Exposure- Amplified directional exposure. Highest strike (0.80) for maximum delta per unit of capital. Most aggressive ranges — 2000 bps drift tolerance, 5000 bps stop-loss. 7-day maturity with shortDated rolls. For sophisticated traders comfortable with significant downside risk.
yield— Yield Generation- Passive yield from options premium. Mid-range strike (0.50), longest maturity (60 days) for lowest roll frequency and cost. Threshold breach notifications off by default — designed for depositors seeking steady returns without active management.
custom— Custom Strategy- Full manual control — no defaults enforced, all parameters must be explicitly configured. autoRoll and all notifications off by default. rollStrategy is manual. For advanced users with specific requirements not covered by the presets.
4. Validation rules
Every intent goes through a two-stage validation pipeline before it can be activated. Stage 1 blocks invalid intents with errors; Stage 2 produces warnings for configurations that are technically legal but likely misconfigured.
4.1 Parameter ranges (hard errors)
| Parameter | Min | Max | Notes |
|---|---|---|---|
config.strikeRatio | 0.01 | 1.0 | Exclusive min — must be > 0 |
config.maturityDays | 1 | 365 | Integer |
config.rebalanceThreshold | 1.0 | 100.0 | Values below 1.0 are nonsensical (price can't go below zero) |
constraints.maxAnnualDriftBps | 0 | 10,000 | 10,000 bps = 100% annual drift |
constraints.maxSlippagePerRollBps | 0 | 100 | 100 bps = 1% per roll |
constraints.stopLossDriftBps | 0 | 10,000 | 10,000 bps = unwind at 100% drift |
type, collateral (with
asset and amount), ticker, config,
constraints, and notifications are all required. The
ticker must match ^[A-Z]{2,5}/[A-Z]{2,5}$.
notifications.channels must have at least one entry from the allowed
set: email, push, inapp, webhook.
collateral.asset must be one of USDC, WETH,
WBTC.
4.2 Cross-field sanity checks (warnings)
| Check | Condition | Severity | Rationale |
|---|---|---|---|
| Stop-loss vs drift tolerance | stopLossDriftBps <= maxAnnualDriftBps |
warning | Stop-loss may trigger during normal drift — your strategy will unwind prematurely. Increase stopLossDriftBps or tighten maxAnnualDriftBps. |
| Short maturity + standard rolls | maturityDays < 7 AND rollStrategy == 'standard' |
warning | Very short maturity with standard roll strategy generates excessive roll costs. Consider shortDated roll strategy for sub-7-day maturities. |
| High strike on non-leverage type | strikeRatio > 0.80 AND type != 'leverage' |
warning | Strike above 0.80 is aggressive — exceptionally high delta per unit of capital with proportionally higher drift. Consider the leverage type which is designed for this range. |
4.3 Type-specific warnings
| Type | Condition | Warning |
|---|---|---|
treasury |
maxAnnualDriftBps > 500 |
Treasury strategy with high drift tolerance — the treasury preset caps at 200 bps. Values above 500 bps are outside the conservative envelope. |
yield |
maturityDays < 14 |
Yield strategy benefits from longer maturity (>=30 days) to reduce roll frequency and cost. Short maturities erode yield through slippage. |
4.4 Validation response shape
Validation returns a structured result — never throws. Errors block activation; warnings are advisory.
interface ValidationResult {
valid: boolean; // false if any errors exist
errors: string[]; // hard blockers — intent cannot activate
warnings: string[]; // advisory — intent can activate but may be misconfigured
}
5. Parameter derivation
When the Strategy Engine activates an intent, it derives three operational parameters from the config and constraints. These drive the agent orchestration plan and appear in the frontend as pre-flight estimates.
| Derived parameter | Formula | Example (BTC/USDC, treasury defaults) |
|---|---|---|
expectedRollsPerYear |
Math.ceil(365 / maturityDays) |
ceil(365 / 30) = 13 rolls/year |
estimatedMaxAnnualCostBps |
expectedRollsPerYear * maxSlippagePerRollBps |
13 * 10 = 130 bps/year (1.30%) |
isPaperOptimal |
strikeRatio === 0.50 && maturityDays === 30 |
true — treasury uses paper-optimal defaults |
isPaperOptimal is true, your config
matches this baseline exactly. The treasury and yield presets
are paper-optimal; hedge and leverage deliberately deviate
for their respective use cases.
Cost model quick reference
| Template | maturityDays | maxSlippagePerRollBps | rolls/year | max annual cost (bps) | max annual cost (%) |
|---|---|---|---|---|---|
treasury | 30 | 10 | 13 | 130 | 1.30% |
hedge | 14 | 15 | 27 | 405 | 4.05% |
leverage | 7 | 25 | 53 | 1325 | 13.25% |
yield | 60 | 10 | 7 | 70 | 0.70% |
custom | 30 | 20 | 13 | 260 | 2.60% |
These are worst-case estimates — actual costs depend on market conditions at roll time. The paper documents empirical drift of ~1–4%/year; real-world costs are typically 30–50% of these ceilings.
6. Agent orchestration
Each strategy type requires a specific set of agents to execute. The Strategy Engine
selects and launches them automatically when the intent transitions to
active.
| Agent | Role | Required by |
|---|---|---|
| Oracle Agent | Publishes consensus prices on-chain (PublishedPrice). Required for settlement and drift calculation. Without it, no strategy can function. |
All types |
| Pool Agent | Manages PoolVault lifecycle: creates vaults, processes deposits/withdrawals, maintains the option inventory. One per collateral asset. | All types |
| Rebalance Agent | Monitors drift, executes rolls when rebalanceThreshold is crossed, enforces maxSlippagePerRollBps. The core execution loop. |
All types |
| Market Maker Agent | Provides liquidity by quoting bid/ask spreads on option positions. Reduces roll slippage. Optional but recommended for leverage and hedge (frequent rolls). |
hedge, leverage (recommended); others (optional) |
| Settlement Agent | Settles matured options, distributes proceeds. Runs on a timer aligned to maturity windows. | All types |
| Position Monitor | Checks for stuck positions, invariant violations, and on-chain state drift. Emits error status transitions when invariants break. |
All types (always on) |
| Swap Taker Agent | Executes asset swaps (e.g. USDC to WBTC) for collateral rotation. Only needed when the collateral asset differs from the pool's denomination. | Conditional (cross-asset only) |
| Strategy | Core agents | Recommended extras | Notes |
|---|---|---|---|
treasury |
Oracle, Pool, Rebalance, Settlement, Position Monitor | Market Maker (optional) | Steady-state operation. 13 rolls/year — Market Maker adds marginal improvement. |
hedge |
Oracle, Pool, Rebalance, Settlement, Position Monitor | Market Maker (strongly recommended) | 27 rolls/year — Market Maker significantly reduces cumulative slippage at this frequency. |
leverage |
Oracle, Pool, Rebalance, Settlement, Position Monitor | Market Maker (strongly recommended), Swap Taker | 53 rolls/year — highest roll frequency. Market Maker is nearly mandatory to keep costs manageable. Swap Taker if collateral != pool denomination. |
yield |
Oracle, Pool, Rebalance, Settlement, Position Monitor | — | Lowest maintenance. 7 rolls/year — Market Maker overhead not justified. |
custom |
Oracle, Pool, Rebalance, Settlement, Position Monitor | Depends on config | Agent set follows the derived parameters, not the template. The engine selects based on your config values. |
7. TypeScript example
End-to-end example: create a BTC/USDC treasury intent, validate it, activate it, and poll for runtime state. Production-quality code using the Agent SDK.
import {
createFromTemplate,
validateStrategyConfig,
deriveParams,
type StrategyIntent,
} from '@medici/agent-sdk';
// ── Step 1: Create from template with overrides ──
const strategy = createFromTemplate('treasury', {
config: { strikeRatio: 0.55 }, // slightly more aggressive than default 0.50
});
console.log('Config:', strategy.config);
console.log('Constraints:', strategy.constraints);
console.log('Notifications:', strategy.notifications);
// ── Step 2: Validate ──
const validation = validateStrategyConfig(
'treasury',
strategy.config,
strategy.constraints,
);
if (!validation.valid) {
console.error('Validation errors:', validation.errors);
process.exit(1);
}
if (validation.warnings.length > 0) {
console.warn('Warnings:', validation.warnings);
}
// ── Step 3: Derive operational params ──
const params = deriveParams(strategy.config, strategy.constraints);
console.log('Expected rolls/year:', params.expectedRollsPerYear);
console.log('Max annual cost (bps):', params.estimatedMaxAnnualCostBps);
console.log('Paper-optimal:', params.isPaperOptimal);
// ── Step 4: Build the create-intent request ──
const request = {
type: 'treasury' as const,
ticker: 'BTC/USDC',
collateral: { asset: 'USDC' as const, amount: '50000.00' },
config: strategy.config,
constraints: strategy.constraints,
notifications: strategy.notifications,
};
// ── Step 5: Submit to Ledger Service ──
const response = await fetch('https://canton.int.medici.loan/api/v1/intents', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(request),
});
if (!response.ok) {
const err = await response.json();
throw new Error(`Create failed: ${err.error} — ${err.message}`);
}
const { intent } = await response.json();
console.log('Intent created:', intent.id, 'status:', intent.status);
// ── Step 6: Poll runtime state (every 30s) ──
const poll = async (intentId: string) => {
const res = await fetch(
`https://canton.int.medici.loan/api/v1/intents/${intentId}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
const { intent: fresh } = await res.json();
if (fresh.runtime) {
console.log(`Drift: ${fresh.runtime.currentDriftBps} bps | Rolls: ${fresh.runtime.totalRolls} | Cost: ${fresh.runtime.totalCostBps} bps`);
}
};
const interval = setInterval(() => poll(intent.id), 30_000);
// Stop polling when done
process.on('SIGINT', () => { clearInterval(interval); });
8. cURL example
Create, inspect, and manage a strategy intent directly via the Ledger Service REST API. All examples use BTC/USDC as the tracked ticker.
8.1 Create a treasury intent
# Set your base URL and JWT token
BASE="https://canton.int.medici.loan"
TOKEN="$(curl -s -X POST https://keycloak.int.medici.loan/realms/medici/protocol/openid-connect/token \
-d grant_type=password \
-d client_id=app-user-validator \
-d username=alice \
-d password="$PASSWORD" \
-d client_secret="$CLIENT_SECRET" | jq -r '.access_token')"
# Create the intent
curl -s -X POST "$BASE/api/v1/intents" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "treasury",
"ticker": "BTC/USDC",
"collateral": {"asset": "USDC", "amount": "50000.00"},
"config": {
"strikeRatio": 0.50,
"maturityDays": 30,
"rebalanceThreshold": 1.5,
"autoRoll": true,
"rollStrategy": "standard"
},
"constraints": {
"maxAnnualDriftBps": 200,
"maxSlippagePerRollBps": 10,
"maxCapital": "50000.00",
"stopLossDriftBps": 500
},
"notifications": {
"onRoll": true,
"onThresholdBreach": true,
"onStopLoss": true,
"channels": ["inapp", "webhook"]
}
}' | jq .
# Response (201):
# {
# "intent": {
# "id": "intent-1719763200000-a1b2c3d4",
# "owner": "alice",
# "type": "treasury",
# "status": "active",
# "ticker": "BTC/USDC",
# "collateral": {"asset": "USDC", "amount": "50000.00"},
# "config": { ... },
# "constraints": { ... },
# "notifications": { ... },
# "createdAt": "2026-06-30T12:00:00Z",
# "updatedAt": "2026-06-30T12:00:00Z"
# },
# "fetchedAt": "2026-06-30T12:00:00Z"
# }
8.2 List, pause, and cancel
# List all active intents curl -s "$BASE/api/v1/intents?status=active" \ -H "Authorization: Bearer $TOKEN" | jq . # Get a specific intent INTENT_ID="intent-1719763200000-a1b2c3d4" curl -s "$BASE/api/v1/intents/$INTENT_ID" \ -H "Authorization: Bearer $TOKEN" | jq . # Pause it curl -s -X POST "$BASE/api/v1/intents/$INTENT_ID/pause" \ -H "Authorization: Bearer $TOKEN" | jq . # Resume it curl -s -X POST "$BASE/api/v1/intents/$INTENT_ID/resume" \ -H "Authorization: Bearer $TOKEN" | jq . # Cancel it (soft-delete — terminal) curl -s -X DELETE "$BASE/api/v1/intents/$INTENT_ID" \ -H "Authorization: Bearer $TOKEN" | jq . # List strategy templates (no auth required) curl -s "$BASE/api/v1/strategies" | jq . # Portfolio summary curl -s "$BASE/api/v1/portfolio" \ -H "Authorization: Bearer $TOKEN" | jq .
8.3 Update an intent
# Update config and constraints (partial — only changed fields)
curl -s -X PUT "$BASE/api/v1/intents/$INTENT_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config": {
"strikeRatio": 0.55,
"maturityDays": 45
},
"constraints": {
"maxAnnualDriftBps": 300
}
}' | jq .
completed or cancelled are immutable — PUT returns
HTTP 500 with message "intent is completed/cancelled and cannot be modified".