Dynamic sizing from external conviction + audit log + error counter
dynamic sizing + audit log
Mirrors runner/src/plugins/12-exampleAuditLog
What this teaches
- Trade size is a function of an external signal's conviction (0..1)
- onExecuted persists every successful trade into ctx.state as a trade ledger you can later query/export
- state.keys() pattern to summarize the ledger
- onError increments a per-error-kind counter to feed dashboards or trigger external alerts when one kind of failure spikes
New vs exampleMultiTrigger
- Conviction-weighted sizing (was halving constant)
- Persistent trade audit log via onExecuted + state.keys()
- onError counters split by phase (client errors vs on-chain aborts)
import { noop, type StrategyPlugin } from "@automark/runtime-core";
import { Market } from "@automark/sdk/market";
import { bigintMin } from "@automark/sdk/math";
import { hours, minutes } from "@automark/sdk/duration";
interface Signal {
asset: string;
direction: "up" | "down";
conviction: number; // 0..1 — higher = bigger trade
}
interface TradeLog {
digest: string;
recordedAtMs: number;
marketId: string;
strike: string;
quantity: string;
isUp: boolean;
gasMist: string;
}
export default function createExampleAuditLog(): StrategyPlugin {
const vaultId = process.env.VAULT_ID;
if (!vaultId) throw new Error("exampleAuditLog: VAULT_ID not set");
const signalUrl =
process.env.SIGNAL_URL ?? "https://your-backend.example/signal";
return {
name: "exampleAuditLog",
vaultId,
triggers: [{ kind: "cron", everySeconds: 90 }],
async decide(ctx) {
if (ctx.vault.isFrozen) return [noop("frozen")];
// `logger.timing` measures + logs the fetch duration. Useful when the
// signal endpoint is the slowest step of the tick — you'll see it
// immediately if latency creeps up.
const signal = await ctx.logger.timing(
"fetch signal",
async () => (await (await fetch(signalUrl)).json()) as Signal,
);
const conviction = Math.max(0, Math.min(1, signal.conviction));
if (conviction < 0.3) {
return [noop(`low conviction ${conviction}`)];
}
const { maxSinglePosition, exposureHeadroom } = ctx.vault;
const ceiling = bigintMin(maxSinglePosition, exposureHeadroom);
const convictionBps = BigInt(Math.round(conviction * 10_000));
const quantity = (ceiling * convictionBps) / 10_000n;
if (quantity === 0n) return [noop("no size")];
// Conviction-weighted trades pick from a mid-range rolling window
// (15min to 1h) — far enough out to let conviction signals matter,
// not so far that pricing is noisy.
const market = await Market.find({
asset: signal.asset,
expiryAfterMs: ctx.now + minutes(15),
expiringWithinMs: hours(1),
client: ctx.suiClient,
});
const p = await market.price();
const isUp = signal.direction === "up";
return [
{
kind: "vault.mintBinary",
params: {
marketId: market.id,
strike: isUp
? market.strikeAbove(p.forwardRaw, { pctBps: 300 })
: market.strikeBelow(p.forwardRaw, { pctBps: 300 }),
isUp,
quantity,
},
},
];
},
async onExecuted(ctx, result) {
if (result.outcome !== "submitted") return;
// Persist each mint into the ledger
for (const action of result.actions) {
if (action.kind !== "vault.mintBinary") continue;
const entry: TradeLog = {
digest: result.digest!,
recordedAtMs: ctx.now,
marketId: action.params.marketId,
strike: action.params.strike.toString(),
quantity: action.params.quantity.toString(),
isUp: action.params.isUp,
gasMist: result.gasUsed?.toString() ?? "0",
};
await ctx.state.set(`trade:${result.digest}`, entry);
}
// Periodic ledger summary (every ~10 trades)
const tradeKeys = (await ctx.state.keys()).filter((k) => k.startsWith("trade:"));
if (tradeKeys.length % 10 === 0) {
ctx.logger.info("ledger summary", { tradeCount: tradeKeys.length });
}
},
// Error counter keyed by phase — lets ops see at a glance whether the
// recent failures are RPC issues (transient) or on-chain rejections
// (logic problem). In a real plugin, push these counters to your
// metrics pipeline or trigger a webhook when one phase spikes.
async onError(ctx, err) {
const key = `errCount:${err.phase}`;
const current = await ctx.state.getOrDefault<number>(key, 0);
await ctx.state.set(key, current + 1);
ctx.logger.warn("execution error counted", {
phase: err.phase,
digest: err.digest,
error: err.error,
totalForPhase: current + 1,
});
},
};
}
Environment variables
- VAULT_ID
- SIGNAL_URL