typescript-financial-data-modeling

star 40

Type-safe data modeling patterns for financial and economic data visualization applications

C00ldudeNoonan By C00ldudeNoonan schedule Updated 5/13/2026

name: TypeScript Financial Data Modeling description: Type-safe data modeling patterns for financial and economic data visualization applications version: 1.0.0 tags: [typescript, financial-data, type-safety, zod, branded-types]

TypeScript Financial Data Modeling

Branded Types for Domain Safety

type Brand<T, B extends string> = T & { readonly __brand: B };

// Currency types
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;

export function usd(value: number): USD { return value as USD; }
export function eur(value: number): EUR { return value as EUR; }

// Type-safe operations
export function addUSD(a: USD, b: USD): USD { return (a + b) as USD; }
// addUSD(usd(100), eur(50)); // Compile error!

// Percentage and Ratio
type Percentage = Brand<number, 'Percentage'>;
type Ratio = Brand<number, 'Ratio'>;
type BasisPoints = Brand<number, 'BasisPoints'>;

export function percentage(value: number): Percentage {
  if (value < 0 || value > 100) throw new Error(`Invalid percentage: ${value}`);
  return value as Percentage;
}

export function percentageToRatio(pct: Percentage): Ratio {
  return (pct / 100) as Ratio;
}

Time Series Data Structures

export interface TimeSeriesPoint<T = number> {
  readonly timestamp: Date;
  readonly value: T;
}

export interface TimeSeries<T = number> {
  readonly id: string;
  readonly name: string;
  readonly unit: string;
  readonly frequency: 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly';
  readonly data: readonly TimeSeriesPoint<T>[];
  readonly metadata?: {
    readonly source: string;
    readonly lastUpdated: Date;
    readonly seasonallyAdjusted?: boolean;
  };
}

export interface OHLCPoint {
  readonly timestamp: Date;
  readonly open: number;
  readonly high: number;
  readonly low: number;
  readonly close: number;
  readonly volume?: number;
}

export interface StockSeries {
  readonly symbol: string;
  readonly exchange: string;
  readonly currency: 'USD' | 'EUR' | 'GBP';
  readonly interval: '1min' | '5min' | 'daily' | 'weekly';
  readonly data: readonly OHLCPoint[];
}

Economic Indicators with Discriminated Unions

interface BaseEconomicIndicator {
  readonly id: string;
  readonly country: string;
  readonly releaseDate: Date;
  readonly period: string;
}

export interface GDPIndicator extends BaseEconomicIndicator {
  readonly type: 'gdp';
  readonly value: number;
  readonly growthRate: number;
  readonly unit: 'billions_usd' | 'trillions_usd';
  readonly seasonallyAdjusted: boolean;
}

export interface InflationIndicator extends BaseEconomicIndicator {
  readonly type: 'inflation';
  readonly rate: number;
  readonly monthOverMonth: number;
  readonly yearOverYear: number;
}

export interface UnemploymentIndicator extends BaseEconomicIndicator {
  readonly type: 'unemployment';
  readonly rate: number;
  readonly laborForceParticipation: number;
}

export type EconomicIndicator = GDPIndicator | InflationIndicator | UnemploymentIndicator;

// Type guards
export function isGDPIndicator(i: EconomicIndicator): i is GDPIndicator {
  return i.type === 'gdp';
}

// Usage with narrowing
function formatIndicator(indicator: EconomicIndicator): string {
  switch (indicator.type) {
    case 'gdp': return `GDP: $${indicator.value}B (${indicator.growthRate}% growth)`;
    case 'inflation': return `Inflation: ${indicator.yearOverYear}% YoY`;
    case 'unemployment': return `Unemployment: ${indicator.rate}%`;
  }
}

Zod Runtime Validation

import { z } from 'zod';

export const OHLCSchema = z.object({
  timestamp: z.coerce.date(),
  open: z.number().positive(),
  high: z.number().positive(),
  low: z.number().positive(),
  close: z.number().positive(),
  volume: z.number().int().nonnegative().optional(),
}).refine(
  (data) => data.high >= data.low && data.high >= data.open && data.low <= data.close,
  { message: 'Invalid OHLC: high >= all, low <= all' }
);

export const TimeSeriesSchema = z.object({
  id: z.string().min(1),
  name: z.string().min(1),
  frequency: z.enum(['daily', 'weekly', 'monthly', 'quarterly', 'yearly']),
  data: z.array(z.object({
    timestamp: z.coerce.date(),
    value: z.number(),
  })),
});

// API response transformation
export const AlphaVantageQuoteSchema = z.object({
  'Global Quote': z.object({
    '01. symbol': z.string(),
    '05. price': z.string().transform(Number),
    '09. change': z.string().transform(Number),
    '10. change percent': z.string().transform(s => parseFloat(s.replace('%', ''))),
  }),
}).transform((data) => ({
  symbol: data['Global Quote']['01. symbol'],
  price: data['Global Quote']['05. price'],
  change: data['Global Quote']['09. change'],
  changePercent: data['Global Quote']['10. change percent'],
}));

Nivo Integration Types

import type { Serie, Datum } from '@nivo/line';

export interface FinancialDatum extends Datum {
  x: Date | string | number;
  y: number | null;
  metadata?: { volume?: number; change?: number };
}

export interface FinancialSerie extends Omit<Serie, 'data'> {
  id: string;
  data: readonly FinancialDatum[];
  color?: string;
}

export function toNivoSeries(timeSeries: TimeSeries[]): FinancialSerie[] {
  return timeSeries.map((series) => ({
    id: series.id,
    data: series.data.map((point) => ({ x: point.timestamp, y: point.value })),
  }));
}

Data Transformation Utilities

export function calculateMovingAverage(
  data: readonly TimeSeriesPoint<number>[],
  windowSize: number
): readonly TimeSeriesPoint<number>[] {
  return data.map((point, index) => {
    const start = Math.max(0, index - windowSize + 1);
    const window = data.slice(start, index + 1);
    const avg = window.reduce((sum, p) => sum + p.value, 0) / window.length;
    return { timestamp: point.timestamp, value: avg };
  });
}

export interface ChangeResult {
  readonly absolute: number;
  readonly percentage: number;
  readonly direction: 'up' | 'down' | 'unchanged';
}

export function calculateChange(current: number, previous: number): ChangeResult {
  const absolute = current - previous;
  const percentage = previous !== 0 ? (absolute / previous) * 100 : 0;
  return {
    absolute,
    percentage,
    direction: absolute > 0 ? 'up' : absolute < 0 ? 'down' : 'unchanged',
  };
}

API Response Types

export type ApiResponse<T> = 
  | { status: 'success'; data: T; timestamp: Date }
  | { status: 'error'; error: { code: string; message: string } }
  | { status: 'loading' };

export interface PaginatedResponse<T> {
  data: readonly T[];
  pagination: {
    page: number;
    pageSize: number;
    totalItems: number;
    hasNext: boolean;
  };
}

Testing Utilities

type DataFactory<T> = (overrides?: Partial<T>) => T;

export const createMockOHLC: DataFactory<OHLCPoint> = (overrides = {}) => ({
  timestamp: new Date(),
  open: 100,
  high: 105,
  low: 98,
  close: 102,
  volume: 1000000,
  ...overrides,
});

export function generatePriceData(startPrice: number, days: number, volatility = 0.02): OHLCPoint[] {
  let price = startPrice;
  return Array.from({ length: days }, (_, i) => {
    const change = (Math.random() - 0.5) * volatility * price;
    const open = price;
    const close = price + change;
    const result = {
      timestamp: new Date(2024, 0, i + 1),
      open, close,
      high: Math.max(open, close) * 1.01,
      low: Math.min(open, close) * 0.99,
      volume: Math.floor(Math.random() * 10000000),
    };
    price = close;
    return result;
  });
}
Install via CLI
npx skills add https://github.com/C00ldudeNoonan/economic-data-project --skill typescript-financial-data-modeling
Repository Details
star Stars 40
call_split Forks 8
navigation Branch main
article Path SKILL.md
More from Creator
C00ldudeNoonan
C00ldudeNoonan Explore all skills →