File size: 4,785 Bytes
6b57d90 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | import type { MarketSnapshot, Period } from "./types.js";
import { resolveSymbol } from "./symbols.js";
export const INPUT_SCHEMAS = {
runners: {
period: { type: "string", enum: ["daily", "weekly", "monthly"] },
market: { type: "string", enum: ["perp", "spot"], default: "perp" },
direction: { type: "string", enum: ["up", "down"], default: "up" },
limit: { type: "integer", min: 1, max: 50, default: 10 },
minVolumeUsd: { type: "number", min: 0, default: 100000 },
},
signal: {
symbol: { type: "string", required: true },
period: {
type: "string",
enum: ["daily", "weekly", "monthly"],
default: "weekly",
},
},
preipo: {
symbol: { type: "string", required: false },
},
} as const;
export interface RunnerInput {
period: Period;
market: "perp" | "spot";
direction: "up" | "down";
limit: number;
minVolumeUsd: number;
}
export interface SignalInput {
symbol: string;
period: Period;
}
type Validation<T> =
| { ok: true; value: T }
| { ok: false; error: string }
| { ok: false; missing: string[]; required: unknown };
function recordBody(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return value as Record<string, unknown>;
}
function unknownKeys(
body: Record<string, unknown>,
accepted: string[],
): string | null {
const unknown = Object.keys(body).find((key) => !accepted.includes(key));
return unknown ? `unknown field: ${unknown}` : null;
}
function periodValue(value: unknown): value is Period {
return value === "daily" || value === "weekly" || value === "monthly";
}
export function validateRunnersInput(input: unknown): Validation<RunnerInput> {
const body = recordBody(input);
if (body.period === undefined) {
return {
ok: false,
missing: ["period"],
required: INPUT_SCHEMAS.runners,
};
}
const extra = unknownKeys(body, [
"period",
"market",
"direction",
"limit",
"minVolumeUsd",
]);
if (extra) return { ok: false, error: extra };
if (!periodValue(body.period)) {
return { ok: false, error: "period must be daily, weekly, or monthly" };
}
const market = body.market ?? "perp";
if (market !== "perp" && market !== "spot") {
return { ok: false, error: "market must be perp or spot" };
}
const direction = body.direction ?? "up";
if (direction !== "up" && direction !== "down") {
return { ok: false, error: "direction must be up or down" };
}
const limit = body.limit ?? 10;
if (
typeof limit !== "number" ||
!Number.isInteger(limit) ||
limit < 1 ||
limit > 50
) {
return { ok: false, error: "limit must be an integer from 1 to 50" };
}
const minVolumeUsd = body.minVolumeUsd ?? 100_000;
if (
typeof minVolumeUsd !== "number" ||
!Number.isFinite(minVolumeUsd) ||
minVolumeUsd < 0
) {
return { ok: false, error: "minVolumeUsd must be a non-negative number" };
}
return {
ok: true,
value: { period: body.period, market, direction, limit, minVolumeUsd },
};
}
export function validateSignalInput(
input: unknown,
snapshot?: MarketSnapshot,
): Validation<SignalInput> {
const body = recordBody(input);
if (body.symbol === undefined) {
return {
ok: false,
missing: ["symbol"],
required: INPUT_SCHEMAS.signal,
};
}
const extra = unknownKeys(body, ["symbol", "period"]);
if (extra) return { ok: false, error: extra };
if (typeof body.symbol !== "string" || !body.symbol.trim()) {
return { ok: false, error: "symbol must be a non-empty string" };
}
const period = body.period ?? "weekly";
if (!periodValue(period)) {
return { ok: false, error: "period must be daily, weekly, or monthly" };
}
if (snapshot && !resolveSymbol(body.symbol, snapshot)) {
return {
ok: false,
error: `unknown symbol: ${body.symbol}`,
};
}
return { ok: true, value: { symbol: body.symbol, period } };
}
export function validatePreIpoInput(
input: unknown,
snapshot?: MarketSnapshot,
): Validation<{ symbol?: string }> {
const body = recordBody(input);
const extra = unknownKeys(body, ["symbol"]);
if (extra) return { ok: false, error: extra };
if (body.symbol === undefined) return { ok: true, value: {} };
if (typeof body.symbol !== "string" || !body.symbol.trim()) {
return { ok: false, error: "symbol must be a non-empty string" };
}
if (snapshot) {
const resolved = resolveSymbol(body.symbol, snapshot);
if (!resolved || resolved.market !== "perp" || !resolved.instrument.preIpo) {
return {
ok: false,
error: `symbol is not a pre-IPO contract: ${body.symbol}`,
};
}
}
return { ok: true, value: { symbol: body.symbol } };
}
|