External API + sized trade + cooldown
signal + sized + cooldown combo
Mirrors runner/src/plugins/06-exampleCombo
What this teaches
- All the moderate-tier patterns combined: fetch signal, size against caps, respect cooldown, trade
- This is roughly where a "v1 production plugin" lives
New vs exampleCooldown
- Wires the external signal back in (was off in exampleCooldown)
import type { StrategyPlugin } from "@automark/runtime-core";
import { Market } from "@automark/sdk/market";
import { minutes } from "@automark/sdk/duration";
interface Signal {
action: "hold" | "buy" | "sell";
asset: string;
pctBps: number
}
const COOLDOWN_MS = minutes(15);
export default function createExampleCombo(): StrategyPlugin {
const vaultId = process.env.VAULT_ID;
if (!vaultId) throw new Error("exampleCombo: VAULT_ID not set");
const signalUrl =
process.env.SIGNAL_URL ?? "https://your-backend.example/signal";
return {
name: "exampleCombo",
vaultId,
triggers: [{ kind: "cron", everySeconds: 60 }],
async decide(ctx) {
if (ctx.vault.isFrozen) return [{ kind: "noop", reason: "frozen" }];
const lastTradeMs = (await ctx.state.get<number>("lastTradeMs")) ?? 0;
if (ctx.now - lastTradeMs < COOLDOWN_MS) {
return [{ kind: "noop", reason: "cooldown" }];
}
const { action, asset, pctBps } = (await (await fetch(signalUrl)).json()) as Signal;
if (action === "hold") return [{ kind: "noop", reason: "hold" }];
const { maxSinglePosition, exposureHeadroom } = ctx.vault;
const ceiling =
maxSinglePosition < exposureHeadroom ? maxSinglePosition : exposureHeadroom;
const quantity = ceiling / 2n;
if (quantity === 0n) return [{ kind: "noop", reason: "no headroom" }];
// Rolling near-term window: markets expiring in the next 5–15 minutes.
// expiryAfterMs is the floor, expiringWithinMs is the ceiling RELATIVE
// to now — slides forward with each tick.
const market = await Market.find({
asset,
expiryAfterMs: ctx.now + minutes(5),
expiringWithinMs: minutes(15),
client: ctx.suiClient,
});
const p = await market.price();
const isUp = action === "buy";
await ctx.state.set("lastTradeMs", ctx.now);
return [
{
kind: "vault.mintBinary",
params: {
marketId: market.id,
strike: isUp
? market.strikeAbove(p.forwardRaw, { pctBps })
: market.strikeBelow(p.forwardRaw, { pctBps }),
isUp,
quantity,
},
},
];
},
};
}
// insights we can take from this example:
// There are no limits to what your backend can deliver: complex logic, sub-second, etc.
Environment variables
- VAULT_ID
- SIGNAL_URL