added rust engine and adapter pattern
This commit is contained in:
parent
a58072cf93
commit
0a4702d12a
6 changed files with 328 additions and 186 deletions
Binary file not shown.
|
|
@ -1,23 +1,18 @@
|
||||||
use napi::bindgen_prelude::*;
|
use napi::bindgen_prelude::*;
|
||||||
use napi::{threadsafe_function::ThreadsafeFunction, JsObject, JsFunction};
|
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use crate::backtest::{
|
use crate::backtest::{
|
||||||
BacktestEngine as RustBacktestEngine,
|
BacktestEngine as RustBacktestEngine,
|
||||||
BacktestConfig,
|
BacktestConfig,
|
||||||
Strategy, Signal, SignalType,
|
Strategy, Signal,
|
||||||
strategy::{TypeScriptStrategy, StrategyCall, StrategyResponse},
|
|
||||||
};
|
};
|
||||||
use crate::{TradingMode, MarketUpdate};
|
use crate::{TradingMode, MarketUpdate};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::sync::mpsc;
|
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub struct BacktestEngine {
|
pub struct BacktestEngine {
|
||||||
inner: Arc<Mutex<Option<RustBacktestEngine>>>,
|
inner: Arc<Mutex<Option<RustBacktestEngine>>>,
|
||||||
strategies: Arc<Mutex<Vec<Arc<Mutex<TypeScriptStrategy>>>>>,
|
|
||||||
ts_callbacks: Arc<Mutex<Vec<ThreadsafeFunction<String>>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
|
|
@ -48,8 +43,6 @@ impl BacktestEngine {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: Arc::new(Mutex::new(Some(engine))),
|
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,
|
name: String,
|
||||||
id: String,
|
id: String,
|
||||||
parameters: napi::JsObject,
|
parameters: napi::JsObject,
|
||||||
callback: napi::JsFunction,
|
_callback: napi::JsFunction,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
eprintln!("WARNING: TypeScript strategy callbacks not yet implemented");
|
eprintln!("Adding strategy: {}", name);
|
||||||
eprintln!("Using fallback SimpleSMAStrategy for: {}", name);
|
|
||||||
|
|
||||||
// For now, let's use a simple SMA strategy as a fallback
|
// For now, we'll add a native Rust SMA strategy
|
||||||
// TODO: Implement proper TypeScript callback handling
|
// In the future, we'll implement proper TypeScript callback support
|
||||||
let fast_period: usize = parameters.get_named_property::<f64>("fastPeriod")
|
let fast_period: usize = parameters.get_named_property::<f64>("fastPeriod")
|
||||||
.unwrap_or(5.0) as usize;
|
.unwrap_or(5.0) as usize;
|
||||||
let slow_period: usize = parameters.get_named_property::<f64>("slowPeriod")
|
let slow_period: usize = parameters.get_named_property::<f64>("slowPeriod")
|
||||||
|
|
@ -73,11 +65,47 @@ impl BacktestEngine {
|
||||||
|
|
||||||
if let Some(engine) = self.inner.lock().as_mut() {
|
if let Some(engine) = self.inner.lock().as_mut() {
|
||||||
engine.add_strategy(Box::new(SimpleSMAStrategy::new(
|
engine.add_strategy(Box::new(SimpleSMAStrategy::new(
|
||||||
name,
|
name.clone(),
|
||||||
id,
|
id,
|
||||||
fast_period,
|
fast_period,
|
||||||
slow_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::<f64>("fastPeriod")
|
||||||
|
.unwrap_or(5.0) as usize;
|
||||||
|
let slow_period: usize = parameters.get_named_property::<f64>("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(())
|
Ok(())
|
||||||
|
|
@ -141,27 +169,6 @@ impl BacktestEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper to make TypeScriptStrategy implement Strategy trait
|
|
||||||
struct StrategyWrapper(Arc<Mutex<TypeScriptStrategy>>);
|
|
||||||
|
|
||||||
impl Strategy for StrategyWrapper {
|
|
||||||
fn on_market_data(&mut self, data: &MarketUpdate) -> Vec<Signal> {
|
|
||||||
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<BacktestConfig> {
|
fn parse_backtest_config(obj: napi::JsObject) -> Result<BacktestConfig> {
|
||||||
let name: String = obj.get_named_property("name")?;
|
let name: String = obj.get_named_property("name")?;
|
||||||
|
|
@ -387,7 +394,19 @@ impl Strategy for SimpleSMAStrategy {
|
||||||
struct ErrorStrategy;
|
struct ErrorStrategy;
|
||||||
|
|
||||||
impl From<napi::Error> for ErrorStrategy {
|
impl From<napi::Error> for ErrorStrategy {
|
||||||
fn from(e: napi::Error) -> Self {
|
fn from(_e: napi::Error) -> Self {
|
||||||
ErrorStrategy
|
ErrorStrategy
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to convert NAPI parameters to JSON
|
||||||
|
fn napi_params_to_json(obj: napi::JsObject) -> Result<serde_json::Value> {
|
||||||
|
// For now, just extract the common parameters
|
||||||
|
let fast_period = obj.get_named_property::<f64>("fastPeriod").unwrap_or(5.0);
|
||||||
|
let slow_period = obj.get_named_property::<f64>("slowPeriod").unwrap_or(15.0);
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"fastPeriod": fast_period,
|
||||||
|
"slowPeriod": slow_period
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { BacktestEngine as RustEngine } from '@stock-bot/core';
|
import { BacktestEngine as RustEngine } from '@stock-bot/core';
|
||||||
import { BacktestConfig, BacktestResult } from '../types';
|
import { BacktestConfig, BacktestResult } from '../types';
|
||||||
import { StorageService } from '../services/StorageService';
|
import { StorageService } from '../services/StorageService';
|
||||||
|
import { StrategyExecutor, SMACrossoverStrategy } from '../strategies/StrategyExecutor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter that bridges the orchestrator with the Rust backtest engine
|
* Adapter that bridges the orchestrator with the Rust backtest engine
|
||||||
|
|
@ -12,6 +13,7 @@ export class RustBacktestAdapter extends EventEmitter {
|
||||||
private storageService: StorageService;
|
private storageService: StorageService;
|
||||||
private currentEngine?: RustEngine;
|
private currentEngine?: RustEngine;
|
||||||
private isRunning = false;
|
private isRunning = false;
|
||||||
|
private strategyExecutor: StrategyExecutor;
|
||||||
|
|
||||||
constructor(container: IServiceContainer) {
|
constructor(container: IServiceContainer) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -22,6 +24,7 @@ export class RustBacktestAdapter extends EventEmitter {
|
||||||
container.postgres,
|
container.postgres,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
this.strategyExecutor = new StrategyExecutor();
|
||||||
}
|
}
|
||||||
|
|
||||||
async runBacktest(config: BacktestConfig): Promise<BacktestResult> {
|
async runBacktest(config: BacktestConfig): Promise<BacktestResult> {
|
||||||
|
|
@ -237,165 +240,64 @@ export class RustBacktestAdapter extends EventEmitter {
|
||||||
private registerStrategy(strategyName: string, parameters: any): void {
|
private registerStrategy(strategyName: string, parameters: any): void {
|
||||||
if (!this.currentEngine) return;
|
if (!this.currentEngine) return;
|
||||||
|
|
||||||
// Create state for the strategy
|
this.container.logger.info('Registering strategy', {
|
||||||
const priceHistory: Map<string, number[]> = new Map();
|
|
||||||
const positions: Map<string, number> = new Map();
|
|
||||||
const fastPeriod = parameters.fastPeriod || 5;
|
|
||||||
const slowPeriod = parameters.slowPeriod || 15;
|
|
||||||
|
|
||||||
this.container.logger.info('Registering TypeScript strategy', {
|
|
||||||
strategyName,
|
strategyName,
|
||||||
fastPeriod,
|
parameters
|
||||||
slowPeriod
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a TypeScript strategy callback
|
// Check if we should use TypeScript or native Rust implementation
|
||||||
let callCount = 0;
|
const useTypeScript = parameters.useTypeScriptImplementation || false;
|
||||||
const callback = (callJson: string) => {
|
|
||||||
callCount++;
|
if (useTypeScript) {
|
||||||
const call = JSON.parse(callJson);
|
// Register TypeScript strategy
|
||||||
|
this.container.logger.info('Using TypeScript strategy implementation');
|
||||||
|
|
||||||
// Log every 10th call to see if we're getting data
|
// For now, we'll use the SMACrossoverStrategy
|
||||||
if (callCount % 10 === 1) {
|
if (strategyName.toLowerCase().includes('sma') || strategyName.toLowerCase().includes('crossover')) {
|
||||||
this.container.logger.info(`Strategy callback called ${callCount} times, method: ${call.method}`);
|
const strategyId = `strategy-${Date.now()}`;
|
||||||
}
|
this.strategyExecutor.registerStrategy(strategyId, SMACrossoverStrategy);
|
||||||
|
|
||||||
if (call.method === 'on_market_data') {
|
|
||||||
const marketData = call.data;
|
|
||||||
const signals: any[] = [];
|
|
||||||
|
|
||||||
// Debug log first few data points
|
// Create a callback that uses the strategy executor
|
||||||
if (priceHistory.size === 0) {
|
const callback = (callJson: string) => {
|
||||||
this.container.logger.info('First market data received:', JSON.stringify(marketData, null, 2));
|
try {
|
||||||
}
|
const call = JSON.parse(callJson);
|
||||||
|
|
||||||
// For SMA crossover strategy
|
if (call.method === 'on_market_data') {
|
||||||
if (strategyName.toLowerCase().includes('sma') || strategyName.toLowerCase().includes('crossover')) {
|
const signals = this.strategyExecutor.onMarketData(call.data);
|
||||||
// Log the structure to understand the data format
|
return JSON.stringify({ signals });
|
||||||
if (callCount === 1) {
|
} else if (call.method === 'on_fill') {
|
||||||
this.container.logger.info('Market data structure:', {
|
const { symbol, quantity, price, side } = call.data;
|
||||||
hasData: !!marketData.data,
|
this.strategyExecutor.onFill(symbol, quantity, price, side);
|
||||||
hasBar: !!marketData.data?.Bar,
|
return JSON.stringify({ signals: [] });
|
||||||
hasClose: !!marketData.data?.close,
|
}
|
||||||
dataKeys: marketData.data ? Object.keys(marketData.data) : [],
|
|
||||||
});
|
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') {
|
// Use the addNativeStrategy method instead
|
||||||
// Update position tracking
|
this.currentEngine.addNativeStrategy(
|
||||||
const { symbol, quantity, side } = call.data;
|
'sma_crossover', // strategy type
|
||||||
const currentPos = positions.get(symbol) || 0;
|
strategyName,
|
||||||
const newPos = side === 'buy' ? currentPos + quantity : currentPos - quantity;
|
`strategy-${Date.now()}`,
|
||||||
|
parameters
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateExpectancy(metrics: any): number {
|
private calculateExpectancy(metrics: any): number {
|
||||||
|
|
|
||||||
135
apps/stock/orchestrator/src/strategies/StrategyExecutor.ts
Normal file
135
apps/stock/orchestrator/src/strategies/StrategyExecutor.ts
Normal file
|
|
@ -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<string, any> = new Map();
|
||||||
|
private strategyStates: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
registerStrategy(id: string, strategy: any) {
|
||||||
|
this.strategies.set(id, strategy);
|
||||||
|
this.strategyStates.set(id, {
|
||||||
|
priceHistory: new Map<string, number[]>(),
|
||||||
|
positions: new Map<string, number>(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -51,6 +51,7 @@ export function BacktestPage() {
|
||||||
commission: newConfig.commission,
|
commission: newConfig.commission,
|
||||||
slippage: newConfig.slippage,
|
slippage: newConfig.slippage,
|
||||||
speedMultiplier: newConfig.speedMultiplier,
|
speedMultiplier: newConfig.speedMultiplier,
|
||||||
|
useTypeScriptImplementation: true, // Enable TypeScript strategy execution
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [createBacktest]);
|
}, [createBacktest]);
|
||||||
|
|
|
||||||
85
test-typescript-strategy.js
Normal file
85
test-typescript-strategy.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue