264 lines
8.9 KiB
TypeScript
264 lines
8.9 KiB
TypeScript
// GammaDesk shared data model
|
|
// ORATS-style options analytics types + Drizzle tables for auth.
|
|
// Database: PostgreSQL
|
|
|
|
import { z } from "zod";
|
|
import { pgTable, text, serial, timestamp, boolean } from "drizzle-orm/pg-core";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Auth - Users table (Drizzle + PostgreSQL)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const users = pgTable("users", {
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
email: text("email").notNull().unique(),
|
|
passwordHash: text("password_hash").notNull(),
|
|
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
verificationToken: text("verification_token"),
|
|
verificationTokenExpiry: timestamp("verification_token_expiry", { mode: "date" }),
|
|
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
});
|
|
|
|
export type User = typeof users.$inferSelect;
|
|
export type NewUser = typeof users.$inferInsert;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Custom Symbols - User-added tickers stored in DB
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const customSymbols = pgTable("custom_symbols", {
|
|
id: serial("id").primaryKey(),
|
|
ticker: text("ticker").notNull().unique(),
|
|
name: text("name").notNull(),
|
|
type: text("type").default("etf").notNull(), // etf, equity, index
|
|
sector: text("sector"),
|
|
userId: text("user_id"), // null = global, otherwise tied to a user
|
|
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
|
|
});
|
|
|
|
export type CustomSymbol = typeof customSymbols.$inferSelect;
|
|
export type NewCustomSymbol = typeof customSymbols.$inferInsert;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Symbols
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const symbolSchema = z.object({
|
|
ticker: z.string(),
|
|
name: z.string(),
|
|
type: z.enum(["index", "etf", "equity"]),
|
|
sector: z.string().optional(),
|
|
});
|
|
|
|
export type SymbolInfo = z.infer<typeof symbolSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Summary card payload — what the Dashboard top row shows
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const summarySchema = z.object({
|
|
ticker: z.string(),
|
|
spot: z.number(),
|
|
spotChange: z.number(), // absolute $ change vs prior close
|
|
spotChangePct: z.number(),
|
|
netGex: z.number(), // signed, in $ per 1% move
|
|
gammaRegime: z.enum(["positive", "negative"]),
|
|
hvl: z.number(), // High Volume Level / gamma flip
|
|
callWall: z.number(),
|
|
putWall: z.number(),
|
|
ivRank: z.number(), // 0-100
|
|
ivx: z.number(), // implied vol index, percent
|
|
expectedMove: z.number(), // absolute $ expected move next session
|
|
expectedMovePct: z.number(),
|
|
skew: z.number(), // put skew minus call skew, percent points
|
|
asOf: z.string(), // ISO timestamp
|
|
});
|
|
|
|
export type Summary = z.infer<typeof summarySchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GEX profile — bars by strike for the gamma-by-strike visualization
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const gexBarSchema = z.object({
|
|
strike: z.number(),
|
|
callGex: z.number(), // $ gamma exposure attributable to call OI
|
|
putGex: z.number(), // negative or positive depending on sign convention
|
|
netGex: z.number(),
|
|
});
|
|
|
|
export const gexProfileSchema = z.object({
|
|
ticker: z.string(),
|
|
spot: z.number(),
|
|
hvl: z.number(),
|
|
callWall: z.number(),
|
|
putWall: z.number(),
|
|
bars: z.array(gexBarSchema),
|
|
asOf: z.string(),
|
|
});
|
|
|
|
export type GexBar = z.infer<typeof gexBarSchema>;
|
|
export type GexProfile = z.infer<typeof gexProfileSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Expirations matrix
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const expirationRowSchema = z.object({
|
|
expiry: z.string(), // ISO date
|
|
dte: z.number(),
|
|
netGex: z.number(),
|
|
ivx: z.number(),
|
|
skew: z.number(),
|
|
expectedMove: z.number(),
|
|
expectedMovePct: z.number(),
|
|
callWall: z.number(),
|
|
putWall: z.number(),
|
|
callSkew: z.number(), // for IV/skew curve chart
|
|
putSkew: z.number(),
|
|
});
|
|
|
|
export const expirationsResponseSchema = z.object({
|
|
ticker: z.string(),
|
|
spot: z.number(),
|
|
rows: z.array(expirationRowSchema),
|
|
asOf: z.string(),
|
|
});
|
|
|
|
export type ExpirationRow = z.infer<typeof expirationRowSchema>;
|
|
export type ExpirationsResponse = z.infer<typeof expirationsResponseSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Screener
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const screenerPresetSchema = z.enum([
|
|
"all",
|
|
"negative-gamma",
|
|
"high-iv-rank",
|
|
"near-call-wall",
|
|
"near-put-wall",
|
|
"zero-dte",
|
|
]);
|
|
|
|
export type ScreenerPreset = z.infer<typeof screenerPresetSchema>;
|
|
|
|
export const screenerRowSchema = z.object({
|
|
ticker: z.string(),
|
|
name: z.string(),
|
|
spot: z.number(),
|
|
spotChangePct: z.number(),
|
|
netGex: z.number(),
|
|
gammaRegime: z.enum(["positive", "negative"]),
|
|
ivRank: z.number(),
|
|
ivx: z.number(),
|
|
callWall: z.number(),
|
|
putWall: z.number(),
|
|
distanceToCallWall: z.number(), // % away from spot (signed: + if wall above)
|
|
distanceToPutWall: z.number(),
|
|
expectedMovePct: z.number(),
|
|
skew: z.number(),
|
|
zeroDteNetGex: z.number(),
|
|
});
|
|
|
|
export type ScreenerRow = z.infer<typeof screenerRowSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Futures conversion: ETF/Index → E-mini futures equivalents
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ETF/Index -> Futures mapping with conversion ratio
|
|
export const futuresConversionSchema = z.object({
|
|
sourceTicker: z.string(), // e.g., "SPY", "QQQ", "SPX"
|
|
targetTicker: z.string(), // e.g., "ES", "NQ", "YM"
|
|
targetName: z.string(), // e.g., "E-mini S&P 500", "E-mini Nasdaq-100"
|
|
multiplier: z.number(), // multiply ETF price to get futures price
|
|
});
|
|
|
|
export type FuturesConversion = z.infer<typeof futuresConversionSchema>;
|
|
|
|
// Known mappings (ETF/Index price → futures price)
|
|
export const FUTURES_CONVERSIONS: FuturesConversion[] = [
|
|
{ sourceTicker: "SPY", targetTicker: "ES", targetName: "E-mini S&P 500 (ES)", multiplier: 10 },
|
|
{ sourceTicker: "SPX", targetTicker: "ES", targetName: "E-mini S&P 500 (ES)", multiplier: 1 },
|
|
{ sourceTicker: "QQQ", targetTicker: "NQ", targetName: "E-mini Nasdaq-100 (NQ)", multiplier: 20 },
|
|
{ sourceTicker: "IWM", targetTicker: "YM", targetName: "E-mini DJIA (YM)", multiplier: 10 },
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Gamma levels mapped to futures
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const futuresLevelSchema = z.object({
|
|
sourceTicker: z.string(),
|
|
targetTicker: z.string(),
|
|
targetName: z.string(),
|
|
sourceSpot: z.number(),
|
|
futuresSpot: z.number(),
|
|
hvl: z.number(),
|
|
callWall: z.number(),
|
|
putWall: z.number(),
|
|
sourceHvl: z.number(),
|
|
sourceCallWall: z.number(),
|
|
sourcePutWall: z.number(),
|
|
topStrikes: z.array(z.object({
|
|
sourceStrike: z.number(),
|
|
futuresStrike: z.number(),
|
|
netGex: z.number(),
|
|
magnitude: z.number(),
|
|
})),
|
|
asOf: z.string(),
|
|
});
|
|
|
|
export type FuturesLevel = z.infer<typeof futuresLevelSchema>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tanuki Trades — 5-level gamma profile + extended levels
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const tanukiLevelSchema = z.object({
|
|
role: z.string(), // "Key Resistance", "Minor Resistance", "Gamma Flip", "Minor Support", "Key Support"
|
|
strike: z.number(),
|
|
label: z.string(), // "C1", "cTrans", "HVL", "pTrans", "P1"
|
|
});
|
|
|
|
export const tanukiExtendedLevelSchema = z.object({
|
|
label: z.string(), // "C2", "C3", "P2", "P3"
|
|
strike: z.number().optional(),
|
|
});
|
|
|
|
export const tanukiRegimeSchema = z.enum([
|
|
"Positive Gamma",
|
|
"Negative Gamma",
|
|
"Positive Extension",
|
|
"Transition",
|
|
"Negative Transition",
|
|
]);
|
|
|
|
export const tanukiLevelsSchema = z.object({
|
|
symbol: z.string(),
|
|
spotPrice: z.number(),
|
|
expiry: z.string(), // ISO date
|
|
expiryDte: z.number(),
|
|
regime: tanukiRegimeSchema,
|
|
updatedAt: z.string(), // ISO timestamp
|
|
levels: z.array(tanukiLevelSchema),
|
|
extendedLevels: z.array(tanukiExtendedLevelSchema),
|
|
netGex: z.number(),
|
|
netDex: z.number(),
|
|
});
|
|
|
|
export type TanukiLevel = z.infer<typeof tanukiLevelSchema>;
|
|
export type TanukiExtendedLevel = z.infer<typeof tanukiExtendedLevelSchema>;
|
|
export type TanukiRegime = z.infer<typeof tanukiRegimeSchema>;
|
|
export type TanukiLevels = z.infer<typeof tanukiLevelsSchema>;
|
|
|
|
// Tanuki config stored server-side
|
|
export const tanukiConfigSchema = z.object({
|
|
email: z.string().email(),
|
|
tvUser: z.string(),
|
|
});
|
|
|
|
export type TanukiConfig = z.infer<typeof tanukiConfigSchema>;
|