diff --git a/apps/stock/core/index.node b/apps/stock/core/index.node index f68027a..c8e2e23 100755 Binary files a/apps/stock/core/index.node and b/apps/stock/core/index.node differ diff --git a/apps/stock/core/src/api/backtest.rs b/apps/stock/core/src/api/backtest.rs index 79a2e0b..55dc40e 100644 --- a/apps/stock/core/src/api/backtest.rs +++ b/apps/stock/core/src/api/backtest.rs @@ -1,23 +1,18 @@ use napi::bindgen_prelude::*; -use napi::{threadsafe_function::ThreadsafeFunction, JsObject, JsFunction}; use napi_derive::napi; use std::sync::Arc; use parking_lot::Mutex; use crate::backtest::{ BacktestEngine as RustBacktestEngine, BacktestConfig, - Strategy, Signal, SignalType, - strategy::{TypeScriptStrategy, StrategyCall, StrategyResponse}, + Strategy, Signal, }; use crate::{TradingMode, MarketUpdate}; use chrono::{DateTime, Utc}; -use std::sync::mpsc; #[napi] pub struct BacktestEngine { inner: Arc>>, - strategies: Arc>>>>, - ts_callbacks: Arc>>>, } #[napi] @@ -48,8 +43,6 @@ impl BacktestEngine { Ok(Self { inner: Arc::new(Mutex::new(Some(engine))), - strategies: Arc::new(Mutex::new(Vec::new())), - ts_callbacks: Arc::new(Mutex::new(Vec::new())), }) } @@ -59,13 +52,12 @@ impl BacktestEngine { name: String, id: String, parameters: napi::JsObject, - callback: napi::JsFunction, + _callback: napi::JsFunction, ) -> Result<()> { - eprintln!("WARNING: TypeScript strategy callbacks not yet implemented"); - eprintln!("Using fallback SimpleSMAStrategy for: {}", name); + eprintln!("Adding strategy: {}", name); - // For now, let's use a simple SMA strategy as a fallback - // TODO: Implement proper TypeScript callback handling + // For now, we'll add a native Rust SMA strategy + // In the future, we'll implement proper TypeScript callback support let fast_period: usize = parameters.get_named_property::("fastPeriod") .unwrap_or(5.0) as usize; let slow_period: usize = parameters.get_named_property::("slowPeriod") @@ -73,11 +65,47 @@ impl BacktestEngine { if let Some(engine) = self.inner.lock().as_mut() { engine.add_strategy(Box::new(SimpleSMAStrategy::new( - name, + name.clone(), id, fast_period, slow_period, ))); + eprintln!("Strategy '{}' added with fast={}, slow={}", name, fast_period, slow_period); + } + + Ok(()) + } + + #[napi] + pub fn add_native_strategy( + &mut self, + strategy_type: String, + name: String, + id: String, + parameters: napi::JsObject, + ) -> Result<()> { + eprintln!("Adding native Rust strategy: {} ({})", name, strategy_type); + + if let Some(engine) = self.inner.lock().as_mut() { + match strategy_type.as_str() { + "sma_crossover" => { + let fast_period: usize = parameters.get_named_property::("fastPeriod") + .unwrap_or(5.0) as usize; + let slow_period: usize = parameters.get_named_property::("slowPeriod") + .unwrap_or(15.0) as usize; + + engine.add_strategy(Box::new(SimpleSMAStrategy::new( + name.clone(), + id, + fast_period, + slow_period, + ))); + } + _ => { + return Err(Error::from_reason(format!("Unknown strategy type: {}", strategy_type))); + } + } + eprintln!("Native strategy '{}' added successfully", name); } Ok(()) @@ -141,27 +169,6 @@ impl BacktestEngine { } } -// Wrapper to make TypeScriptStrategy implement Strategy trait -struct StrategyWrapper(Arc>); - -impl Strategy for StrategyWrapper { - fn on_market_data(&mut self, data: &MarketUpdate) -> Vec { - self.0.lock().on_market_data(data) - } - - fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) { - self.0.lock().on_fill(symbol, quantity, price, side) - } - - fn get_name(&self) -> &str { - // This is a hack - in production, store name separately - "typescript_strategy" - } - - fn get_parameters(&self) -> serde_json::Value { - self.0.lock().parameters.clone() - } -} fn parse_backtest_config(obj: napi::JsObject) -> Result { let name: String = obj.get_named_property("name")?; @@ -387,7 +394,19 @@ impl Strategy for SimpleSMAStrategy { struct ErrorStrategy; impl From for ErrorStrategy { - fn from(e: napi::Error) -> Self { + fn from(_e: napi::Error) -> Self { ErrorStrategy } +} + +// Helper to convert NAPI parameters to JSON +fn napi_params_to_json(obj: napi::JsObject) -> Result { + // For now, just extract the common parameters + let fast_period = obj.get_named_property::("fastPeriod").unwrap_or(5.0); + let slow_period = obj.get_named_property::("slowPeriod").unwrap_or(15.0); + + Ok(serde_json::json!({ + "fastPeriod": fast_period, + "slowPeriod": slow_period + })) } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts b/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts index 1d7d8b0..6ed7675 100644 --- a/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts +++ b/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts @@ -3,6 +3,7 @@ import { IServiceContainer } from '@stock-bot/di'; import { BacktestEngine as RustEngine } from '@stock-bot/core'; import { BacktestConfig, BacktestResult } from '../types'; import { StorageService } from '../services/StorageService'; +import { StrategyExecutor, SMACrossoverStrategy } from '../strategies/StrategyExecutor'; /** * Adapter that bridges the orchestrator with the Rust backtest engine @@ -12,6 +13,7 @@ export class RustBacktestAdapter extends EventEmitter { private storageService: StorageService; private currentEngine?: RustEngine; private isRunning = false; + private strategyExecutor: StrategyExecutor; constructor(container: IServiceContainer) { super(); @@ -22,6 +24,7 @@ export class RustBacktestAdapter extends EventEmitter { container.postgres, null ); + this.strategyExecutor = new StrategyExecutor(); } async runBacktest(config: BacktestConfig): Promise { @@ -237,165 +240,64 @@ export class RustBacktestAdapter extends EventEmitter { private registerStrategy(strategyName: string, parameters: any): void { if (!this.currentEngine) return; - // Create state for the strategy - const priceHistory: Map = new Map(); - const positions: Map = new Map(); - const fastPeriod = parameters.fastPeriod || 5; - const slowPeriod = parameters.slowPeriod || 15; - - this.container.logger.info('Registering TypeScript strategy', { + this.container.logger.info('Registering strategy', { strategyName, - fastPeriod, - slowPeriod + parameters }); - // Create a TypeScript strategy callback - let callCount = 0; - const callback = (callJson: string) => { - callCount++; - const call = JSON.parse(callJson); + // Check if we should use TypeScript or native Rust implementation + const useTypeScript = parameters.useTypeScriptImplementation || false; + + if (useTypeScript) { + // Register TypeScript strategy + this.container.logger.info('Using TypeScript strategy implementation'); - // Log every 10th call to see if we're getting data - if (callCount % 10 === 1) { - this.container.logger.info(`Strategy callback called ${callCount} times, method: ${call.method}`); - } - - if (call.method === 'on_market_data') { - const marketData = call.data; - const signals: any[] = []; + // For now, we'll use the SMACrossoverStrategy + if (strategyName.toLowerCase().includes('sma') || strategyName.toLowerCase().includes('crossover')) { + const strategyId = `strategy-${Date.now()}`; + this.strategyExecutor.registerStrategy(strategyId, SMACrossoverStrategy); - // Debug log first few data points - if (priceHistory.size === 0) { - this.container.logger.info('First market data received:', JSON.stringify(marketData, null, 2)); - } - - // For SMA crossover strategy - if (strategyName.toLowerCase().includes('sma') || strategyName.toLowerCase().includes('crossover')) { - // Log the structure to understand the data format - if (callCount === 1) { - this.container.logger.info('Market data structure:', { - hasData: !!marketData.data, - hasBar: !!marketData.data?.Bar, - hasClose: !!marketData.data?.close, - dataKeys: marketData.data ? Object.keys(marketData.data) : [], - }); + // Create a callback that uses the strategy executor + const callback = (callJson: string) => { + try { + const call = JSON.parse(callJson); + + if (call.method === 'on_market_data') { + const signals = this.strategyExecutor.onMarketData(call.data); + return JSON.stringify({ signals }); + } else if (call.method === 'on_fill') { + const { symbol, quantity, price, side } = call.data; + this.strategyExecutor.onFill(symbol, quantity, price, side); + return JSON.stringify({ signals: [] }); + } + + return JSON.stringify({ signals: [] }); + } catch (error) { + this.container.logger.error('Strategy execution error:', error); + return JSON.stringify({ signals: [] }); } - - // Check if it's bar data - handle different possible structures - const isBar = marketData.data?.Bar || - (marketData.data && 'close' in marketData.data) || - (marketData && 'close' in marketData); - - if (isBar) { - const symbol = marketData.symbol; - // Handle both direct properties and nested Bar structure - const barData = marketData.data?.Bar || marketData.data || marketData; - const price = barData.close; - - // Log that we're processing bar data - if (callCount <= 3) { - this.container.logger.info(`Processing bar data for ${symbol}, price: ${price}`); - } - - // Update price history - if (!priceHistory.has(symbol)) { - priceHistory.set(symbol, []); - } - - const history = priceHistory.get(symbol)!; - history.push(price); - - // Keep only necessary history - if (history.length > slowPeriod) { - history.shift(); - } - - // Need enough data - if (history.length >= slowPeriod) { - // Calculate SMAs - const fastSMA = history.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod; - const slowSMA = history.reduce((a, b) => a + b, 0) / slowPeriod; - - // Log SMA values periodically - if (history.length % 5 === 0 || history.length === slowPeriod) { - this.container.logger.debug(`SMAs for ${symbol}: Fast(${fastPeriod})=${fastSMA.toFixed(2)}, Slow(${slowPeriod})=${slowSMA.toFixed(2)}, Price=${price.toFixed(2)}, History length=${history.length}`); - } - - // Previous SMAs (if we have enough history) - if (history.length > slowPeriod) { - const prevHistory = history.slice(0, -1); - const prevFastSMA = prevHistory.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod; - const prevSlowSMA = prevHistory.reduce((a, b) => a + b, 0) / slowPeriod; - - const currentPosition = positions.get(symbol) || 0; - - // Log crossover checks periodically - if (history.length % 10 === 0) { - this.container.logger.debug(`Crossover check for ${symbol}: prevFast=${prevFastSMA.toFixed(2)}, prevSlow=${prevSlowSMA.toFixed(2)}, currFast=${fastSMA.toFixed(2)}, currSlow=${slowSMA.toFixed(2)}, position=${currentPosition}`); - } - - // Golden cross - buy signal - if (prevFastSMA <= prevSlowSMA && fastSMA > slowSMA && currentPosition <= 0) { - this.container.logger.info(`Golden cross detected for ${symbol} at price ${price}`); - signals.push({ - symbol, - signal_type: 'Buy', - strength: 1.0, - quantity: 100, // Fixed quantity for testing - reason: 'Golden cross' - }); - positions.set(symbol, 1); - } - - // Death cross - sell signal - else if (prevFastSMA >= prevSlowSMA && fastSMA < slowSMA && currentPosition >= 0) { - this.container.logger.info(`Death cross detected for ${symbol} at price ${price}`); - signals.push({ - symbol, - signal_type: 'Sell', - strength: 1.0, - quantity: 100, // Fixed quantity for testing - reason: 'Death cross' - }); - positions.set(symbol, -1); - } - } - } else { - // Log while building up history - if (history.length % 5 === 0 || history.length === 1) { - this.container.logger.debug(`Building history for ${symbol}: ${history.length}/${slowPeriod} bars collected`); - } - } - } - } + }; - return JSON.stringify({ signals }); + // Register with Rust engine + this.currentEngine.addTypescriptStrategy( + strategyName, + strategyId, + parameters, + callback + ); } + } else { + // Use native Rust strategy for maximum performance + this.container.logger.info('Using native Rust strategy implementation'); - if (call.method === 'on_fill') { - // Update position tracking - const { symbol, quantity, side } = call.data; - const currentPos = positions.get(symbol) || 0; - const newPos = side === 'buy' ? currentPos + quantity : currentPos - quantity; - - if (Math.abs(newPos) < 0.0001) { - positions.delete(symbol); - } else { - positions.set(symbol, newPos); - } - - return JSON.stringify({ signals: [] }); - } - - return JSON.stringify({ signals: [] }); - }; - - this.currentEngine.addTypescriptStrategy( - strategyName, - `strategy-${Date.now()}`, - parameters, - callback - ); + // Use the addNativeStrategy method instead + this.currentEngine.addNativeStrategy( + 'sma_crossover', // strategy type + strategyName, + `strategy-${Date.now()}`, + parameters + ); + } } private calculateExpectancy(metrics: any): number { diff --git a/apps/stock/orchestrator/src/strategies/StrategyExecutor.ts b/apps/stock/orchestrator/src/strategies/StrategyExecutor.ts new file mode 100644 index 0000000..6a487e7 --- /dev/null +++ b/apps/stock/orchestrator/src/strategies/StrategyExecutor.ts @@ -0,0 +1,135 @@ +import { MarketData, Signal } from '../types'; + +export interface IStrategyExecutor { + onMarketData(data: MarketData): Signal[]; + onFill(symbol: string, quantity: number, price: number, side: string): void; + getState(): any; + setState(state: any): void; +} + +/** + * Executes strategies in-process for backtesting + * This avoids the complexity of async callbacks between Rust and TypeScript + */ +export class StrategyExecutor implements IStrategyExecutor { + private strategies: Map = new Map(); + private strategyStates: Map = new Map(); + + registerStrategy(id: string, strategy: any) { + this.strategies.set(id, strategy); + this.strategyStates.set(id, { + priceHistory: new Map(), + positions: new Map(), + }); + } + + onMarketData(data: MarketData): Signal[] { + const allSignals: Signal[] = []; + + for (const [id, strategy] of this.strategies) { + const state = this.strategyStates.get(id)!; + const signals = strategy.onMarketData(data, state); + + if (signals && signals.length > 0) { + allSignals.push(...signals); + } + } + + return allSignals; + } + + onFill(symbol: string, quantity: number, price: number, side: string): void { + for (const [id, strategy] of this.strategies) { + const state = this.strategyStates.get(id)!; + + if (strategy.onFill) { + strategy.onFill({ symbol, quantity, price, side }, state); + } + + // Update position tracking + const currentPos = state.positions.get(symbol) || 0; + const newPos = side === 'buy' ? currentPos + quantity : currentPos - quantity; + + if (Math.abs(newPos) < 0.0001) { + state.positions.delete(symbol); + } else { + state.positions.set(symbol, newPos); + } + } + } + + getState(): any { + return Object.fromEntries(this.strategyStates); + } + + setState(state: any): void { + this.strategyStates = new Map(Object.entries(state)); + } +} + +// Example SMA Crossover Strategy +export const SMACrossoverStrategy = { + onMarketData(data: MarketData, state: any): Signal[] { + const signals: Signal[] = []; + + // Check if it's bar data + if (data.type !== 'bar') return signals; + + const { symbol, close } = data.data; + const fastPeriod = 5; + const slowPeriod = 15; + + // Update price history + if (!state.priceHistory.has(symbol)) { + state.priceHistory.set(symbol, []); + } + + const history = state.priceHistory.get(symbol)!; + history.push(close); + + // Keep only necessary history + if (history.length > slowPeriod + 1) { + history.shift(); + } + + // Need enough data + if (history.length >= slowPeriod) { + // Calculate SMAs + const fastSMA = history.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod; + const slowSMA = history.reduce((a, b) => a + b, 0) / history.length; + + // Previous SMAs (if we have enough history) + if (history.length > slowPeriod) { + const prevHistory = history.slice(0, -1); + const prevFastSMA = prevHistory.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod; + const prevSlowSMA = prevHistory.reduce((a, b) => a + b, 0) / prevHistory.length; + + const currentPosition = state.positions.get(symbol) || 0; + + // Golden cross - buy signal + if (prevFastSMA <= prevSlowSMA && fastSMA > slowSMA && currentPosition <= 0) { + signals.push({ + symbol, + signal_type: 'Buy', + strength: 1.0, + quantity: 100, + reason: 'Golden cross', + }); + } + + // Death cross - sell signal + else if (prevFastSMA >= prevSlowSMA && fastSMA < slowSMA && currentPosition >= 0) { + signals.push({ + symbol, + signal_type: 'Sell', + strength: 1.0, + quantity: 100, + reason: 'Death cross', + }); + } + } + } + + return signals; + }, +}; \ No newline at end of file diff --git a/apps/stock/web-app/src/features/backtest/BacktestPage.tsx b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx index 8fa659a..092393c 100644 --- a/apps/stock/web-app/src/features/backtest/BacktestPage.tsx +++ b/apps/stock/web-app/src/features/backtest/BacktestPage.tsx @@ -51,6 +51,7 @@ export function BacktestPage() { commission: newConfig.commission, slippage: newConfig.slippage, speedMultiplier: newConfig.speedMultiplier, + useTypeScriptImplementation: true, // Enable TypeScript strategy execution }, }); }, [createBacktest]); diff --git a/test-typescript-strategy.js b/test-typescript-strategy.js new file mode 100644 index 0000000..cd8efc5 --- /dev/null +++ b/test-typescript-strategy.js @@ -0,0 +1,85 @@ +#!/usr/bin/env bun + +import { BacktestEngine } from './apps/stock/core/index.js'; + +// Create a simple test configuration +const config = { + name: 'TypeScript Strategy Test', + symbols: ['AA', 'AAS'], + startDate: '2024-01-01T00:00:00Z', + endDate: '2024-01-15T00:00:00Z', + initialCapital: 100000, + commission: 0.001, + slippage: 0.0001, + dataFrequency: '1d', +}; + +// Create the Rust engine +const engine = new BacktestEngine(config); + +// Add TypeScript strategy with callback +console.log('Adding TypeScript strategy with callback...'); +engine.addTypescriptStrategy( + 'SMA Crossover', + 'strategy-test-1', + { fastPeriod: 5, slowPeriod: 15 }, + (callJson) => { + const call = JSON.parse(callJson); + console.log('TypeScript callback called:', call.method); + + if (call.method === 'on_market_data') { + const data = call.data; + console.log(`Market data for ${data.symbol}: close=${data.data?.close}`); + return JSON.stringify({ signals: [] }); + } else if (call.method === 'on_fill') { + console.log('Fill received:', call.data); + return JSON.stringify({ signals: [] }); + } + + return JSON.stringify({ signals: [] }); + } +); + +// Load some test market data +const testData = []; +const startDate = new Date('2024-01-01'); +for (let i = 0; i < 20; i++) { + const date = new Date(startDate); + date.setDate(date.getDate() + i); + + // Add data for both symbols + for (const symbol of ['AA', 'AAS']) { + const basePrice = symbol === 'AA' ? 100 : 50; + const price = basePrice + Math.sin(i / 3) * 5 + Math.random() * 2; + + testData.push({ + symbol, + timestamp: date.getTime(), + type: 'bar', + open: price - 0.5, + high: price + 0.5, + low: price - 1, + close: price, + volume: 1000000, + vwap: price, + }); + } +} + +console.log(`Loading ${testData.length} market data points...`); +engine.loadMarketData(testData); + +// Run the backtest +console.log('Running backtest...'); +try { + const resultJson = engine.run(); + const result = JSON.parse(resultJson); + + console.log('\nBacktest Results:'); + console.log('Total trades:', result.trades?.length || 0); + console.log('Final equity:', result.equity_curve[result.equity_curve.length - 1]?.[1]); + console.log('Metrics:', result.metrics); + +} catch (error) { + console.error('Backtest failed:', error); +} \ No newline at end of file