fed-policy-cli
Version:
Macro trading intelligence from Fed policy analysis. Transform economic data into actionable trading insights by analyzing historical Fed policy analogues.
582 lines (530 loc) • 15.7 kB
text/typescript
// /src/services/database.ts
import sqlite3 from 'sqlite3';
import { DB_PATH, FRED_SERIES, CROSS_ASSET_SERIES } from '../constants';
import { EconomicDataPoint } from '../types';
import { ETFDataPoint, ETFFundamentals } from './etfDataService';
let db: sqlite3.Database;
const getDb = () => {
if (!db) {
db = new sqlite3.Database(DB_PATH);
}
return db;
};
export const initDatabase = (): Promise<void> => {
return new Promise((resolve, reject) => {
const db = getDb();
// First, check if the table exists and get its schema
db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='economic_data'", (err, row) => {
if (err) {
reject(err);
return;
}
if (row) {
// Table exists, check if we need to add new columns
db.all("PRAGMA table_info(economic_data)", (err, columns: any[]) => {
if (err) {
reject(err);
return;
}
const existingColumns = new Set(columns.map(col => col.name));
const requiredColumns = Object.keys(FRED_SERIES);
const missingColumns = requiredColumns.filter(col => !existingColumns.has(col));
if (missingColumns.length > 0) {
// Add missing columns
const addColumnPromises = missingColumns.map(columnName => {
return new Promise<void>((resolve, reject) => {
const alterQuery = `ALTER TABLE economic_data ADD COLUMN ${columnName} REAL`;
db.run(alterQuery, (err) => {
if (err) {
reject(err);
} else {
console.log(`Added column: ${columnName}`);
resolve();
}
});
});
});
Promise.all(addColumnPromises)
.then(() => resolve())
.catch(reject);
} else {
resolve();
}
});
} else {
// Table doesn't exist, create it
const columns = Object.keys(FRED_SERIES)
.map(key => `${key} REAL`)
.join(', \n');
const query = `
CREATE TABLE economic_data (
date TEXT PRIMARY KEY,
${columns}
)
`;
db.run(query, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}
});
});
};
export const insertData = (data: EconomicDataPoint[]): Promise<void> => {
return new Promise((resolve, reject) => {
if (data.length === 0) {
return resolve();
}
const db = getDb();
const allKeys = Object.keys(FRED_SERIES);
const columns = ['date', ...allKeys];
const placeholders = columns.map(() => '?').join(', ');
const stmt = db.prepare(`
INSERT OR REPLACE INTO economic_data (${columns.join(', ')})
VALUES (${placeholders})
`);
db.serialize(() => {
db.run('BEGIN TRANSACTION');
data.forEach(point => {
const values = columns.map(col => point[col] ?? null);
stmt.run(values);
});
db.run('COMMIT', (err) => {
if (err) {
db.run('ROLLBACK');
reject(err);
} else {
stmt.finalize(err => {
if (err) reject(err);
else resolve();
});
}
});
});
});
};
export const getAllData = (): Promise<EconomicDataPoint[]> => {
return new Promise((resolve, reject) => {
const db = getDb();
db.all('SELECT * FROM economic_data ORDER BY date ASC', (err, rows: EconomicDataPoint[]) => {
if (err) {
reject(err);
} else {
resolve(rows || []);
}
});
});
};
// Initialize FOMC projections table for Fed dot plot data
export const initProjectionsTable = (): Promise<void> => {
return new Promise((resolve, reject) => {
const db = getDb();
const query = `
CREATE TABLE IF NOT EXISTS fomc_projections (
meeting_date TEXT NOT NULL,
projection_year TEXT NOT NULL,
median_rate REAL,
range_midpoint REAL,
range_low REAL,
range_high REAL,
longer_run_median REAL,
PRIMARY KEY (meeting_date, projection_year)
)
`;
db.run(query, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
// Insert FOMC projection data
export interface FOMCProjection {
meeting_date: string;
projection_year: string;
median_rate?: number;
range_midpoint?: number;
range_low?: number;
range_high?: number;
longer_run_median?: number;
}
export const insertProjections = (projections: FOMCProjection[]): Promise<void> => {
return new Promise((resolve, reject) => {
if (projections.length === 0) {
return resolve();
}
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO fomc_projections
(meeting_date, projection_year, median_rate, range_midpoint, range_low, range_high, longer_run_median)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
db.serialize(() => {
db.run('BEGIN TRANSACTION');
projections.forEach(proj => {
stmt.run([
proj.meeting_date,
proj.projection_year,
proj.median_rate ?? null,
proj.range_midpoint ?? null,
proj.range_low ?? null,
proj.range_high ?? null,
proj.longer_run_median ?? null
]);
});
db.run('COMMIT', (err) => {
if (err) {
db.run('ROLLBACK');
reject(err);
} else {
stmt.finalize(err => {
if (err) reject(err);
else resolve();
});
}
});
});
});
};
// Get latest FOMC projections
export const getLatestProjections = (): Promise<FOMCProjection[]> => {
return new Promise((resolve, reject) => {
const db = getDb();
const query = `
SELECT * FROM fomc_projections
WHERE meeting_date = (SELECT MAX(meeting_date) FROM fomc_projections)
ORDER BY projection_year ASC
`;
db.all(query, (err, rows: FOMCProjection[]) => {
if (err) {
reject(err);
} else {
resolve(rows || []);
}
});
});
};
// Cross-Asset Data Tables and Functions
// Initialize cross-asset data table for FRED commodities/currencies
export const initCrossAssetTable = (): Promise<void> => {
return new Promise((resolve, reject) => {
const db = getDb();
// Check if the table exists and get its schema
db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='cross_asset_data'", (err, row) => {
if (err) {
reject(err);
return;
}
if (row) {
// Table exists, check if we need to add new columns
db.all("PRAGMA table_info(cross_asset_data)", (err, columns: any[]) => {
if (err) {
reject(err);
return;
}
const existingColumns = new Set(columns.map(col => col.name));
const requiredColumns = Object.keys(CROSS_ASSET_SERIES);
const missingColumns = requiredColumns.filter(col => !existingColumns.has(col));
if (missingColumns.length > 0) {
// Add missing columns
const addColumnPromises = missingColumns.map(columnName => {
return new Promise<void>((resolve, reject) => {
const alterQuery = `ALTER TABLE cross_asset_data ADD COLUMN ${columnName} REAL`;
db.run(alterQuery, (err) => {
if (err) {
reject(err);
} else {
console.log(`Added cross-asset column: ${columnName}`);
resolve();
}
});
});
});
Promise.all(addColumnPromises)
.then(() => resolve())
.catch(reject);
} else {
resolve();
}
});
} else {
// Table doesn't exist, create it
const columns = Object.keys(CROSS_ASSET_SERIES)
.map(key => `${key} REAL`)
.join(', \n');
const query = `
CREATE TABLE cross_asset_data (
date TEXT PRIMARY KEY,
${columns}
)
`;
db.run(query, (err) => {
if (err) {
reject(err);
} else {
console.log('Created cross_asset_data table');
resolve();
}
});
}
});
});
};
// Initialize ETF data table for Alpha Vantage data
export const initETFTable = (): Promise<void> => {
return new Promise((resolve, reject) => {
const db = getDb();
const query = `
CREATE TABLE IF NOT EXISTS etf_data (
symbol TEXT NOT NULL,
name TEXT NOT NULL,
date TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume INTEGER,
asset_class TEXT,
PRIMARY KEY (symbol, date)
)
`;
db.run(query, (err) => {
if (err) {
reject(err);
} else {
console.log('Initialized etf_data table');
resolve();
}
});
});
};
// Initialize ETF fundamentals table
export const initETFFundamentalsTable = (): Promise<void> => {
return new Promise((resolve, reject) => {
const db = getDb();
const query = `
CREATE TABLE IF NOT EXISTS etf_fundamentals (
symbol TEXT PRIMARY KEY,
name TEXT NOT NULL,
net_assets REAL,
expense_ratio REAL,
yield_percent REAL,
asset_class TEXT,
last_updated TEXT DEFAULT CURRENT_TIMESTAMP
)
`;
db.run(query, (err) => {
if (err) {
reject(err);
} else {
console.log('Initialized etf_fundamentals table');
resolve();
}
});
});
};
// Cross-asset data point interface
export interface CrossAssetDataPoint {
date: string;
[key: string]: string | number | null | undefined;
}
// Insert cross-asset FRED data
export const insertCrossAssetData = (data: CrossAssetDataPoint[]): Promise<void> => {
return new Promise((resolve, reject) => {
if (data.length === 0) {
return resolve();
}
const db = getDb();
const allKeys = Object.keys(CROSS_ASSET_SERIES);
const columns = ['date', ...allKeys];
const placeholders = columns.map(() => '?').join(', ');
const stmt = db.prepare(`
INSERT OR REPLACE INTO cross_asset_data (${columns.join(', ')})
VALUES (${placeholders})
`);
db.serialize(() => {
db.run('BEGIN TRANSACTION');
data.forEach(point => {
const values = columns.map(col => point[col] ?? null);
stmt.run(values);
});
db.run('COMMIT', (err) => {
if (err) {
db.run('ROLLBACK');
reject(err);
} else {
stmt.finalize(err => {
if (err) reject(err);
else resolve();
});
}
});
});
});
};
// Insert ETF historical data
export const insertETFData = (data: ETFDataPoint[]): Promise<void> => {
return new Promise((resolve, reject) => {
if (data.length === 0) {
return resolve();
}
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO etf_data
(symbol, name, date, open, high, low, close, volume, asset_class)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
db.serialize(() => {
db.run('BEGIN TRANSACTION');
data.forEach(point => {
stmt.run([
point.symbol,
point.name,
point.date,
point.open,
point.high,
point.low,
point.close,
point.volume,
null // asset_class will be set from fundamentals
]);
});
db.run('COMMIT', (err) => {
if (err) {
db.run('ROLLBACK');
reject(err);
} else {
stmt.finalize(err => {
if (err) reject(err);
else resolve();
});
}
});
});
});
};
// Insert ETF fundamentals
export const insertETFFundamentals = (fundamentals: ETFFundamentals[]): Promise<void> => {
return new Promise((resolve, reject) => {
if (fundamentals.length === 0) {
return resolve();
}
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO etf_fundamentals
(symbol, name, net_assets, expense_ratio, yield_percent, asset_class)
VALUES (?, ?, ?, ?, ?, ?)
`);
db.serialize(() => {
db.run('BEGIN TRANSACTION');
fundamentals.forEach(fund => {
stmt.run([
fund.symbol,
fund.name,
fund.netAssets,
fund.expenseRatio,
fund.yieldPercent,
fund.assetClass
]);
});
db.run('COMMIT', (err) => {
if (err) {
db.run('ROLLBACK');
reject(err);
} else {
stmt.finalize(err => {
if (err) reject(err);
else resolve();
});
}
});
});
});
};
// Get all cross-asset data
export const getAllCrossAssetData = (): Promise<CrossAssetDataPoint[]> => {
return new Promise((resolve, reject) => {
const db = getDb();
db.all('SELECT * FROM cross_asset_data ORDER BY date ASC', (err, rows: CrossAssetDataPoint[]) => {
if (err) {
reject(err);
} else {
resolve(rows || []);
}
});
});
};
// Get all ETF data
export const getAllETFData = (): Promise<ETFDataPoint[]> => {
return new Promise((resolve, reject) => {
const db = getDb();
db.all('SELECT * FROM etf_data ORDER BY symbol, date ASC', (err, rows: any[]) => {
if (err) {
reject(err);
} else {
const etfData: ETFDataPoint[] = rows.map(row => ({
symbol: row.symbol,
name: row.name,
date: row.date,
open: row.open,
high: row.high,
low: row.low,
close: row.close,
volume: row.volume
}));
resolve(etfData);
}
});
});
};
// Get ETF data for specific symbols
export const getETFDataBySymbols = (symbols: string[]): Promise<ETFDataPoint[]> => {
return new Promise((resolve, reject) => {
const db = getDb();
const placeholders = symbols.map(() => '?').join(', ');
const query = `SELECT * FROM etf_data WHERE symbol IN (${placeholders}) ORDER BY symbol, date ASC`;
db.all(query, symbols, (err, rows: any[]) => {
if (err) {
reject(err);
} else {
const etfData: ETFDataPoint[] = rows.map(row => ({
symbol: row.symbol,
name: row.name,
date: row.date,
open: row.open,
high: row.high,
low: row.low,
close: row.close,
volume: row.volume
}));
resolve(etfData);
}
});
});
};
// Get ETF fundamentals
export const getAllETFFundamentals = (): Promise<ETFFundamentals[]> => {
return new Promise((resolve, reject) => {
const db = getDb();
db.all('SELECT * FROM etf_fundamentals ORDER BY symbol ASC', (err, rows: any[]) => {
if (err) {
reject(err);
} else {
const fundamentals: ETFFundamentals[] = rows.map(row => ({
symbol: row.symbol,
name: row.name,
netAssets: row.net_assets,
expenseRatio: row.expense_ratio,
yieldPercent: row.yield_percent,
assetClass: row.asset_class
}));
resolve(fundamentals);
}
});
});
};