Skip to content
Runner · ExamplesLadderrung 06

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