feat: add stock indicators route and Alpaca account info
- New /stocks route with StockViewer component - New /api/indicators endpoint with SMA, EMA, RSI, MACD - New /api/alpaca/account endpoint - AlpacaAccountInfo component on home page - Indicator calculation utilities - Tests for utilities and components - Vite proxy config for /api
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
calculateSMA,
|
||||
calculateEMA,
|
||||
calculateRSI,
|
||||
calculateMACD,
|
||||
} from "../../utils/indicators";
|
||||
|
||||
describe("calculateSMA", () => {
|
||||
it("returns 0 when prices length < period", () => {
|
||||
expect(calculateSMA([1, 2], 5)).toBe(0);
|
||||
});
|
||||
|
||||
it("calculates simple moving average correctly", () => {
|
||||
expect(calculateSMA([1, 2, 3, 4, 5], 5)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateEMA", () => {
|
||||
it("returns 0 when prices length < period", () => {
|
||||
expect(calculateEMA([1, 2], 5)).toBe(0);
|
||||
});
|
||||
|
||||
it("calculates exponential moving average", () => {
|
||||
const prices = [10, 11, 12, 13, 14, 15];
|
||||
const result = calculateEMA(prices, 3);
|
||||
expect(result).toBeCloseTo(14.125, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateRSI", () => {
|
||||
it("returns 0 when prices length < period + 1", () => {
|
||||
expect(calculateRSI([1, 2, 3], 5)).toBe(0);
|
||||
});
|
||||
|
||||
it("calculates relative strength index", () => {
|
||||
const prices = [100, 102, 101, 105, 107, 110, 108, 115, 120, 119, 121];
|
||||
const result = calculateRSI(prices, 5);
|
||||
expect(result).toBeGreaterThan(50);
|
||||
expect(result).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateMACD", () => {
|
||||
it("returns 0 when prices length < slowPeriod", () => {
|
||||
expect(calculateMACD([1, 2, 3], 12, 26, 9)).toBe(0);
|
||||
});
|
||||
|
||||
it("calculates MACD line", () => {
|
||||
const prices = Array.from({ length: 30 }, (_, i) => 100 + i * 0.5);
|
||||
const result = calculateMACD(prices);
|
||||
expect(result).toBeCloseTo(-0.96, 2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
export function calculateSMA(prices: number[], period: number = 20): number {
|
||||
if (prices.length < period) return 0;
|
||||
const sum = prices.slice(0, period).reduce((a, b) => a + b, 0);
|
||||
return sum / period;
|
||||
}
|
||||
|
||||
export function calculateEMA(prices: number[], period: number = 20): number {
|
||||
if (prices.length < period) return 0;
|
||||
const multiplier = 2 / (period + 1);
|
||||
let ema = prices[period - 1];
|
||||
for (let i = period; i < prices.length; i++) {
|
||||
ema = prices[i] * multiplier + ema * (1 - multiplier);
|
||||
}
|
||||
return ema;
|
||||
}
|
||||
|
||||
export function calculateRSI(prices: number[], period: number = 14): number {
|
||||
if (prices.length < period + 1) return 0;
|
||||
let gains = 0;
|
||||
let losses = 0;
|
||||
for (let i = 1; i <= period; i++) {
|
||||
const diff = prices[i] - prices[i - 1];
|
||||
if (diff > 0) gains += diff;
|
||||
else losses -= diff;
|
||||
}
|
||||
const avgGain = gains / period;
|
||||
const avgLoss = losses / period;
|
||||
if (avgLoss === 0) return 100;
|
||||
const rs = avgGain / avgLoss;
|
||||
return 100 - (100 / (1 + rs));
|
||||
}
|
||||
|
||||
export function calculateMACD(
|
||||
prices: number[],
|
||||
fastPeriod: number = 12,
|
||||
slowPeriod: number = 26,
|
||||
signalPeriod: number = 9
|
||||
): number {
|
||||
if (prices.length < slowPeriod) return 0;
|
||||
const emaFast = calculateEMA(prices, fastPeriod);
|
||||
const emaSlow = calculateEMA(prices, slowPeriod);
|
||||
const macdLine = emaFast - emaSlow;
|
||||
const signal = calculateEMA([macdLine], signalPeriod);
|
||||
return macdLine - signal;
|
||||
}
|
||||
Reference in New Issue
Block a user