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,
});
}
},
};
}
Environment variables
- VAULT_ID
- DB_PATH (optional, used by db.ts when activated)