Skip to content
Runner · ExamplesLadderrung 14

Plugin as a folder, with its own local DB

schema + db helper next to index.ts (stubs default)

Mirrors runner/src/plugins/14-exampleLocalDb

What this teaches

  • A plugin can be a FOLDER instead of a single file. Node/TS resolves `./14-exampleLocalDb` to `./14-exampleLocalDb/index.ts` automatically — the registry import line stays the same.
  • Co-locate plugin-specific persistence (`db.ts`) next to the plugin. Clean separation from `ctx.state` (which is shared KV) vs your own schema-rich storage.
  • Real-world strategy data (price history for moving averages, trade ledgers, custom metrics) doesn't fit in KV — it belongs in a real DB.

New vs examplePipeline

  • Plugin is a folder, not a single file
  • DB adapter lives in `db.ts` (sibling), with Drizzle stubs by default Strategy logic (toy example — replace with yours): 1. Every tick, record the BTC spot price into a local time-series table 2. Once we have enough samples (SMA_WINDOW), compute the simple moving average over the last N spots 3. If current spot is X bps above the SMA → trend-follow with a binary mint at +3% above forward 4. Persist every successful trade into the same DB via onExecuted
import type { StrategyPlugin } from "@automark/runtime-core";
import { Market } from "@automark/sdk/market";
import { minutes } from "@automark/sdk/duration";
import { recentPrices, recordPriceSample, recordTrade } from "./db";

const SMA_WINDOW = 30;          // samples to average (~30min at 60s cron)
const TREND_THRESHOLD_BPS = 200n;   // 2% above SMA triggers a mint

export default function createExampleLocalDb(): StrategyPlugin {
  const vaultId = process.env.VAULT_ID;
  if (!vaultId) throw new Error("exampleLocalDb: VAULT_ID not set");

  return {
    name: "exampleLocalDb",
    vaultId,
    triggers: [{ kind: "cron", everySeconds: 60 }],

    async decide(ctx) {
      if (ctx.vault.isFrozen) return [{ kind: "noop", reason: "frozen" }];

      const btc = await Market.find({
        asset: "BTC",
        expiryAfterMs: ctx.now + minutes(5),
        client: ctx.suiClient,
      });
      const p = await btc.price();

      // Record the current sample for future SMA calculations
      await recordPriceSample({
        asset: "BTC",
        atMs: ctx.now,
        spotRaw: p.spotRaw,
      });

      // Read back the most recent N samples and compute SMA
      const history = await recentPrices({ asset: "BTC", limit: SMA_WINDOW });
      if (history.length < SMA_WINDOW) {
        return [
          {
            kind: "noop",
            reason: `warming up (${history.length}/${SMA_WINDOW} samples) — stubs default to empty; install drizzle to persist`,
          },
        ];
      }

      const sum = history.reduce((acc, s) => acc + s.spotRaw, 0n);
      const sma = sum / BigInt(history.length);
      const deviationBps =
        p.spotRaw > sma
          ? ((p.spotRaw - sma) * 10_000n) / sma
          : -(((sma - p.spotRaw) * 10_000n) / sma);

      ctx.logger.info("trend check", {
        spot: p.spotUsd,
        smaRaw: sma.toString(),
        deviationBps: deviationBps.toString(),
      });

      if (deviationBps < TREND_THRESHOLD_BPS) {
        return [{ kind: "noop", reason: `within range (${deviationBps}bps)` }];
      }

      const quantity = ctx.vault.maxSinglePosition / 4n;
      if (quantity === 0n) return [{ kind: "noop", reason: "no headroom" }];

      return [
        {
          kind: "vault.mintBinary",
          params: {
            marketId: btc.id,
            strike: btc.strikeAbove(p.forwardRaw, { pctBps: 300 }),
            isUp: true,
            quantity,
          },
        },
      ];
    },

    async onExecuted(ctx, result) {
      // Don't pollute the trade history table with fake dry-run trades
      if (result.dryRun) return;
      if (result.outcome !== "submitted") return;
      for (const action of result.actions) {
        if (action.kind !== "vault.mintBinary") continue;
        await recordTrade({
          digest: result.digest!,
          marketId: action.params.marketId,
          strike: action.params.strike,
          quantity: action.params.quantity,
          isUp: action.params.isUp,
          gasMist: result.gasUsed ?? 0n,
          atMs: ctx.now,
        });
      }
    },
  };
}
Go deeperPersistent stateWhen in-memory isn't enough: plugin KV and richer data, with Drizzle.

Environment variables

  • VAULT_ID
  • DB_PATH (optional, used by db.ts when activated)