A modern, web-based backtesting platform for stock and crypto trading strategies. Build, test, and analyze your trading algorithms with real market data from Alpaca Markets.
- Full-featured TypeScript/JavaScript editor with syntax highlighting
- IntelliSense and auto-completion
- Built-in strategy templates and examples
- Real-time code validation
- Enhanced error highlighting with line-specific visual indicators
- Precise error reporting with line numbers and code context
- Auto-clearing errors when code changes or new backtests run
- Connect to Alpaca Markets API for live stock and crypto data
- Support for US equities and cryptocurrency pairs (BTC/USD, ETH/USD, etc.)
- Support for both daily and minute-level data
- Automatic timeframe selection based on date range
- IEX data feed for reliable market information
- Execute custom trading strategies on historical data
- Portfolio tracking with real-time value calculations
- Buy & Hold comparison benchmarking
- Detailed transaction history and metadata
- Performance metrics and summary cards
- Color-coded portfolio value changes (green
↗️ / red↘️ ) - Detailed transaction table with expandable metadata
- Strategy vs Buy & Hold performance comparison
- CSV export functionality
- Auto-save API credentials, parameters, and strategy code
- Version history for strategy iterations
- Local storage with timestamp tracking
- Resume work seamlessly across sessions
- Node.js 20.19.0+ or 22+ (required by Vite React plugin)
- Yarn package manager
- Alpaca Markets account (free tier available)
-
Clone the repository
git clone <repository-url> cd stonks.js
-
Install dependencies
yarn install
-
Start development server
yarn dev
-
Get Alpaca API credentials
- Sign up at Alpaca Markets
- Generate paper trading API keys
- Enter credentials in the app's API Configuration section
- Open the API Configuration panel
- Enter your Alpaca API Key and Secret
- Credentials are automatically saved locally
- Symbol: Enter any valid ticker (e.g., AAPL, MSFT, TSLA) or crypto pair (e.g., ETH/USD, BTC/USDT)
- Starting Amount: Initial portfolio value (e.g., $10,000)
- Date Range: Select start and end dates for backtesting
- Time Frame: Automatically selected (1-minute for same-day, daily for longer periods)
// Example: Simple buy and hold strategy
if (data.dayNumber === 0) {
const sharesToBuy = Math.floor(1000 / data.nextBar.open);
result.changeInShares = sharesToBuy;
result.price = data.nextBar.open; // Optional - defaults to nextBar.open if not specified
// Store purchase info in scratchpad for later reference
data.scratchpad.purchasePrice = data.nextBar.open;
data.scratchpad.purchaseDate = data.currentBar.timestamp;
} else {
// Hold for the rest of the period
result.changeInShares = 0;
// Track performance using scratchpad data
if (data.scratchpad.purchasePrice) {
const currentReturn = ((data.currentBar.close - data.scratchpad.purchasePrice) / data.scratchpad.purchasePrice) * 100;
result.meta = { returnPercent: currentReturn.toFixed(2) };
}
}- Click "Run Backtest" to execute your strategy
- View results in the interactive dashboard
- Export data to CSV for further analysis
data.dayNumber; // Current day (0-based)
data.currentBar; // Current price bar
data.previousBar; // Previous price bar
data.nextBar; // Next price bar (for reference)
data.currentPortfolio; // Portfolio state (shares, cash, value)
data.history; // Array of previous strategy results and portfolio snapshots
data.scratchpad; // Persistent object for storing custom data across trading days{
timestamp: "2024-01-15T09:30:00Z",
open: 150.25,
high: 152.30,
low: 149.80,
close: 151.75,
volume: 1234567
}// Modify the result object (don't return values)
result.changeInShares = 10; // Buy 10 shares (negative to sell)
result.price = data.nextBar.open; // Execution price (optional, defaults to nextBar.open)
result.meta = { reason: "RSI signal" }; // Custom metadata (optional)The data.scratchpad object provides persistent storage for custom data across trading days. It's perfect for maintaining state, calculating indicators, or storing historical values that your strategy needs to reference.
// Store any type of data
data.scratchpad.myValue = 42; // Numbers
data.scratchpad.myArray = []; // Arrays
data.scratchpad.myObject = { key: "value" }; // Objects
// Retrieve with fallback values
const stored = data.scratchpad.myValue || 0;
// Initialize arrays and track price history
data.scratchpad.prices = data.scratchpad.prices || [];
data.scratchpad.prices.push(data.currentBar.close);
// Maintain rolling windows
if (data.scratchpad.prices.length > 20) {
data.scratchpad.prices.shift(); // Keep only last 20 prices
}
// Calculate indicators using stored data
const avg = data.scratchpad.prices.reduce((a, b) => a + b) / data.scratchpad.prices.length;Key Features:
- Persistent: Data survives across all trading days in a backtest
- Flexible: Store numbers, arrays, objects, or any JavaScript data type
- Automatic: No need to initialize - the object is always available
- Isolated: Each backtest gets its own fresh scratchpad instance
// Initialize price arrays in scratchpad
data.scratchpad.prices = data.scratchpad.prices || [];
data.scratchpad.prices.push(data.currentBar.close);
// Maintain rolling windows efficiently
if (data.scratchpad.prices.length > 50) {
data.scratchpad.prices.shift(); // Keep only last 50 prices
}
// Calculate moving averages once we have enough data
if (data.scratchpad.prices.length >= 50) {
const prices = data.scratchpad.prices;
const sma20 = prices.slice(-20).reduce((sum, price) => sum + price, 0) / 20;
const sma50 = prices.reduce((sum, price) => sum + price, 0) / prices.length;
if (sma20 > sma50 && data.currentPortfolio.sharesOwned === 0) {
// Buy signal - golden cross
const sharesToBuy = Math.floor(data.currentPortfolio.availableCash / data.nextBar.open);
result.changeInShares = sharesToBuy;
result.price = data.nextBar.open;
result.meta = { signal: "buy", sma20, sma50, crossover: "golden" };
} else if (sma20 < sma50 && data.currentPortfolio.sharesOwned > 0) {
// Sell signal - death cross
result.changeInShares = -data.currentPortfolio.sharesOwned;
result.price = data.nextBar.open;
result.meta = { signal: "sell", sma20, sma50, crossover: "death" };
}
}// Initialize scratchpad arrays for RSI calculation
data.scratchpad.prices = data.scratchpad.prices || [];
data.scratchpad.gains = data.scratchpad.gains || [];
data.scratchpad.losses = data.scratchpad.losses || [];
// Store current price and calculate price change
data.scratchpad.prices.push(data.currentBar.close);
if (data.scratchpad.prices.length > 1) {
const change = data.currentBar.close - data.scratchpad.prices[data.scratchpad.prices.length - 2];
data.scratchpad.gains.push(change > 0 ? change : 0);
data.scratchpad.losses.push(change < 0 ? Math.abs(change) : 0);
}
// Maintain 14-period rolling windows
if (data.scratchpad.gains.length > 14) {
data.scratchpad.gains.shift();
data.scratchpad.losses.shift();
}
if (data.scratchpad.prices.length > 15) {
data.scratchpad.prices.shift();
}
// Calculate RSI once we have enough data
if (data.scratchpad.gains.length >= 14) {
const avgGain = data.scratchpad.gains.reduce((sum, gain) => sum + gain, 0) / 14;
const avgLoss = data.scratchpad.losses.reduce((sum, loss) => sum + loss, 0) / 14;
const rsi = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
if (rsi < 30 && data.currentPortfolio.sharesOwned === 0) {
// Oversold - buy signal
const sharesToBuy = Math.floor(data.currentPortfolio.availableCash / data.nextBar.open);
result.changeInShares = sharesToBuy;
result.price = data.nextBar.open;
result.meta = { rsi: rsi.toFixed(2), signal: "oversold", avgGain, avgLoss };
} else if (rsi > 70 && data.currentPortfolio.sharesOwned > 0) {
// Overbought - sell signal
result.changeInShares = -data.currentPortfolio.sharesOwned;
result.price = data.nextBar.open;
result.meta = { rsi: rsi.toFixed(2), signal: "overbought", avgGain, avgLoss };
}
}The platform includes sophisticated error handling to help debug strategy code:
- Syntax errors: Caught during code compilation with exact line numbers
- Runtime errors: Captured during strategy execution with stack trace analysis
- Type validation: Monaco editor provides real-time TypeScript validation
- Line highlighting: Red wavy underlines at error locations in Monaco editor
- Margin indicators: Red dots in editor gutter for quick error spotting
- Hover tooltips: Detailed error messages on hover
- Auto-scroll: Automatic navigation to error line when errors occur
- Precise line numbers: Accurate mapping from compiled code to source code
- Code context: 5-line context window showing surrounding code
- Error categorization: Syntax, runtime, or unknown error types
- Column positions: Exact character position where errors occur
- Auto-clear on edit: Error highlights disappear when you start typing
- Backtest reset: Errors clear when running new backtests
- Parameter changes: Previous errors clear when modifying backtest settings
- React 19 with TypeScript for type safety
- Vite for fast development and building
- TailwindCSS for modern, responsive styling
- Monaco Editor for advanced code editing experience
- Luxon for date/time handling
CodeEditor- Monaco-based strategy editor with TypeScript support and error highlightingBacktestParameters- Form for configuring backtest settingsResultsDisplay- Interactive results dashboard with enhanced error displayApiConfiguration- Secure API credential managementTypeExtractor- Dynamic type definition system that syncs TypeScript types with Monaco editor
- User writes strategy in Monaco editor with real-time TypeScript validation
- Code is validated, enhanced with error tracking, and transpiled to executable JavaScript
- Selected data provider fetches historical market data
- Backtesting engine executes strategy with enhanced error capture
- Results are displayed with interactive charts and detailed error reporting if needed
The platform uses a modular data provider system that allows easy integration of different market data sources. This architecture separates data fetching logic from the core backtesting engine, making it simple to add support for new APIs.
All data providers must extend the StockDataProviderBase abstract class:
export abstract class StockDataProviderBase {
static readonly name: string = "ProviderName";
// Fetch historical market data
abstract getBars(props: BacktestMarketDataProps, abortSignal?: AbortSignal): Promise<Bar[]>;
// Render provider-specific configuration UI
abstract renderSettings(initialSettings: Record<string, any>, onSettingsChange: (settings: Record<string, any>) => void): React.ReactNode;
// Validate provider configuration
abstract get isConfigured(): { isValid: boolean; error?: string };
}- Input:
BacktestMarketDataPropscontaining symbol, date range, and bar resolution - Output: Array of
Barobjects with OHLCV data - Features:
- Support for abort signals (cancellation)
- Handle pagination for large datasets
- Convert data to standardized
Barformat - Proper error handling and user-friendly messages
- Purpose: Render provider-specific configuration (API keys, endpoints, etc.)
- Input: Initial settings and change callback
- Output: React component for configuration
- Features:
- Real-time validation feedback
- Secure credential handling
- Auto-save functionality via callback
- Purpose: Check if provider is ready to fetch data
- Output: Validation result with optional error message
- Usage: Enables/disables backtest functionality
interface Bar {
timestamp: string; // ISO 8601 format
open: number; // Opening price
high: number; // Highest price
low: number; // Lowest price
close: number; // Closing price
volume: number; // Trading volume
}interface BacktestMarketDataProps {
symbol: string; // Stock ticker (e.g., "AAPL")
startDate: string; // ISO date string
endDate?: string; // Optional end date
barResolutionValue: string; // Resolution value (e.g., "1", "5")
barResolutionPeriod: string; // Period type ("minute", "hour", "day", "week", "month")
}// src/providers/YourDataProvider.tsx
import React, { useState, useEffect } from "react";
import { StockDataProviderBase } from "./StockDataProviderBase";
import type { Bar, BacktestMarketDataProps } from "../types/backtesting";
export class YourDataProvider extends StockDataProviderBase {
static readonly name: string = "Your Provider";
private settings?: { apiKey: string };
async getBars(props: BacktestMarketDataProps, abortSignal?: AbortSignal): Promise<Bar[]> {
const { symbol, startDate, endDate, barResolutionValue, barResolutionPeriod } = props;
// Convert resolution to your API's format
const timeframe = this.convertTimeframe(barResolutionValue, barResolutionPeriod);
// Build API request
const url = `https://your-api.com/v1/bars/${symbol}?timeframe=${timeframe}&start=${startDate}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${this.settings?.apiKey}`,
'Accept': 'application/json'
},
signal: abortSignal
});
if (!response.ok) {
throw new Error(`API Error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
// Convert to standard Bar format
return data.bars.map((bar: any) => ({
timestamp: bar.timestamp,
open: Number(bar.open),
high: Number(bar.high),
low: Number(bar.low),
close: Number(bar.close),
volume: Number(bar.volume)
}));
}
get isConfigured(): { isValid: boolean, error?: string } {
if (!this.settings?.apiKey) {
return { isValid: false, error: "API key required" };
}
return { isValid: true };
}
renderSettings(initialSettings: Record<string, any>, onSettingsChange: (settings: Record<string, any>) => void): React.ReactNode {
const [apiKey, setApiKey] = useState(initialSettings.apiKey ?? '');
useEffect(() => {
onSettingsChange({ apiKey });
this.settings = { apiKey };
}, [apiKey]);
return (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="w-full px-3 py-2 border rounded-md"
placeholder="Enter your API key"
/>
</div>
</div>
);
}
private convertTimeframe(value: string, period: string): string {
// Convert standard resolution to your API's format
const numValue = parseInt(value);
switch (period) {
case 'minute': return `${numValue}m`;
case 'hour': return `${numValue}h`;
case 'day': return `${numValue}d`;
default: return '1d';
}
}
}// src/providers/AvailableProviders.ts
import { AlpacaDataProvider } from "./AlpacaDataProvider";
import { YourDataProvider } from "./YourDataProvider";
export const AvailableProviders = [
AlpacaDataProvider,
YourDataProvider // Add your provider here
] as const;- Start the development server:
yarn dev - Select your provider from the dropdown in the UI
- Configure credentials in the settings panel
- Run a backtest to verify data fetching works correctly
- Features: Real-time and historical stock and crypto data
- Asset Classes:
- US equities via IEX feed
- Cryptocurrency pairs (BTC/USD, ETH/USD, DOGE/USD, and more)
- Requirements: Free Alpaca account with paper trading API keys
- Supported Resolutions: 1min to 1month bars
- Rate Limits: 200 requests/minute for free accounts
- Data Coverage: Extensive historical data for both stocks and crypto
// Provide user-friendly error messages
if (response.status === 401) {
throw new Error("Invalid API credentials. Please check your API key.");
}
if (response.status === 429) {
throw new Error("Rate limit exceeded. Please wait before trying again.");
}
if (response.status === 404) {
throw new Error(`Symbol '${symbol}' not found. Please verify the ticker symbol.`);
}// Handle large datasets with pagination
async getBars(props: BacktestMarketDataProps, abortSignal?: AbortSignal): Promise<Bar[]> {
let allBars: Bar[] = [];
let nextPageToken: string | undefined;
do {
const response = await this.fetchPage(props, nextPageToken, abortSignal);
allBars.push(...response.bars);
nextPageToken = response.nextPageToken;
} while (nextPageToken);
return allBars;
}// Ensure consistent timezone handling
import { DateTime } from "luxon";
// Convert to market timezone (e.g., Eastern for US markets)
const marketTime = DateTime.fromISO(bar.timestamp).setZone("America/New_York").toISO();// Consider implementing caching for frequently requested data
private cache = new Map<string, { data: Bar[], timestamp: number }>();
private getCacheKey(props: BacktestMarketDataProps): string {
return `${props.symbol}-${props.startDate}-${props.endDate}-${props.barResolutionValue}${props.barResolutionPeriod}`;
}Some providers may support custom timeframes not available in the standard resolution options. You can extend the UI by modifying the BacktestParameters component to include provider-specific options.
While the current architecture focuses on historical backtesting, providers can be extended to support real-time data feeds for live strategy monitoring.
The Bar interface can be extended to support additional asset classes (forex, crypto, commodities) by adding provider-specific metadata fields.
- Local Storage: All data stored locally in browser
- No Server: Pure client-side application
- API Keys: Stored securely in localStorage with auto-complete prevention
- HTTPS Required: Alpaca API requires secure connections
- Browser Security: API credential fields prevent password manager save prompts
Build the project for production deployment:
# Standard production build
yarn build
# Build for GitHub Pages (if deploying to GitHub Pages)
yarn build:github
# Build for root path deployment (custom domains)
yarn build:github-root
# Preview the production build locally
yarn deploy:previewThe built files will be in the dist/ directory, ready for deployment to any static hosting service.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-strategy) - Commit your changes (
git commit -m 'Add amazing strategy feature') - Push to the branch (
git push origin feature/amazing-strategy) - Open a Pull Request
This project is licensed under the MIT License.
- Alpaca Markets for providing market data API
- Monaco Editor for the excellent code editing experience
- TailwindCSS for beautiful, responsive styling
- 🐛 Issues: Report bugs via GitHub Issues
- 💡 Feature Requests: Submit via GitHub Discussions
- 📧 Contact: Create an issue on GitHub for support
Made with ❤️ by jheising
Happy Trading! 📈💰
⚠️ Disclaimer: This tool is for educational and research purposes only. Past performance does not guarantee future results. Always do your own research before making investment decisions.