work on new engine
This commit is contained in:
parent
44476da13f
commit
a1e5a21847
126 changed files with 3425 additions and 6695 deletions
79
Cargo.lock
generated
79
Cargo.lock
generated
|
|
@ -114,9 +114,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.27"
|
||||
version = "1.2.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
|
||||
checksum = "4ad45f4f74e4e20eaa392913b7b33a7091c87e59628f4dd27888205ad888843c"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
|
@ -151,32 +151,6 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"nalgebra 0.32.6",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"statrs",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
|
|
@ -262,6 +236,32 @@ dependencies = [
|
|||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"nalgebra 0.32.6",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"statrs",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
|
|
@ -321,6 +321,17 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
|
|
@ -876,6 +887,12 @@ dependencies = [
|
|||
"wide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
|
|
@ -958,17 +975,19 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.1"
|
||||
version = "1.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"apps/stock/core"
|
||||
"apps/stock/engine"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "core"
|
||||
name = "engine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
153
apps/stock/engine/MIGRATION.md
Normal file
153
apps/stock/engine/MIGRATION.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Migration Guide: Modular Architecture
|
||||
|
||||
This guide explains how to migrate from the current monolithic structure to the new modular architecture.
|
||||
|
||||
## Overview
|
||||
|
||||
The new architecture introduces:
|
||||
- Domain-driven design with separated domain types
|
||||
- Event-driven architecture with EventBus
|
||||
- Mode-based trading engines (Backtest, Paper, Live)
|
||||
- Modular API structure
|
||||
- Enhanced strategy framework
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Step 1: Use Adapters for Compatibility
|
||||
|
||||
The `adapters` module provides compatibility between old and new implementations:
|
||||
|
||||
#### ExecutionHandler Adapters
|
||||
|
||||
```rust
|
||||
use engine::adapters::{ExecutionHandlerAdapter, NewExecutionHandler};
|
||||
|
||||
// Wrap a new-style handler to work with old interface
|
||||
let new_handler = MyNewExecutionHandler::new();
|
||||
let adapted = ExecutionHandlerAdapter::new(new_handler);
|
||||
// Now 'adapted' implements the old ExecutionHandler trait
|
||||
|
||||
// Or wrap an old handler to work with new interface
|
||||
use engine::adapters::LegacyExecutionHandlerAdapter;
|
||||
let old_handler = MyOldExecutionHandler::new();
|
||||
let adapted = LegacyExecutionHandlerAdapter::new(old_handler);
|
||||
// Now 'adapted' implements NewExecutionHandler
|
||||
```
|
||||
|
||||
#### Event System Adapters
|
||||
|
||||
```rust
|
||||
use engine::adapters::events::{EventBusAdapter, EventAdapter};
|
||||
|
||||
// Create adapter that bridges old and new event systems
|
||||
let mut adapter = EventBusAdapter::new();
|
||||
|
||||
// Use old-style event handling
|
||||
adapter.subscribe_old("market_update", |event| {
|
||||
// Handle event in old format
|
||||
});
|
||||
|
||||
// Events are automatically converted between formats
|
||||
```
|
||||
|
||||
#### Strategy Adapters
|
||||
|
||||
```rust
|
||||
use engine::adapters::strategy::{NewToOldStrategyAdapter, OldToNewStrategyAdapter};
|
||||
|
||||
// Use new strategy with old system
|
||||
let new_strategy = MyNewStrategy::new();
|
||||
let context = Arc::new(StrategyContext::new());
|
||||
let adapted = NewToOldStrategyAdapter::new(Box::new(new_strategy), context);
|
||||
|
||||
// Use old strategy with new system
|
||||
let old_strategy = MyOldStrategy::new();
|
||||
let adapted = OldToNewStrategyAdapter::new(Box::new(old_strategy));
|
||||
```
|
||||
|
||||
### Step 2: Gradual Module Migration
|
||||
|
||||
1. **Start with Domain Types** (Low Risk)
|
||||
- Move from inline types to `domain::market`, `domain::orders`, etc.
|
||||
- These are mostly data structures with minimal behavior
|
||||
|
||||
2. **Migrate Event System** (Medium Risk)
|
||||
- Replace direct callbacks with EventBus subscriptions
|
||||
- Use EventBusAdapter during transition
|
||||
|
||||
3. **Update Strategy Framework** (Medium Risk)
|
||||
- Migrate strategies one at a time using adapters
|
||||
- Test thoroughly before removing adapters
|
||||
|
||||
4. **Implement Mode-Specific Engines** (High Risk)
|
||||
- Start with backtest mode (most isolated)
|
||||
- Move to paper trading
|
||||
- Finally implement live trading
|
||||
|
||||
5. **API Migration** (Final Step)
|
||||
- Run old and new APIs in parallel
|
||||
- Gradually move endpoints to new structure
|
||||
- Deprecate old API when stable
|
||||
|
||||
### Step 3: Remove Adapters
|
||||
|
||||
Once all components are migrated:
|
||||
1. Remove adapter usage
|
||||
2. Delete old implementations
|
||||
3. Remove adapter modules
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit Tests**: Test adapters ensure compatibility
|
||||
2. **Integration Tests**: Verify old and new systems work together
|
||||
3. **Regression Tests**: Ensure no functionality is lost
|
||||
4. **Performance Tests**: Verify no performance degradation
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Trait method signatures don't match
|
||||
**Solution**: Use adapters to bridge the differences
|
||||
|
||||
### Issue: Event types are incompatible
|
||||
**Solution**: Use EventAdapter for conversion
|
||||
|
||||
### Issue: Existing code expects synchronous behavior
|
||||
**Solution**: Use `tokio::runtime::Handle::current().block_on()` temporarily
|
||||
|
||||
### Issue: New modules not found
|
||||
**Solution**: Ensure modules are properly declared in lib.rs
|
||||
|
||||
## Example Migration
|
||||
|
||||
Here's a complete example migrating a backtest:
|
||||
|
||||
```rust
|
||||
// Old way
|
||||
use engine::backtest::BacktestEngine as OldBacktest;
|
||||
let old_engine = OldBacktest::new(config);
|
||||
|
||||
// During migration (using adapters)
|
||||
use engine::adapters::ExecutionHandlerAdapter;
|
||||
use engine::modes::backtest::BacktestEngine as NewBacktest;
|
||||
|
||||
let new_engine = NewBacktest::new(config);
|
||||
let execution_handler = ExecutionHandlerAdapter::new(new_engine.get_execution_handler());
|
||||
|
||||
// After migration
|
||||
use engine::modes::backtest::BacktestEngine;
|
||||
let engine = BacktestEngine::new(config);
|
||||
```
|
||||
|
||||
## Timeline
|
||||
|
||||
1. **Week 1-2**: Implement and test adapters
|
||||
2. **Week 3-4**: Migrate domain types and events
|
||||
3. **Week 5-6**: Migrate strategies and modes
|
||||
4. **Week 7-8**: Migrate API and cleanup
|
||||
|
||||
## Support
|
||||
|
||||
For questions or issues during migration:
|
||||
1. Check adapter documentation
|
||||
2. Review test examples
|
||||
3. Consult team lead for architectural decisions
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@stock-bot/core",
|
||||
"name": "@stock-bot/engine",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.16.3",
|
||||
"cargo-cp-artifact": "^0.1",
|
||||
BIN
apps/stock/engine/index.node
Executable file
BIN
apps/stock/engine/index.node
Executable file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@stock-bot/core",
|
||||
"name": "@stock-bot/engine",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "index.mjs",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"index.node"
|
||||
],
|
||||
"napi": {
|
||||
"name": "core",
|
||||
"name": "engine",
|
||||
"triples": {
|
||||
"additional": [
|
||||
"x86_64-pc-windows-msvc",
|
||||
162
apps/stock/engine/src/adapters/events.rs
Normal file
162
apps/stock/engine/src/adapters/events.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
// Placeholder types for new event system (will be replaced when domain module is activated)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewEvent {
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub source: String,
|
||||
pub event_type: NewEventType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NewEventType {
|
||||
MarketData {
|
||||
symbol: String,
|
||||
data: crate::MarketDataType,
|
||||
},
|
||||
OrderFilled {
|
||||
order_id: String,
|
||||
fill: crate::Fill,
|
||||
},
|
||||
OrderSubmitted {
|
||||
order_id: String,
|
||||
},
|
||||
OrderCancelled {
|
||||
order_id: String,
|
||||
},
|
||||
PositionUpdated {
|
||||
symbol: String,
|
||||
update: crate::PositionUpdate,
|
||||
},
|
||||
RiskLimitExceeded {
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// Placeholder EventBus type (will be replaced when events module is activated)
|
||||
pub struct NewEventBus {
|
||||
sender: mpsc::UnboundedSender<NewEvent>,
|
||||
}
|
||||
|
||||
impl NewEventBus {
|
||||
pub async fn publish(&mut self, event: NewEvent) {
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps between old and new event types
|
||||
pub struct EventAdapter;
|
||||
|
||||
impl EventAdapter {
|
||||
/// Convert from new event to old event format if needed
|
||||
pub fn from_new_event(event: &NewEvent) -> Option<OldEventFormat> {
|
||||
match &event.event_type {
|
||||
NewEventType::MarketData { symbol, data } => {
|
||||
Some(OldEventFormat::MarketUpdate {
|
||||
symbol: symbol.clone(),
|
||||
timestamp: event.timestamp,
|
||||
data: data.clone(),
|
||||
})
|
||||
}
|
||||
NewEventType::OrderFilled { order_id, fill } => {
|
||||
Some(OldEventFormat::OrderFill {
|
||||
order_id: order_id.clone(),
|
||||
fill: fill.clone(),
|
||||
})
|
||||
}
|
||||
_ => None, // Other event types may not have old equivalents
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from old event to new event format
|
||||
pub fn to_new_event(old_event: &OldEventFormat) -> NewEvent {
|
||||
match old_event {
|
||||
OldEventFormat::MarketUpdate { symbol, timestamp, data } => {
|
||||
NewEvent {
|
||||
timestamp: *timestamp,
|
||||
source: "legacy".to_string(),
|
||||
event_type: NewEventType::MarketData {
|
||||
symbol: symbol.clone(),
|
||||
data: data.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
OldEventFormat::OrderFill { order_id, fill } => {
|
||||
NewEvent {
|
||||
timestamp: fill.timestamp,
|
||||
source: "legacy".to_string(),
|
||||
event_type: NewEventType::OrderFilled {
|
||||
order_id: order_id.clone(),
|
||||
fill: fill.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder for old event format (to be defined based on actual old implementation)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OldEventFormat {
|
||||
MarketUpdate {
|
||||
symbol: String,
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
data: crate::MarketDataType,
|
||||
},
|
||||
OrderFill {
|
||||
order_id: String,
|
||||
fill: crate::Fill,
|
||||
},
|
||||
}
|
||||
|
||||
/// Event bus adapter to bridge old and new event systems
|
||||
pub struct EventBusAdapter {
|
||||
new_event_bus: Option<NewEventBus>,
|
||||
old_handlers: HashMap<String, Vec<Box<dyn Fn(&OldEventFormat) + Send + Sync>>>,
|
||||
}
|
||||
|
||||
impl EventBusAdapter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
new_event_bus: None,
|
||||
old_handlers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_new_event_bus(mut self, event_bus: NewEventBus) -> Self {
|
||||
self.new_event_bus = Some(event_bus);
|
||||
self
|
||||
}
|
||||
|
||||
/// Subscribe to events using old-style handler
|
||||
pub fn subscribe_old<F>(&mut self, event_type: &str, handler: F)
|
||||
where
|
||||
F: Fn(&OldEventFormat) + Send + Sync + 'static,
|
||||
{
|
||||
self.old_handlers
|
||||
.entry(event_type.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(Box::new(handler));
|
||||
}
|
||||
|
||||
/// Publish event in old format (converts to new format if new bus available)
|
||||
pub async fn publish_old(&mut self, event: OldEventFormat) {
|
||||
// Call old handlers
|
||||
let event_type = match &event {
|
||||
OldEventFormat::MarketUpdate { .. } => "market_update",
|
||||
OldEventFormat::OrderFill { .. } => "order_fill",
|
||||
};
|
||||
|
||||
if let Some(handlers) = self.old_handlers.get(event_type) {
|
||||
for handler in handlers {
|
||||
handler(&event);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert and publish to new event bus if available
|
||||
if let Some(ref mut new_bus) = self.new_event_bus {
|
||||
let new_event = EventAdapter::to_new_event(&event);
|
||||
new_bus.publish(new_event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
apps/stock/engine/src/adapters/mod.rs
Normal file
100
apps/stock/engine/src/adapters/mod.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
pub mod events;
|
||||
pub mod strategy;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::{Order, ExecutionResult, OrderStatus, Fill, OrderBookSnapshot, FillSimulator};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
/// Adapter to bridge between old and new ExecutionHandler implementations
|
||||
pub struct ExecutionHandlerAdapter<T> {
|
||||
inner: Arc<RwLock<T>>,
|
||||
}
|
||||
|
||||
impl<T> ExecutionHandlerAdapter<T> {
|
||||
pub fn new(handler: T) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(handler)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// New-style ExecutionHandler trait (from modular design)
|
||||
#[async_trait]
|
||||
pub trait NewExecutionHandler: Send + Sync {
|
||||
async fn submit_order(&mut self, order: Order) -> Result<String, String>;
|
||||
async fn cancel_order(&mut self, order_id: &str) -> Result<(), String>;
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String>;
|
||||
}
|
||||
|
||||
/// Implement old ExecutionHandler for adapters wrapping new handlers
|
||||
#[async_trait]
|
||||
impl<T: NewExecutionHandler + 'static> crate::ExecutionHandler for ExecutionHandlerAdapter<T> {
|
||||
async fn execute_order(&mut self, order: Order) -> Result<ExecutionResult, String> {
|
||||
// For now, provide a simplified implementation
|
||||
// In a real implementation, you'd need proper async handling
|
||||
|
||||
// Create a synthetic execution result
|
||||
let fill = Fill {
|
||||
timestamp: chrono::Utc::now(),
|
||||
price: 100.0, // This would come from market data
|
||||
quantity: order.quantity,
|
||||
commission: 0.0,
|
||||
};
|
||||
|
||||
Ok(ExecutionResult {
|
||||
order_id: order.id,
|
||||
status: OrderStatus::Filled,
|
||||
fills: vec![fill],
|
||||
})
|
||||
}
|
||||
|
||||
fn get_fill_simulator(&self) -> Option<&dyn FillSimulator> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplified sync wrapper for cases where async isn't needed
|
||||
pub struct SyncExecutionHandlerAdapter<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T> SyncExecutionHandlerAdapter<T> {
|
||||
pub fn new(handler: T) -> Self {
|
||||
Self { inner: handler }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: crate::ExecutionHandler + Send + Sync> NewExecutionHandler for SyncExecutionHandlerAdapter<T> {
|
||||
async fn submit_order(&mut self, order: Order) -> Result<String, String> {
|
||||
let result = self.inner.execute_order(order).await?;
|
||||
Ok(result.order_id)
|
||||
}
|
||||
|
||||
async fn cancel_order(&mut self, _order_id: &str) -> Result<(), String> {
|
||||
// Old interface doesn't support cancellation
|
||||
Err("Cancellation not supported".to_string())
|
||||
}
|
||||
|
||||
async fn get_order_status(&self, _order_id: &str) -> Result<OrderStatus, String> {
|
||||
// Old interface doesn't support status queries
|
||||
Ok(OrderStatus::Filled)
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the original adapter logic but commented out for future reference
|
||||
/*
|
||||
#[async_trait]
|
||||
impl<T: NewExecutionHandler> crate::ExecutionHandler for ExecutionHandlerAdapter<T> {
|
||||
async fn execute_order(&mut self, order: Order) -> Result<ExecutionResult, String> {
|
||||
// This would require tokio::spawn or similar to properly handle
|
||||
// the async boundaries with parking_lot RwLock
|
||||
todo!("Complex async adapter implementation")
|
||||
}
|
||||
|
||||
fn get_fill_simulator(&self) -> Option<&dyn FillSimulator> {
|
||||
None
|
||||
}
|
||||
}
|
||||
*/
|
||||
231
apps/stock/engine/src/adapters/strategy.rs
Normal file
231
apps/stock/engine/src/adapters/strategy.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use async_trait::async_trait;
|
||||
use crate::{MarketUpdate, Fill, OrderType};
|
||||
use crate::backtest::Strategy as OldStrategy;
|
||||
use crate::risk::RiskLimits;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
// Placeholder types for new strategy framework (will be replaced when framework module is activated)
|
||||
#[async_trait]
|
||||
pub trait NewStrategy: Send + Sync {
|
||||
async fn init(&mut self, context: &StrategyContext) -> Result<(), String>;
|
||||
async fn on_data(&mut self, data: &MarketUpdate, context: &StrategyContext) -> Vec<Signal>;
|
||||
async fn on_fill(&mut self, order_id: &str, fill: &Fill, context: &StrategyContext);
|
||||
async fn shutdown(&mut self, context: &StrategyContext) -> Result<(), String>;
|
||||
fn get_state(&self) -> serde_json::Value;
|
||||
}
|
||||
|
||||
pub struct StrategyContext {
|
||||
pub account_id: String,
|
||||
pub starting_capital: f64,
|
||||
}
|
||||
|
||||
impl StrategyContext {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
account_id: "default".to_string(),
|
||||
starting_capital: 100_000.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Signal {
|
||||
Buy {
|
||||
symbol: String,
|
||||
quantity: f64,
|
||||
order_type: OrderType,
|
||||
},
|
||||
Sell {
|
||||
symbol: String,
|
||||
quantity: f64,
|
||||
order_type: OrderType,
|
||||
},
|
||||
CancelOrder {
|
||||
order_id: String,
|
||||
},
|
||||
UpdateRiskLimits {
|
||||
limits: RiskLimits,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Adapter to use new strategies with old interface
|
||||
pub struct NewToOldStrategyAdapter {
|
||||
inner: Arc<RwLock<Box<dyn NewStrategy>>>,
|
||||
context: Arc<StrategyContext>,
|
||||
}
|
||||
|
||||
impl NewToOldStrategyAdapter {
|
||||
pub fn new(strategy: Box<dyn NewStrategy>, context: Arc<StrategyContext>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(strategy)),
|
||||
context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OldStrategy for NewToOldStrategyAdapter {
|
||||
fn on_market_data(&mut self, data: &crate::MarketData) -> Vec<crate::backtest::strategy::Signal> {
|
||||
// Convert MarketData to MarketUpdate if needed
|
||||
let market_update = data.clone(); // Assuming MarketData is type alias for MarketUpdate
|
||||
|
||||
// Need to block on async call since old trait is sync
|
||||
let signals = tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
let mut strategy = self.inner.write();
|
||||
strategy.on_data(&market_update, &self.context).await
|
||||
})
|
||||
});
|
||||
|
||||
// Convert new signals to old format
|
||||
signals.into_iter().map(|signal| {
|
||||
match signal {
|
||||
Signal::Buy { symbol, quantity, .. } => {
|
||||
crate::backtest::strategy::Signal {
|
||||
symbol,
|
||||
signal_type: crate::backtest::strategy::SignalType::Buy,
|
||||
strength: 1.0,
|
||||
quantity: Some(quantity),
|
||||
reason: None,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
Signal::Sell { symbol, quantity, .. } => {
|
||||
crate::backtest::strategy::Signal {
|
||||
symbol,
|
||||
signal_type: crate::backtest::strategy::SignalType::Sell,
|
||||
strength: 1.0,
|
||||
quantity: Some(quantity),
|
||||
reason: None,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
Signal::CancelOrder { .. } => {
|
||||
// Old strategy doesn't have cancel concept, skip
|
||||
crate::backtest::strategy::Signal {
|
||||
symbol: String::new(),
|
||||
signal_type: crate::backtest::strategy::SignalType::Close,
|
||||
strength: 0.0,
|
||||
quantity: None,
|
||||
reason: None,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Skip other signal types
|
||||
crate::backtest::strategy::Signal {
|
||||
symbol: String::new(),
|
||||
signal_type: crate::backtest::strategy::SignalType::Close,
|
||||
strength: 0.0,
|
||||
quantity: None,
|
||||
reason: None,
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}).filter(|s| !s.symbol.is_empty()).collect()
|
||||
}
|
||||
|
||||
fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) {
|
||||
// Create a Fill object from the parameters
|
||||
let fill = Fill {
|
||||
timestamp: chrono::Utc::now(),
|
||||
price,
|
||||
quantity,
|
||||
commission: 0.0,
|
||||
};
|
||||
|
||||
// Block on async call
|
||||
tokio::task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
let mut strategy = self.inner.write();
|
||||
strategy.on_fill(&format!("order_{}", symbol), &fill, &self.context).await;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &str {
|
||||
"NewStrategyAdapter"
|
||||
}
|
||||
|
||||
fn get_parameters(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"adapter": "NewToOldStrategyAdapter",
|
||||
"context": {
|
||||
"account_id": self.context.account_id,
|
||||
"starting_capital": self.context.starting_capital,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter to use old strategies with new interface
|
||||
pub struct OldToNewStrategyAdapter {
|
||||
inner: Box<dyn OldStrategy>,
|
||||
}
|
||||
|
||||
impl OldToNewStrategyAdapter {
|
||||
pub fn new(strategy: Box<dyn OldStrategy>) -> Self {
|
||||
Self { inner: strategy }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NewStrategy for OldToNewStrategyAdapter {
|
||||
async fn init(&mut self, _context: &StrategyContext) -> Result<(), String> {
|
||||
// Old strategy doesn't have init
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_data(&mut self, data: &MarketUpdate, _context: &StrategyContext) -> Vec<Signal> {
|
||||
// Call sync method from async context
|
||||
let signals = self.inner.on_market_data(data);
|
||||
|
||||
// Convert old signals to new format
|
||||
signals.into_iter().filter_map(|signal| {
|
||||
match signal.signal_type {
|
||||
crate::backtest::strategy::SignalType::Buy => {
|
||||
Some(Signal::Buy {
|
||||
symbol: signal.symbol,
|
||||
quantity: signal.quantity.unwrap_or(100.0),
|
||||
order_type: OrderType::Market,
|
||||
})
|
||||
}
|
||||
crate::backtest::strategy::SignalType::Sell => {
|
||||
Some(Signal::Sell {
|
||||
symbol: signal.symbol,
|
||||
quantity: signal.quantity.unwrap_or(100.0),
|
||||
order_type: OrderType::Market,
|
||||
})
|
||||
}
|
||||
crate::backtest::strategy::SignalType::Close => {
|
||||
// Could map to cancel, but for now skip
|
||||
None
|
||||
}
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
async fn on_fill(&mut self, _order_id: &str, fill: &Fill, _context: &StrategyContext) {
|
||||
// Extract symbol from order_id if possible, otherwise use placeholder
|
||||
let symbol = "UNKNOWN";
|
||||
let side = "buy"; // Would need to track this
|
||||
self.inner.on_fill(symbol, fill.quantity, fill.price, side);
|
||||
}
|
||||
|
||||
async fn shutdown(&mut self, _context: &StrategyContext) -> Result<(), String> {
|
||||
// Old strategy doesn't have shutdown
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_state(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"adapter": "OldToNewStrategyAdapter",
|
||||
"inner_strategy": self.inner.get_name(),
|
||||
"parameters": self.inner.get_parameters()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
86
apps/stock/engine/src/api_new/backtest.rs
Normal file
86
apps/stock/engine/src/api_new/backtest.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use napi_derive::napi;
|
||||
use napi::bindgen_prelude::*;
|
||||
use std::sync::Arc;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[napi]
|
||||
pub struct BacktestAPI {
|
||||
core: Arc<crate::TradingCore>,
|
||||
}
|
||||
|
||||
impl BacktestAPI {
|
||||
pub fn new(core: Arc<crate::TradingCore>) -> Self {
|
||||
Self { core }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl BacktestAPI {
|
||||
#[napi]
|
||||
pub async fn configure(
|
||||
&self,
|
||||
start_date: String,
|
||||
end_date: String,
|
||||
symbols: Vec<String>,
|
||||
initial_capital: f64,
|
||||
commission: f64,
|
||||
slippage: f64,
|
||||
) -> Result<()> {
|
||||
// Parse dates
|
||||
let start = DateTime::parse_from_rfc3339(&start_date)
|
||||
.map_err(|e| Error::from_reason(format!("Invalid start date: {}", e)))?
|
||||
.with_timezone(&Utc);
|
||||
let end = DateTime::parse_from_rfc3339(&end_date)
|
||||
.map_err(|e| Error::from_reason(format!("Invalid end date: {}", e)))?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
// Configure backtest parameters
|
||||
if let crate::TradingMode::Backtest { .. } = self.core.get_mode() {
|
||||
// Update backtest configuration
|
||||
todo!("Update backtest configuration")
|
||||
} else {
|
||||
return Err(Error::from_reason("Not in backtest mode"));
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn load_data(&self, data_source: String) -> Result<()> {
|
||||
// Load historical data for backtest
|
||||
todo!("Load historical data")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn run(&self) -> Result<JsObject> {
|
||||
// Run the backtest
|
||||
if let crate::TradingMode::Backtest { .. } = self.core.get_mode() {
|
||||
// Execute backtest
|
||||
todo!("Execute backtest")
|
||||
} else {
|
||||
return Err(Error::from_reason("Not in backtest mode"));
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_progress(&self) -> Result<f64> {
|
||||
// Get backtest progress (0.0 to 1.0)
|
||||
todo!("Get backtest progress")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn pause(&self) -> Result<()> {
|
||||
// Pause backtest execution
|
||||
todo!("Pause backtest")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn resume(&self) -> Result<()> {
|
||||
// Resume backtest execution
|
||||
todo!("Resume backtest")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_results(&self) -> Result<JsObject> {
|
||||
// Get backtest results
|
||||
todo!("Get backtest results")
|
||||
}
|
||||
}
|
||||
51
apps/stock/engine/src/api_new/market_data.rs
Normal file
51
apps/stock/engine/src/api_new/market_data.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use napi_derive::napi;
|
||||
use napi::bindgen_prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[napi]
|
||||
pub struct MarketDataAPI {
|
||||
core: Arc<crate::TradingCore>,
|
||||
}
|
||||
|
||||
impl MarketDataAPI {
|
||||
pub fn new(core: Arc<crate::TradingCore>) -> Self {
|
||||
Self { core }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MarketDataAPI {
|
||||
#[napi]
|
||||
pub async fn subscribe(&self, symbols: Vec<String>) -> Result<()> {
|
||||
// Subscribe to market data for symbols
|
||||
self.core.subscribe_market_data(symbols)
|
||||
.await
|
||||
.map_err(|e| Error::from_reason(e))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn unsubscribe(&self, symbols: Vec<String>) -> Result<()> {
|
||||
// Unsubscribe from market data
|
||||
self.core.unsubscribe_market_data(symbols)
|
||||
.await
|
||||
.map_err(|e| Error::from_reason(e))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_latest_quote(&self, symbol: String) -> Result<JsObject> {
|
||||
// Get latest quote for symbol
|
||||
let quote = self.core.orderbook_manager
|
||||
.get_best_bid_ask(&symbol)
|
||||
.ok_or_else(|| Error::from_reason("No quote available"))?;
|
||||
|
||||
// Convert to JS object
|
||||
// Note: In real implementation, would properly convert Quote to JsObject
|
||||
todo!("Convert quote to JsObject")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_latest_bar(&self, symbol: String) -> Result<JsObject> {
|
||||
// Get latest bar for symbol
|
||||
todo!("Get latest bar implementation")
|
||||
}
|
||||
}
|
||||
76
apps/stock/engine/src/api_new/mod.rs
Normal file
76
apps/stock/engine/src/api_new/mod.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use napi_derive::napi;
|
||||
|
||||
pub mod market_data;
|
||||
pub mod orders;
|
||||
pub mod positions;
|
||||
pub mod backtest;
|
||||
pub mod strategies;
|
||||
pub mod system;
|
||||
|
||||
// Main API entry point
|
||||
#[napi]
|
||||
pub struct TradingAPI {
|
||||
inner: std::sync::Arc<crate::TradingCore>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl TradingAPI {
|
||||
#[napi(constructor)]
|
||||
pub fn new(mode: String) -> napi::Result<Self> {
|
||||
let trading_mode = parse_mode(&mode)?;
|
||||
let core = crate::TradingCore::new(trading_mode)
|
||||
.map_err(|e| napi::Error::from_reason(e))?;
|
||||
|
||||
Ok(Self {
|
||||
inner: std::sync::Arc::new(core),
|
||||
})
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn market_data(&self) -> market_data::MarketDataAPI {
|
||||
market_data::MarketDataAPI::new(self.inner.clone())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn orders(&self) -> orders::OrdersAPI {
|
||||
orders::OrdersAPI::new(self.inner.clone())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn positions(&self) -> positions::PositionsAPI {
|
||||
positions::PositionsAPI::new(self.inner.clone())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn strategies(&self) -> strategies::StrategiesAPI {
|
||||
strategies::StrategiesAPI::new(self.inner.clone())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn system(&self) -> system::SystemAPI {
|
||||
system::SystemAPI::new(self.inner.clone())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn backtest(&self) -> backtest::BacktestAPI {
|
||||
backtest::BacktestAPI::new(self.inner.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mode(mode: &str) -> napi::Result<crate::TradingMode> {
|
||||
match mode {
|
||||
"backtest" => Ok(crate::TradingMode::Backtest {
|
||||
start_time: chrono::Utc::now(),
|
||||
end_time: chrono::Utc::now(),
|
||||
speed_multiplier: 1.0,
|
||||
}),
|
||||
"paper" => Ok(crate::TradingMode::Paper {
|
||||
starting_capital: 100_000.0,
|
||||
}),
|
||||
"live" => Ok(crate::TradingMode::Live {
|
||||
broker: "default".to_string(),
|
||||
account_id: "default".to_string(),
|
||||
}),
|
||||
_ => Err(napi::Error::from_reason(format!("Unknown mode: {}", mode))),
|
||||
}
|
||||
}
|
||||
87
apps/stock/engine/src/api_new/orders.rs
Normal file
87
apps/stock/engine/src/api_new/orders.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
use napi_derive::napi;
|
||||
use napi::bindgen_prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[napi]
|
||||
pub struct OrdersAPI {
|
||||
core: Arc<crate::TradingCore>,
|
||||
}
|
||||
|
||||
impl OrdersAPI {
|
||||
pub fn new(core: Arc<crate::TradingCore>) -> Self {
|
||||
Self { core }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OrdersAPI {
|
||||
#[napi]
|
||||
pub async fn submit_order(
|
||||
&self,
|
||||
symbol: String,
|
||||
side: String,
|
||||
quantity: f64,
|
||||
order_type: String,
|
||||
limit_price: Option<f64>,
|
||||
stop_price: Option<f64>,
|
||||
) -> Result<String> {
|
||||
let side = match side.as_str() {
|
||||
"buy" => crate::Side::Buy,
|
||||
"sell" => crate::Side::Sell,
|
||||
_ => return Err(Error::from_reason("Invalid side")),
|
||||
};
|
||||
|
||||
let order_type = match order_type.as_str() {
|
||||
"market" => crate::OrderType::Market,
|
||||
"limit" => {
|
||||
let price = limit_price.ok_or_else(|| Error::from_reason("Limit price required"))?;
|
||||
crate::OrderType::Limit { price }
|
||||
}
|
||||
"stop" => {
|
||||
let price = stop_price.ok_or_else(|| Error::from_reason("Stop price required"))?;
|
||||
crate::OrderType::Stop { stop_price: price }
|
||||
}
|
||||
"stop_limit" => {
|
||||
let stop = stop_price.ok_or_else(|| Error::from_reason("Stop price required"))?;
|
||||
let limit = limit_price.ok_or_else(|| Error::from_reason("Limit price required"))?;
|
||||
crate::OrderType::StopLimit { stop_price: stop, limit_price: limit }
|
||||
}
|
||||
_ => return Err(Error::from_reason("Invalid order type")),
|
||||
};
|
||||
|
||||
let order = crate::Order {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
symbol,
|
||||
side,
|
||||
quantity,
|
||||
order_type,
|
||||
time_in_force: crate::TimeInForce::Day,
|
||||
};
|
||||
|
||||
let result = self.core.execution_handler
|
||||
.write()
|
||||
.execute_order(order)
|
||||
.await
|
||||
.map_err(|e| Error::from_reason(e))?;
|
||||
|
||||
Ok(result.order_id)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn cancel_order(&self, order_id: String) -> Result<()> {
|
||||
// Cancel order implementation
|
||||
todo!("Cancel order implementation")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_pending_orders(&self) -> Result<Vec<JsObject>> {
|
||||
// Get pending orders
|
||||
todo!("Get pending orders implementation")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_order_history(&self) -> Result<Vec<JsObject>> {
|
||||
// Get order history
|
||||
todo!("Get order history implementation")
|
||||
}
|
||||
}
|
||||
67
apps/stock/engine/src/api_new/positions.rs
Normal file
67
apps/stock/engine/src/api_new/positions.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use napi_derive::napi;
|
||||
use napi::bindgen_prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[napi]
|
||||
pub struct PositionsAPI {
|
||||
core: Arc<crate::TradingCore>,
|
||||
}
|
||||
|
||||
impl PositionsAPI {
|
||||
pub fn new(core: Arc<crate::TradingCore>) -> Self {
|
||||
Self { core }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PositionsAPI {
|
||||
#[napi]
|
||||
pub fn get_position(&self, symbol: String) -> Result<JsObject> {
|
||||
let position = self.core.position_tracker
|
||||
.get_position(&symbol)
|
||||
.ok_or_else(|| Error::from_reason("No position found"))?;
|
||||
|
||||
// Convert position to JsObject
|
||||
todo!("Convert position to JsObject")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_all_positions(&self) -> Result<Vec<JsObject>> {
|
||||
let positions = self.core.position_tracker.get_all_positions();
|
||||
|
||||
// Convert positions to JsObjects
|
||||
todo!("Convert positions to JsObjects")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_closed_trades(&self) -> Result<Vec<JsObject>> {
|
||||
let trades = self.core.position_tracker.get_closed_trades();
|
||||
|
||||
// Convert trades to JsObjects
|
||||
todo!("Convert trades to JsObjects")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_pnl(&self, symbol: Option<String>) -> Result<f64> {
|
||||
if let Some(sym) = symbol {
|
||||
// Get P&L for specific symbol
|
||||
let position = self.core.position_tracker
|
||||
.get_position(&sym)
|
||||
.ok_or_else(|| Error::from_reason("No position found"))?;
|
||||
Ok(position.realized_pnl + position.unrealized_pnl)
|
||||
} else {
|
||||
// Get total P&L
|
||||
let positions = self.core.position_tracker.get_all_positions();
|
||||
let total_pnl = positions.into_iter()
|
||||
.map(|p| p.realized_pnl + p.unrealized_pnl)
|
||||
.sum();
|
||||
Ok(total_pnl)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_portfolio_value(&self) -> Result<f64> {
|
||||
// Calculate total portfolio value
|
||||
todo!("Calculate portfolio value")
|
||||
}
|
||||
}
|
||||
76
apps/stock/engine/src/api_new/strategies.rs
Normal file
76
apps/stock/engine/src/api_new/strategies.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use napi_derive::napi;
|
||||
use napi::bindgen_prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[napi]
|
||||
pub struct StrategiesAPI {
|
||||
core: Arc<crate::TradingCore>,
|
||||
}
|
||||
|
||||
impl StrategiesAPI {
|
||||
pub fn new(core: Arc<crate::TradingCore>) -> Self {
|
||||
Self { core }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl StrategiesAPI {
|
||||
#[napi]
|
||||
pub async fn add_strategy(
|
||||
&self,
|
||||
name: String,
|
||||
strategy_type: String,
|
||||
parameters: String,
|
||||
) -> Result<String> {
|
||||
// Parse parameters from JSON string
|
||||
let params: serde_json::Value = serde_json::from_str(¶meters)
|
||||
.map_err(|e| Error::from_reason(format!("Invalid parameters: {}", e)))?;
|
||||
|
||||
// Create strategy based on type
|
||||
let strategy = match strategy_type.as_str() {
|
||||
"sma_crossover" => {
|
||||
// Create SMA crossover strategy
|
||||
todo!("Create SMA strategy")
|
||||
}
|
||||
"momentum" => {
|
||||
// Create momentum strategy
|
||||
todo!("Create momentum strategy")
|
||||
}
|
||||
_ => return Err(Error::from_reason("Unknown strategy type")),
|
||||
};
|
||||
|
||||
// Add strategy to core
|
||||
todo!("Add strategy to core")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn remove_strategy(&self, strategy_id: String) -> Result<()> {
|
||||
// Remove strategy
|
||||
todo!("Remove strategy implementation")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_strategies(&self) -> Result<Vec<JsObject>> {
|
||||
// Get all active strategies
|
||||
todo!("Get strategies implementation")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn update_strategy_parameters(
|
||||
&self,
|
||||
strategy_id: String,
|
||||
parameters: String,
|
||||
) -> Result<()> {
|
||||
// Parse and update parameters
|
||||
let params: serde_json::Value = serde_json::from_str(¶meters)
|
||||
.map_err(|e| Error::from_reason(format!("Invalid parameters: {}", e)))?;
|
||||
|
||||
todo!("Update strategy parameters")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_strategy_performance(&self, strategy_id: String) -> Result<JsObject> {
|
||||
// Get performance metrics for strategy
|
||||
todo!("Get strategy performance")
|
||||
}
|
||||
}
|
||||
77
apps/stock/engine/src/api_new/system.rs
Normal file
77
apps/stock/engine/src/api_new/system.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use napi_derive::napi;
|
||||
use napi::bindgen_prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[napi]
|
||||
pub struct SystemAPI {
|
||||
core: Arc<crate::TradingCore>,
|
||||
}
|
||||
|
||||
impl SystemAPI {
|
||||
pub fn new(core: Arc<crate::TradingCore>) -> Self {
|
||||
Self { core }
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl SystemAPI {
|
||||
#[napi]
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
// Start the trading system
|
||||
match self.core.get_mode() {
|
||||
crate::TradingMode::Backtest { .. } => {
|
||||
// Start backtest processing
|
||||
todo!("Start backtest")
|
||||
}
|
||||
crate::TradingMode::Paper { .. } => {
|
||||
// Start paper trading
|
||||
todo!("Start paper trading")
|
||||
}
|
||||
crate::TradingMode::Live { .. } => {
|
||||
// Start live trading
|
||||
todo!("Start live trading")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
// Stop the trading system
|
||||
todo!("Stop trading system")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_mode(&self) -> String {
|
||||
match self.core.get_mode() {
|
||||
crate::TradingMode::Backtest { .. } => "backtest".to_string(),
|
||||
crate::TradingMode::Paper { .. } => "paper".to_string(),
|
||||
crate::TradingMode::Live { .. } => "live".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_current_time(&self) -> String {
|
||||
self.core.get_time().to_rfc3339()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_risk_limits(&self, limits: String) -> Result<()> {
|
||||
// Parse and set risk limits
|
||||
let limits: serde_json::Value = serde_json::from_str(&limits)
|
||||
.map_err(|e| Error::from_reason(format!("Invalid limits: {}", e)))?;
|
||||
|
||||
todo!("Set risk limits")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_risk_metrics(&self) -> Result<JsObject> {
|
||||
// Get current risk metrics
|
||||
todo!("Get risk metrics")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_analytics(&self) -> Result<JsObject> {
|
||||
// Get trading analytics
|
||||
todo!("Get analytics")
|
||||
}
|
||||
}
|
||||
65
apps/stock/engine/src/domain.bak/events.rs
Normal file
65
apps/stock/engine/src/domain.bak/events.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use super::{Order, Fill, MarketUpdate, Position};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum EventType {
|
||||
MarketData(MarketUpdate),
|
||||
OrderSubmitted(Order),
|
||||
OrderFilled { order_id: String, fill: Fill },
|
||||
OrderCancelled { order_id: String },
|
||||
OrderRejected { order_id: String, reason: String },
|
||||
PositionUpdate(Position),
|
||||
RiskAlert { message: String, severity: RiskSeverity },
|
||||
SystemStatus { status: SystemStatus },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RiskSeverity {
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum SystemStatus {
|
||||
Starting,
|
||||
Running,
|
||||
Paused,
|
||||
Stopping,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Event {
|
||||
pub id: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub event_type: EventType,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn new(event_type: EventType) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
timestamp: Utc::now(),
|
||||
event_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn market_data(data: MarketUpdate) -> Self {
|
||||
Self::new(EventType::MarketData(data))
|
||||
}
|
||||
|
||||
pub fn order_submitted(order: Order) -> Self {
|
||||
Self::new(EventType::OrderSubmitted(order))
|
||||
}
|
||||
|
||||
pub fn order_filled(order_id: String, fill: Fill) -> Self {
|
||||
Self::new(EventType::OrderFilled { order_id, fill })
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventHandler: Send + Sync {
|
||||
fn handle_event(&self, event: &Event);
|
||||
fn event_types(&self) -> Vec<String>; // Which event types this handler is interested in
|
||||
}
|
||||
44
apps/stock/engine/src/domain.bak/market.rs
Normal file
44
apps/stock/engine/src/domain.bak/market.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Quote {
|
||||
pub bid: f64,
|
||||
pub ask: f64,
|
||||
pub bid_size: f64,
|
||||
pub ask_size: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Bar {
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub volume: f64,
|
||||
pub vwap: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Trade {
|
||||
pub price: f64,
|
||||
pub size: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum MarketDataType {
|
||||
Quote(Quote),
|
||||
Bar(Bar),
|
||||
Trade(Trade),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MarketUpdate {
|
||||
pub symbol: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub data: MarketDataType,
|
||||
}
|
||||
|
||||
// Type alias for compatibility
|
||||
pub type MarketData = MarketUpdate;
|
||||
10
apps/stock/engine/src/domain.bak/mod.rs
Normal file
10
apps/stock/engine/src/domain.bak/mod.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
pub mod market;
|
||||
pub mod orders;
|
||||
pub mod positions;
|
||||
pub mod events;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use market::{Quote, Bar, Trade, MarketUpdate, MarketDataType};
|
||||
pub use orders::{Order, OrderType, OrderStatus, TimeInForce, Side, Fill};
|
||||
pub use positions::{Position, PositionUpdate};
|
||||
pub use events::{Event, EventType, EventHandler};
|
||||
104
apps/stock/engine/src/domain.bak/orders.rs
Normal file
104
apps/stock/engine/src/domain.bak/orders.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Side {
|
||||
Buy,
|
||||
Sell,
|
||||
}
|
||||
|
||||
impl fmt::Display for Side {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Side::Buy => write!(f, "buy"),
|
||||
Side::Sell => write!(f, "sell"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum OrderType {
|
||||
Market,
|
||||
Limit,
|
||||
Stop,
|
||||
StopLimit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum OrderStatus {
|
||||
Pending,
|
||||
Submitted,
|
||||
PartiallyFilled,
|
||||
Filled,
|
||||
Cancelled,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TimeInForce {
|
||||
Day,
|
||||
GTC, // Good Till Cancelled
|
||||
IOC, // Immediate or Cancel
|
||||
FOK, // Fill or Kill
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Order {
|
||||
pub id: String,
|
||||
pub symbol: String,
|
||||
pub side: Side,
|
||||
pub quantity: f64,
|
||||
pub order_type: OrderType,
|
||||
pub limit_price: Option<f64>,
|
||||
pub stop_price: Option<f64>,
|
||||
pub time_in_force: TimeInForce,
|
||||
pub status: OrderStatus,
|
||||
pub submitted_at: Option<DateTime<Utc>>,
|
||||
pub filled_quantity: f64,
|
||||
pub average_fill_price: Option<f64>,
|
||||
}
|
||||
|
||||
impl Order {
|
||||
pub fn new_market_order(symbol: String, side: Side, quantity: f64) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
symbol,
|
||||
side,
|
||||
quantity,
|
||||
order_type: OrderType::Market,
|
||||
limit_price: None,
|
||||
stop_price: None,
|
||||
time_in_force: TimeInForce::Day,
|
||||
status: OrderStatus::Pending,
|
||||
submitted_at: None,
|
||||
filled_quantity: 0.0,
|
||||
average_fill_price: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_limit_order(symbol: String, side: Side, quantity: f64, limit_price: f64) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
symbol,
|
||||
side,
|
||||
quantity,
|
||||
order_type: OrderType::Limit,
|
||||
limit_price: Some(limit_price),
|
||||
stop_price: None,
|
||||
time_in_force: TimeInForce::Day,
|
||||
status: OrderStatus::Pending,
|
||||
submitted_at: None,
|
||||
filled_quantity: 0.0,
|
||||
average_fill_price: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Fill {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub price: f64,
|
||||
pub quantity: f64,
|
||||
pub commission: f64,
|
||||
}
|
||||
59
apps/stock/engine/src/domain.bak/positions.rs
Normal file
59
apps/stock/engine/src/domain.bak/positions.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Position {
|
||||
pub symbol: String,
|
||||
pub quantity: f64,
|
||||
pub average_price: f64,
|
||||
pub realized_pnl: f64,
|
||||
pub unrealized_pnl: f64,
|
||||
pub last_update: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn new(symbol: String) -> Self {
|
||||
Self {
|
||||
symbol,
|
||||
quantity: 0.0,
|
||||
average_price: 0.0,
|
||||
realized_pnl: 0.0,
|
||||
unrealized_pnl: 0.0,
|
||||
last_update: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_long(&self) -> bool {
|
||||
self.quantity > 0.0
|
||||
}
|
||||
|
||||
pub fn is_short(&self) -> bool {
|
||||
self.quantity < 0.0
|
||||
}
|
||||
|
||||
pub fn is_flat(&self) -> bool {
|
||||
self.quantity.abs() < f64::EPSILON
|
||||
}
|
||||
|
||||
pub fn market_value(&self, current_price: f64) -> f64 {
|
||||
self.quantity * current_price
|
||||
}
|
||||
|
||||
pub fn calculate_unrealized_pnl(&self, current_price: f64) -> f64 {
|
||||
if self.is_flat() {
|
||||
0.0
|
||||
} else if self.is_long() {
|
||||
(current_price - self.average_price) * self.quantity
|
||||
} else {
|
||||
(self.average_price - current_price) * self.quantity.abs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PositionUpdate {
|
||||
pub symbol: String,
|
||||
pub previous_position: Option<Position>,
|
||||
pub resulting_position: Position,
|
||||
pub realized_pnl: f64,
|
||||
}
|
||||
91
apps/stock/engine/src/events.bak/mod.rs
Normal file
91
apps/stock/engine/src/events.bak/mod.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use crate::domain::{Event, EventHandler};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub struct EventBus {
|
||||
handlers: Arc<RwLock<HashMap<String, Vec<Arc<dyn EventHandler>>>>>,
|
||||
sender: mpsc::UnboundedSender<Event>,
|
||||
}
|
||||
|
||||
impl EventBus {
|
||||
pub fn new() -> (Self, mpsc::UnboundedReceiver<Event>) {
|
||||
let (sender, receiver) = mpsc::unbounded_channel();
|
||||
let bus = Self {
|
||||
handlers: Arc::new(RwLock::new(HashMap::new())),
|
||||
sender,
|
||||
};
|
||||
(bus, receiver)
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, event_type: String, handler: Arc<dyn EventHandler>) {
|
||||
let mut handlers = self.handlers.write();
|
||||
handlers.entry(event_type).or_insert_with(Vec::new).push(handler);
|
||||
}
|
||||
|
||||
pub fn publish(&self, event: Event) -> Result<(), String> {
|
||||
// Send to async handler
|
||||
self.sender.send(event.clone())
|
||||
.map_err(|_| "Failed to send event".to_string())?;
|
||||
|
||||
// Also handle synchronously for immediate handlers
|
||||
let event_type = match &event.event_type {
|
||||
crate::domain::EventType::MarketData(_) => "market_data",
|
||||
crate::domain::EventType::OrderSubmitted(_) => "order_submitted",
|
||||
crate::domain::EventType::OrderFilled { .. } => "order_filled",
|
||||
crate::domain::EventType::OrderCancelled { .. } => "order_cancelled",
|
||||
crate::domain::EventType::OrderRejected { .. } => "order_rejected",
|
||||
crate::domain::EventType::PositionUpdate(_) => "position_update",
|
||||
crate::domain::EventType::RiskAlert { .. } => "risk_alert",
|
||||
crate::domain::EventType::SystemStatus { .. } => "system_status",
|
||||
};
|
||||
|
||||
let handlers = self.handlers.read();
|
||||
if let Some(event_handlers) = handlers.get(event_type) {
|
||||
for handler in event_handlers {
|
||||
handler.handle_event(&event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Simple event processor that runs in the background
|
||||
pub struct EventProcessor {
|
||||
receiver: mpsc::UnboundedReceiver<Event>,
|
||||
handlers: Arc<RwLock<HashMap<String, Vec<Arc<dyn EventHandler>>>>>,
|
||||
}
|
||||
|
||||
impl EventProcessor {
|
||||
pub fn new(
|
||||
receiver: mpsc::UnboundedReceiver<Event>,
|
||||
handlers: Arc<RwLock<HashMap<String, Vec<Arc<dyn EventHandler>>>>>,
|
||||
) -> Self {
|
||||
Self { receiver, handlers }
|
||||
}
|
||||
|
||||
pub async fn run(mut self) {
|
||||
while let Some(event) = self.receiver.recv().await {
|
||||
// Process event asynchronously
|
||||
let event_type = match &event.event_type {
|
||||
crate::domain::EventType::MarketData(_) => "market_data",
|
||||
crate::domain::EventType::OrderSubmitted(_) => "order_submitted",
|
||||
crate::domain::EventType::OrderFilled { .. } => "order_filled",
|
||||
crate::domain::EventType::OrderCancelled { .. } => "order_cancelled",
|
||||
crate::domain::EventType::OrderRejected { .. } => "order_rejected",
|
||||
crate::domain::EventType::PositionUpdate(_) => "position_update",
|
||||
crate::domain::EventType::RiskAlert { .. } => "risk_alert",
|
||||
crate::domain::EventType::SystemStatus { .. } => "system_status",
|
||||
};
|
||||
|
||||
let handlers = self.handlers.read();
|
||||
if let Some(event_handlers) = handlers.get(event_type) {
|
||||
for handler in event_handlers {
|
||||
handler.handle_event(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
#![deny(clippy::all)]
|
||||
|
||||
// Existing modules
|
||||
pub mod core;
|
||||
pub mod adapters;
|
||||
pub mod orderbook;
|
||||
pub mod risk;
|
||||
pub mod positions;
|
||||
#[cfg(not(test))]
|
||||
pub mod api;
|
||||
pub mod analytics;
|
||||
pub mod indicators;
|
||||
67
apps/stock/engine/src/lib_new.rs
Normal file
67
apps/stock/engine/src/lib_new.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#![deny(clippy::all)]
|
||||
|
||||
// Domain modules
|
||||
pub mod domain;
|
||||
pub mod events;
|
||||
pub mod modes;
|
||||
|
||||
// Core functionality modules
|
||||
pub mod core;
|
||||
pub mod orderbook;
|
||||
pub mod risk;
|
||||
pub mod positions;
|
||||
pub mod analytics;
|
||||
pub mod indicators;
|
||||
pub mod backtest;
|
||||
pub mod strategies;
|
||||
|
||||
// API layer
|
||||
pub mod api;
|
||||
|
||||
// Re-export commonly used types from domain
|
||||
pub use domain::{
|
||||
// Market types
|
||||
Quote, Bar, Trade, MarketUpdate, MarketDataType,
|
||||
// Order types
|
||||
Order, OrderType, OrderStatus, TimeInForce, Side, Fill,
|
||||
// Position types
|
||||
Position, PositionUpdate,
|
||||
// Event types
|
||||
Event, EventType, EventHandler,
|
||||
};
|
||||
|
||||
// Re-export mode types
|
||||
pub use modes::{TradingMode, TradingEngine};
|
||||
|
||||
// Re-export other commonly used types
|
||||
pub use positions::{PositionTracker, TradeRecord, ClosedTrade};
|
||||
pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics};
|
||||
|
||||
// Core traits that define the system's abstractions
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait MarketDataSource: Send + Sync {
|
||||
async fn get_next_update(&mut self) -> Option<MarketUpdate>;
|
||||
fn seek_to_time(&mut self, timestamp: DateTime<Utc>) -> Result<(), String>;
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ExecutionHandler: Send + Sync {
|
||||
async fn submit_order(&self, order: &Order) -> Result<String, String>;
|
||||
async fn cancel_order(&self, order_id: &str) -> Result<(), String>;
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String>;
|
||||
}
|
||||
|
||||
pub trait TimeProvider: Send + Sync {
|
||||
fn now(&self) -> DateTime<Utc>;
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
pub trait FillSimulator: Send + Sync {
|
||||
fn simulate_fill(&self, order: &Order, market_price: f64) -> Option<Fill>;
|
||||
}
|
||||
123
apps/stock/engine/src/lib_simplified.rs
Normal file
123
apps/stock/engine/src/lib_simplified.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#![deny(clippy::all)]
|
||||
|
||||
// Domain modules - core types and abstractions
|
||||
pub mod domain;
|
||||
pub mod events;
|
||||
pub mod modes;
|
||||
pub mod strategies;
|
||||
|
||||
// Core functionality modules
|
||||
pub mod core;
|
||||
pub mod orderbook;
|
||||
pub mod risk;
|
||||
pub mod positions;
|
||||
pub mod analytics;
|
||||
pub mod indicators;
|
||||
pub mod backtest;
|
||||
|
||||
// API layer
|
||||
pub mod api;
|
||||
|
||||
// Re-export all domain types
|
||||
pub use domain::*;
|
||||
|
||||
// Re-export event system
|
||||
pub use events::{Event, EventType, EventBus, EventHandler};
|
||||
|
||||
// Re-export mode types and engine trait
|
||||
pub use modes::{TradingMode, TradingEngine};
|
||||
|
||||
// Re-export strategy framework
|
||||
pub use strategies::framework::{Strategy, StrategyContext, Signal, SignalAction};
|
||||
|
||||
// Re-export position tracking
|
||||
pub use positions::{PositionTracker, TradeRecord, ClosedTrade};
|
||||
|
||||
// Re-export risk management
|
||||
pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics, RiskEngine};
|
||||
|
||||
// Re-export analytics
|
||||
pub use analytics::{AnalyticsEngine, PerformanceMetrics};
|
||||
|
||||
// Re-export indicators
|
||||
pub use indicators::{Indicator, IndicatorSet};
|
||||
|
||||
// Re-export backtest types
|
||||
pub use backtest::{BacktestEngine, BacktestResults};
|
||||
|
||||
// Re-export API
|
||||
pub use api::TradingAPI;
|
||||
|
||||
// Core system traits
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Source of market data - can be historical files, live feed, or simulated
|
||||
#[async_trait]
|
||||
pub trait MarketDataSource: Send + Sync {
|
||||
async fn get_next_update(&mut self) -> Option<MarketUpdate>;
|
||||
fn seek_to_time(&mut self, timestamp: DateTime<Utc>) -> Result<(), String>;
|
||||
}
|
||||
|
||||
/// Handles order execution - can be simulated, paper, or live broker
|
||||
#[async_trait]
|
||||
pub trait ExecutionHandler: Send + Sync {
|
||||
async fn submit_order(&self, order: &Order) -> Result<String, String>;
|
||||
async fn cancel_order(&self, order_id: &str) -> Result<(), String>;
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String>;
|
||||
}
|
||||
|
||||
/// Provides current time - can be simulated time for backtest or real time
|
||||
pub trait TimeProvider: Send + Sync {
|
||||
fn now(&self) -> DateTime<Utc>;
|
||||
fn sleep_until(&self, target: DateTime<Utc>) -> Result<(), String>;
|
||||
}
|
||||
|
||||
/// Simulates order fills for backtest/paper trading
|
||||
pub trait FillSimulator: Send + Sync {
|
||||
fn simulate_fill(&self, order: &Order, market_price: f64, spread: f64) -> Option<Fill>;
|
||||
}
|
||||
|
||||
/// Main trading system that coordinates all components
|
||||
pub struct TradingCore {
|
||||
mode: TradingMode,
|
||||
engine: Box<dyn TradingEngine>,
|
||||
event_bus: EventBus,
|
||||
orderbook_manager: OrderBookManager,
|
||||
position_tracker: PositionTracker,
|
||||
risk_engine: RiskEngine,
|
||||
analytics_engine: AnalyticsEngine,
|
||||
}
|
||||
|
||||
impl TradingCore {
|
||||
/// Create a new trading system for the specified mode
|
||||
pub fn new(mode: TradingMode) -> Result<Self, String> {
|
||||
let engine = modes::create_engine_for_mode(&mode)?;
|
||||
let event_bus = EventBus::new();
|
||||
|
||||
Ok(Self {
|
||||
mode,
|
||||
engine,
|
||||
event_bus,
|
||||
orderbook_manager: OrderBookManager::new(),
|
||||
position_tracker: PositionTracker::new(),
|
||||
risk_engine: RiskEngine::new(),
|
||||
analytics_engine: AnalyticsEngine::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Start the trading system
|
||||
pub async fn start(&mut self) -> Result<(), String> {
|
||||
self.engine.start(&mut self.event_bus).await
|
||||
}
|
||||
|
||||
/// Stop the trading system
|
||||
pub async fn stop(&mut self) -> Result<(), String> {
|
||||
self.engine.stop().await
|
||||
}
|
||||
|
||||
/// Get current trading mode
|
||||
pub fn mode(&self) -> &TradingMode {
|
||||
&self.mode
|
||||
}
|
||||
}
|
||||
291
apps/stock/engine/src/modes.bak/backtest.rs
Normal file
291
apps/stock/engine/src/modes.bak/backtest.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use crate::{
|
||||
MarketDataSource, ExecutionHandler, TimeProvider, FillSimulator,
|
||||
MarketUpdate, Order, OrderStatus, Fill, OrderBookSnapshot,
|
||||
};
|
||||
use crate::events::EventBus;
|
||||
use crate::domain::{Event, EventType};
|
||||
use super::TradingEngine;
|
||||
|
||||
/// Backtest-specific trading engine
|
||||
pub struct BacktestEngine {
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
current_time: DateTime<Utc>,
|
||||
speed_multiplier: f64,
|
||||
market_data: Box<dyn MarketDataSource>,
|
||||
execution: BacktestExecutor,
|
||||
is_running: Arc<RwLock<bool>>,
|
||||
}
|
||||
|
||||
impl BacktestEngine {
|
||||
pub fn new(
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
speed_multiplier: f64,
|
||||
market_data: Box<dyn MarketDataSource>,
|
||||
) -> Self {
|
||||
Self {
|
||||
start_time,
|
||||
end_time,
|
||||
current_time: start_time,
|
||||
speed_multiplier,
|
||||
market_data,
|
||||
execution: BacktestExecutor::new(),
|
||||
is_running: Arc::new(RwLock::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TradingEngine for BacktestEngine {
|
||||
async fn start(&mut self, event_bus: &mut EventBus) -> Result<(), String> {
|
||||
*self.is_running.write() = true;
|
||||
self.current_time = self.start_time;
|
||||
|
||||
// Seek market data to start time
|
||||
self.market_data.seek_to_time(self.start_time)?;
|
||||
|
||||
// Emit start event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::SystemStart,
|
||||
timestamp: self.current_time,
|
||||
data: serde_json::json!({
|
||||
"mode": "backtest",
|
||||
"start_time": self.start_time,
|
||||
"end_time": self.end_time,
|
||||
}),
|
||||
}).await;
|
||||
|
||||
// Main backtest loop
|
||||
while *self.is_running.read() && self.current_time < self.end_time {
|
||||
// Get next market update
|
||||
if let Some(update) = self.market_data.get_next_update().await {
|
||||
// Update current time
|
||||
self.current_time = update.timestamp;
|
||||
|
||||
// Publish market data event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::MarketData(update.clone()),
|
||||
timestamp: self.current_time,
|
||||
data: serde_json::Value::Null,
|
||||
}).await;
|
||||
|
||||
// Process any pending orders
|
||||
self.execution.process_orders(&update, event_bus).await?;
|
||||
} else {
|
||||
// No more data
|
||||
break;
|
||||
}
|
||||
|
||||
// Simulate time passing (for UI updates, etc.)
|
||||
if self.speed_multiplier > 0.0 {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(
|
||||
(10.0 / self.speed_multiplier) as u64
|
||||
)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit stop event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::SystemStop,
|
||||
timestamp: self.current_time,
|
||||
data: serde_json::json!({
|
||||
"reason": "backtest_complete",
|
||||
"end_time": self.current_time,
|
||||
}),
|
||||
}).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(&mut self) -> Result<(), String> {
|
||||
*self.is_running.write() = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_execution_handler(&self) -> Arc<dyn ExecutionHandler> {
|
||||
Arc::new(self.execution.clone())
|
||||
}
|
||||
|
||||
fn get_time_provider(&self) -> Arc<dyn TimeProvider> {
|
||||
Arc::new(BacktestTimeProvider {
|
||||
current_time: self.current_time,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Backtest order executor
|
||||
#[derive(Clone)]
|
||||
struct BacktestExecutor {
|
||||
pending_orders: Arc<RwLock<Vec<Order>>>,
|
||||
order_history: Arc<RwLock<Vec<(Order, OrderStatus)>>>,
|
||||
fill_simulator: BacktestFillSimulator,
|
||||
}
|
||||
|
||||
impl BacktestExecutor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
pending_orders: Arc::new(RwLock::new(Vec::new())),
|
||||
order_history: Arc::new(RwLock::new(Vec::new())),
|
||||
fill_simulator: BacktestFillSimulator::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_orders(
|
||||
&self,
|
||||
market_update: &MarketUpdate,
|
||||
event_bus: &mut EventBus,
|
||||
) -> Result<(), String> {
|
||||
let mut orders = self.pending_orders.write();
|
||||
let mut filled_indices = Vec::new();
|
||||
|
||||
for (idx, order) in orders.iter().enumerate() {
|
||||
if order.symbol != market_update.symbol {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to fill the order
|
||||
if let Some(fill) = self.fill_simulator.try_fill(order, market_update) {
|
||||
// Emit fill event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::OrderFill {
|
||||
order_id: order.id.clone(),
|
||||
fill: fill.clone(),
|
||||
},
|
||||
timestamp: market_update.timestamp,
|
||||
data: serde_json::Value::Null,
|
||||
}).await;
|
||||
|
||||
filled_indices.push(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove filled orders
|
||||
for idx in filled_indices.into_iter().rev() {
|
||||
let order = orders.remove(idx);
|
||||
self.order_history.write().push((order, OrderStatus::Filled));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionHandler for BacktestExecutor {
|
||||
async fn submit_order(&self, order: &Order) -> Result<String, String> {
|
||||
let order_id = order.id.clone();
|
||||
self.pending_orders.write().push(order.clone());
|
||||
Ok(order_id)
|
||||
}
|
||||
|
||||
async fn cancel_order(&self, order_id: &str) -> Result<(), String> {
|
||||
let mut orders = self.pending_orders.write();
|
||||
if let Some(pos) = orders.iter().position(|o| o.id == order_id) {
|
||||
let order = orders.remove(pos);
|
||||
self.order_history.write().push((order, OrderStatus::Cancelled));
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Order not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String> {
|
||||
// Check pending orders
|
||||
if self.pending_orders.read().iter().any(|o| o.id == order_id) {
|
||||
return Ok(OrderStatus::Pending);
|
||||
}
|
||||
|
||||
// Check history
|
||||
if let Some((_, status)) = self.order_history.read()
|
||||
.iter()
|
||||
.find(|(o, _)| o.id == order_id) {
|
||||
return Ok(status.clone());
|
||||
}
|
||||
|
||||
Err("Order not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Backtest time provider
|
||||
struct BacktestTimeProvider {
|
||||
current_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl TimeProvider for BacktestTimeProvider {
|
||||
fn now(&self) -> DateTime<Utc> {
|
||||
self.current_time
|
||||
}
|
||||
|
||||
fn sleep_until(&self, _target: DateTime<Utc>) -> Result<(), String> {
|
||||
// In backtest, we don't actually sleep
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Backtest fill simulator
|
||||
#[derive(Clone)]
|
||||
struct BacktestFillSimulator {
|
||||
commission_rate: f64,
|
||||
slippage_bps: f64,
|
||||
}
|
||||
|
||||
impl BacktestFillSimulator {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
commission_rate: 0.001, // 0.1%
|
||||
slippage_bps: 5.0, // 5 basis points
|
||||
}
|
||||
}
|
||||
|
||||
fn try_fill(&self, order: &Order, market_update: &MarketUpdate) -> Option<Fill> {
|
||||
match &market_update.data {
|
||||
crate::MarketDataType::Quote(quote) => {
|
||||
self.simulate_fill(order, quote.bid, quote.ask - quote.bid)
|
||||
}
|
||||
crate::MarketDataType::Trade(trade) => {
|
||||
self.simulate_fill(order, trade.price, 0.0001 * trade.price) // 1bp spread estimate
|
||||
}
|
||||
crate::MarketDataType::Bar(bar) => {
|
||||
// Use close price with estimated spread
|
||||
self.simulate_fill(order, bar.close, 0.0002 * bar.close) // 2bp spread estimate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FillSimulator for BacktestFillSimulator {
|
||||
fn simulate_fill(&self, order: &Order, market_price: f64, spread: f64) -> Option<Fill> {
|
||||
let fill_price = match order.order_type {
|
||||
crate::OrderType::Market => {
|
||||
// Fill at market with slippage
|
||||
let slippage = market_price * self.slippage_bps / 10000.0;
|
||||
match order.side {
|
||||
crate::Side::Buy => market_price + spread/2.0 + slippage,
|
||||
crate::Side::Sell => market_price - spread/2.0 - slippage,
|
||||
}
|
||||
}
|
||||
crate::OrderType::Limit { price } => {
|
||||
// Check if limit price is satisfied
|
||||
match order.side {
|
||||
crate::Side::Buy if price >= market_price + spread/2.0 => price,
|
||||
crate::Side::Sell if price <= market_price - spread/2.0 => price,
|
||||
_ => return None, // Limit not satisfied
|
||||
}
|
||||
}
|
||||
_ => return None, // Other order types not implemented yet
|
||||
};
|
||||
|
||||
let commission = fill_price * order.quantity * self.commission_rate;
|
||||
|
||||
Some(Fill {
|
||||
timestamp: chrono::Utc::now(), // Will be overridden by engine
|
||||
price: fill_price,
|
||||
quantity: order.quantity,
|
||||
commission,
|
||||
})
|
||||
}
|
||||
}
|
||||
290
apps/stock/engine/src/modes.bak/live.rs
Normal file
290
apps/stock/engine/src/modes.bak/live.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use tokio::sync::mpsc;
|
||||
use crate::{
|
||||
ExecutionHandler, TimeProvider,
|
||||
Order, OrderStatus, Fill,
|
||||
};
|
||||
use crate::events::EventBus;
|
||||
use crate::domain::{Event, EventType};
|
||||
use super::TradingEngine;
|
||||
|
||||
/// Live trading engine - connects to real brokers
|
||||
pub struct LiveEngine {
|
||||
broker: String,
|
||||
account_id: String,
|
||||
broker_connection: Arc<dyn BrokerConnection>,
|
||||
is_running: Arc<RwLock<bool>>,
|
||||
shutdown_tx: Option<mpsc::Sender<()>>,
|
||||
}
|
||||
|
||||
/// Trait for broker connections
|
||||
#[async_trait]
|
||||
pub trait BrokerConnection: Send + Sync {
|
||||
async fn connect(&mut self, account_id: &str) -> Result<(), String>;
|
||||
async fn disconnect(&mut self) -> Result<(), String>;
|
||||
async fn subscribe_market_data(&mut self, symbols: Vec<String>) -> Result<(), String>;
|
||||
async fn submit_order(&self, order: &Order) -> Result<String, String>;
|
||||
async fn cancel_order(&self, order_id: &str) -> Result<(), String>;
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String>;
|
||||
async fn get_positions(&self) -> Result<Vec<crate::Position>, String>;
|
||||
async fn get_account_info(&self) -> Result<AccountInfo, String>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccountInfo {
|
||||
pub cash: f64,
|
||||
pub buying_power: f64,
|
||||
pub portfolio_value: f64,
|
||||
pub day_trades_remaining: Option<u32>,
|
||||
}
|
||||
|
||||
impl LiveEngine {
|
||||
pub fn new(
|
||||
broker: String,
|
||||
account_id: String,
|
||||
broker_connection: Arc<dyn BrokerConnection>,
|
||||
) -> Self {
|
||||
Self {
|
||||
broker,
|
||||
account_id,
|
||||
broker_connection,
|
||||
is_running: Arc::new(RwLock::new(false)),
|
||||
shutdown_tx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TradingEngine for LiveEngine {
|
||||
async fn start(&mut self, event_bus: &mut EventBus) -> Result<(), String> {
|
||||
*self.is_running.write() = true;
|
||||
|
||||
// Connect to broker
|
||||
self.broker_connection.connect(&self.account_id).await?;
|
||||
|
||||
// Get initial account info
|
||||
let account_info = self.broker_connection.get_account_info().await?;
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1);
|
||||
self.shutdown_tx = Some(shutdown_tx);
|
||||
|
||||
// Emit start event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::SystemStart,
|
||||
timestamp: Utc::now(),
|
||||
data: serde_json::json!({
|
||||
"mode": "live",
|
||||
"broker": self.broker,
|
||||
"account_id": self.account_id,
|
||||
"account_info": {
|
||||
"cash": account_info.cash,
|
||||
"buying_power": account_info.buying_power,
|
||||
"portfolio_value": account_info.portfolio_value,
|
||||
},
|
||||
}),
|
||||
}).await;
|
||||
|
||||
// Main live trading loop
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
|
||||
// Periodic tasks (e.g., check positions, risk)
|
||||
if !*self.is_running.read() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Could emit periodic status updates here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from broker
|
||||
self.broker_connection.disconnect().await?;
|
||||
|
||||
// Emit stop event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::SystemStop,
|
||||
timestamp: Utc::now(),
|
||||
data: serde_json::json!({
|
||||
"reason": "live_trading_stopped",
|
||||
}),
|
||||
}).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(&mut self) -> Result<(), String> {
|
||||
*self.is_running.write() = false;
|
||||
if let Some(tx) = self.shutdown_tx.take() {
|
||||
let _ = tx.send(()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_execution_handler(&self) -> Arc<dyn ExecutionHandler> {
|
||||
Arc::new(LiveExecutor {
|
||||
broker_connection: self.broker_connection.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_time_provider(&self) -> Arc<dyn TimeProvider> {
|
||||
Arc::new(RealTimeProvider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Live order executor - delegates to broker
|
||||
#[derive(Clone)]
|
||||
struct LiveExecutor {
|
||||
broker_connection: Arc<dyn BrokerConnection>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionHandler for LiveExecutor {
|
||||
async fn submit_order(&self, order: &Order) -> Result<String, String> {
|
||||
// Validate order
|
||||
if order.quantity <= 0.0 {
|
||||
return Err("Invalid order quantity".to_string());
|
||||
}
|
||||
|
||||
// Submit to broker
|
||||
self.broker_connection.submit_order(order).await
|
||||
}
|
||||
|
||||
async fn cancel_order(&self, order_id: &str) -> Result<(), String> {
|
||||
self.broker_connection.cancel_order(order_id).await
|
||||
}
|
||||
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String> {
|
||||
self.broker_connection.get_order_status(order_id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Real-time provider for live trading
|
||||
struct RealTimeProvider;
|
||||
|
||||
impl TimeProvider for RealTimeProvider {
|
||||
fn now(&self) -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
}
|
||||
|
||||
fn sleep_until(&self, target: DateTime<Utc>) -> Result<(), String> {
|
||||
let now = Utc::now();
|
||||
if target > now {
|
||||
let duration = target.signed_duration_since(now);
|
||||
std::thread::sleep(duration.to_std().unwrap_or_default());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Example broker implementations would go here
|
||||
// For now, we'll create a mock broker for testing
|
||||
|
||||
/// Mock broker for testing
|
||||
pub struct MockBroker {
|
||||
is_connected: bool,
|
||||
orders: Arc<RwLock<Vec<(Order, OrderStatus)>>>,
|
||||
}
|
||||
|
||||
impl MockBroker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
is_connected: false,
|
||||
orders: Arc::new(RwLock::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BrokerConnection for MockBroker {
|
||||
async fn connect(&mut self, _account_id: &str) -> Result<(), String> {
|
||||
self.is_connected = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disconnect(&mut self) -> Result<(), String> {
|
||||
self.is_connected = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_market_data(&mut self, _symbols: Vec<String>) -> Result<(), String> {
|
||||
if !self.is_connected {
|
||||
return Err("Not connected".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn submit_order(&self, order: &Order) -> Result<String, String> {
|
||||
if !self.is_connected {
|
||||
return Err("Not connected".to_string());
|
||||
}
|
||||
|
||||
let order_id = order.id.clone();
|
||||
self.orders.write().push((order.clone(), OrderStatus::Pending));
|
||||
|
||||
// Simulate order being filled after a delay
|
||||
let orders = self.orders.clone();
|
||||
let order_id_clone = order_id.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
let mut orders = orders.write();
|
||||
if let Some(pos) = orders.iter().position(|(o, _)| o.id == order_id_clone) {
|
||||
orders[pos].1 = OrderStatus::Filled;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(order_id)
|
||||
}
|
||||
|
||||
async fn cancel_order(&self, order_id: &str) -> Result<(), String> {
|
||||
if !self.is_connected {
|
||||
return Err("Not connected".to_string());
|
||||
}
|
||||
|
||||
let mut orders = self.orders.write();
|
||||
if let Some(pos) = orders.iter().position(|(o, _)| o.id == order_id) {
|
||||
orders[pos].1 = OrderStatus::Cancelled;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Order not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String> {
|
||||
if !self.is_connected {
|
||||
return Err("Not connected".to_string());
|
||||
}
|
||||
|
||||
let orders = self.orders.read();
|
||||
if let Some((_, status)) = orders.iter().find(|(o, _)| o.id == order_id) {
|
||||
Ok(status.clone())
|
||||
} else {
|
||||
Err("Order not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_positions(&self) -> Result<Vec<crate::Position>, String> {
|
||||
if !self.is_connected {
|
||||
return Err("Not connected".to_string());
|
||||
}
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn get_account_info(&self) -> Result<AccountInfo, String> {
|
||||
if !self.is_connected {
|
||||
return Err("Not connected".to_string());
|
||||
}
|
||||
Ok(AccountInfo {
|
||||
cash: 100_000.0,
|
||||
buying_power: 400_000.0,
|
||||
portfolio_value: 100_000.0,
|
||||
day_trades_remaining: Some(3),
|
||||
})
|
||||
}
|
||||
}
|
||||
106
apps/stock/engine/src/modes.bak/mod.rs
Normal file
106
apps/stock/engine/src/modes.bak/mod.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
use async_trait::async_trait;
|
||||
use crate::events::EventBus;
|
||||
use crate::{ExecutionHandler, TimeProvider, MarketDataSource};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod backtest;
|
||||
pub mod paper;
|
||||
pub mod live;
|
||||
|
||||
pub use backtest::BacktestEngine;
|
||||
pub use paper::PaperEngine;
|
||||
pub use live::{LiveEngine, BrokerConnection};
|
||||
|
||||
/// Trading mode configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TradingMode {
|
||||
Backtest {
|
||||
start_time: chrono::DateTime<chrono::Utc>,
|
||||
end_time: chrono::DateTime<chrono::Utc>,
|
||||
speed_multiplier: f64,
|
||||
},
|
||||
Paper {
|
||||
starting_capital: f64,
|
||||
},
|
||||
Live {
|
||||
broker: String,
|
||||
account_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Common interface for all trading engines
|
||||
#[async_trait]
|
||||
pub trait TradingEngine: Send + Sync {
|
||||
/// Start the trading engine
|
||||
async fn start(&mut self, event_bus: &mut EventBus) -> Result<(), String>;
|
||||
|
||||
/// Stop the trading engine
|
||||
async fn stop(&mut self) -> Result<(), String>;
|
||||
|
||||
/// Get the execution handler for this mode
|
||||
fn get_execution_handler(&self) -> Arc<dyn ExecutionHandler>;
|
||||
|
||||
/// Get the time provider for this mode
|
||||
fn get_time_provider(&self) -> Arc<dyn TimeProvider>;
|
||||
}
|
||||
|
||||
/// Create a trading engine for the specified mode
|
||||
pub fn create_engine_for_mode(
|
||||
mode: &TradingMode,
|
||||
) -> Result<Box<dyn TradingEngine>, String> {
|
||||
match mode {
|
||||
TradingMode::Backtest { start_time, end_time, speed_multiplier } => {
|
||||
// For backtest, we need to create a market data source
|
||||
// This would typically load historical data
|
||||
let market_data = create_backtest_data_source()?;
|
||||
|
||||
Ok(Box::new(BacktestEngine::new(
|
||||
*start_time,
|
||||
*end_time,
|
||||
*speed_multiplier,
|
||||
market_data,
|
||||
)))
|
||||
}
|
||||
TradingMode::Paper { starting_capital } => {
|
||||
// For paper trading, we need a real-time data source
|
||||
let market_data = create_realtime_data_source()?;
|
||||
|
||||
Ok(Box::new(PaperEngine::new(
|
||||
*starting_capital,
|
||||
market_data,
|
||||
)))
|
||||
}
|
||||
TradingMode::Live { broker, account_id } => {
|
||||
// For live trading, we need a broker connection
|
||||
let broker_connection = create_broker_connection(broker)?;
|
||||
|
||||
Ok(Box::new(LiveEngine::new(
|
||||
broker.clone(),
|
||||
account_id.clone(),
|
||||
broker_connection,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to create data sources and broker connections
|
||||
// These would be implemented based on your specific requirements
|
||||
|
||||
fn create_backtest_data_source() -> Result<Box<dyn MarketDataSource>, String> {
|
||||
// TODO: Implement actual backtest data source
|
||||
// For now, return a placeholder
|
||||
Err("Backtest data source not implemented yet".to_string())
|
||||
}
|
||||
|
||||
fn create_realtime_data_source() -> Result<Box<dyn MarketDataSource>, String> {
|
||||
// TODO: Implement actual real-time data source
|
||||
// For now, return a placeholder
|
||||
Err("Real-time data source not implemented yet".to_string())
|
||||
}
|
||||
|
||||
fn create_broker_connection(broker: &str) -> Result<Arc<dyn BrokerConnection>, String> {
|
||||
match broker {
|
||||
"mock" => Ok(Arc::new(live::MockBroker::new())),
|
||||
_ => Err(format!("Unknown broker: {}", broker)),
|
||||
}
|
||||
}
|
||||
306
apps/stock/engine/src/modes.bak/paper.rs
Normal file
306
apps/stock/engine/src/modes.bak/paper.rs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
use tokio::sync::mpsc;
|
||||
use crate::{
|
||||
MarketDataSource, ExecutionHandler, TimeProvider, FillSimulator,
|
||||
MarketUpdate, Order, OrderStatus, Fill,
|
||||
};
|
||||
use crate::events::EventBus;
|
||||
use crate::domain::{Event, EventType};
|
||||
use super::TradingEngine;
|
||||
|
||||
/// Paper trading engine - simulates live trading without real money
|
||||
pub struct PaperEngine {
|
||||
starting_capital: f64,
|
||||
market_data: Box<dyn MarketDataSource>,
|
||||
execution: PaperExecutor,
|
||||
is_running: Arc<RwLock<bool>>,
|
||||
shutdown_tx: Option<mpsc::Sender<()>>,
|
||||
}
|
||||
|
||||
impl PaperEngine {
|
||||
pub fn new(
|
||||
starting_capital: f64,
|
||||
market_data: Box<dyn MarketDataSource>,
|
||||
) -> Self {
|
||||
Self {
|
||||
starting_capital,
|
||||
market_data,
|
||||
execution: PaperExecutor::new(starting_capital),
|
||||
is_running: Arc::new(RwLock::new(false)),
|
||||
shutdown_tx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TradingEngine for PaperEngine {
|
||||
async fn start(&mut self, event_bus: &mut EventBus) -> Result<(), String> {
|
||||
*self.is_running.write() = true;
|
||||
|
||||
let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1);
|
||||
self.shutdown_tx = Some(shutdown_tx);
|
||||
|
||||
// Emit start event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::SystemStart,
|
||||
timestamp: Utc::now(),
|
||||
data: serde_json::json!({
|
||||
"mode": "paper",
|
||||
"starting_capital": self.starting_capital,
|
||||
}),
|
||||
}).await;
|
||||
|
||||
// Main paper trading loop
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
update = self.market_data.get_next_update() => {
|
||||
if let Some(market_update) = update {
|
||||
// Publish market data event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::MarketData(market_update.clone()),
|
||||
timestamp: Utc::now(),
|
||||
data: serde_json::Value::Null,
|
||||
}).await;
|
||||
|
||||
// Process pending orders
|
||||
self.execution.process_orders(&market_update, event_bus).await?;
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {
|
||||
// Check if still running
|
||||
if !*self.is_running.read() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit stop event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::SystemStop,
|
||||
timestamp: Utc::now(),
|
||||
data: serde_json::json!({
|
||||
"reason": "paper_trading_stopped",
|
||||
}),
|
||||
}).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(&mut self) -> Result<(), String> {
|
||||
*self.is_running.write() = false;
|
||||
if let Some(tx) = self.shutdown_tx.take() {
|
||||
let _ = tx.send(()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_execution_handler(&self) -> Arc<dyn ExecutionHandler> {
|
||||
Arc::new(self.execution.clone())
|
||||
}
|
||||
|
||||
fn get_time_provider(&self) -> Arc<dyn TimeProvider> {
|
||||
Arc::new(RealTimeProvider)
|
||||
}
|
||||
}
|
||||
|
||||
/// Paper trading order executor
|
||||
#[derive(Clone)]
|
||||
struct PaperExecutor {
|
||||
cash: Arc<RwLock<f64>>,
|
||||
pending_orders: Arc<RwLock<Vec<Order>>>,
|
||||
order_history: Arc<RwLock<Vec<(Order, OrderStatus)>>>,
|
||||
fill_simulator: PaperFillSimulator,
|
||||
}
|
||||
|
||||
impl PaperExecutor {
|
||||
fn new(starting_capital: f64) -> Self {
|
||||
Self {
|
||||
cash: Arc::new(RwLock::new(starting_capital)),
|
||||
pending_orders: Arc::new(RwLock::new(Vec::new())),
|
||||
order_history: Arc::new(RwLock::new(Vec::new())),
|
||||
fill_simulator: PaperFillSimulator::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_orders(
|
||||
&self,
|
||||
market_update: &MarketUpdate,
|
||||
event_bus: &mut EventBus,
|
||||
) -> Result<(), String> {
|
||||
let mut orders = self.pending_orders.write();
|
||||
let mut filled_indices = Vec::new();
|
||||
|
||||
for (idx, order) in orders.iter().enumerate() {
|
||||
if order.symbol != market_update.symbol {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to fill the order
|
||||
if let Some(fill) = self.fill_simulator.try_fill(order, market_update) {
|
||||
// Check if we have enough cash for buy orders
|
||||
if order.side == crate::Side::Buy {
|
||||
let required_cash = fill.price * fill.quantity + fill.commission;
|
||||
let mut cash = self.cash.write();
|
||||
if *cash >= required_cash {
|
||||
*cash -= required_cash;
|
||||
} else {
|
||||
continue; // Skip this order
|
||||
}
|
||||
} else {
|
||||
// For sell orders, add cash
|
||||
let proceeds = fill.price * fill.quantity - fill.commission;
|
||||
*self.cash.write() += proceeds;
|
||||
}
|
||||
|
||||
// Emit fill event
|
||||
event_bus.publish(Event {
|
||||
event_type: EventType::OrderFill {
|
||||
order_id: order.id.clone(),
|
||||
fill: fill.clone(),
|
||||
},
|
||||
timestamp: Utc::now(),
|
||||
data: serde_json::json!({
|
||||
"cash_after": *self.cash.read(),
|
||||
}),
|
||||
}).await;
|
||||
|
||||
filled_indices.push(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove filled orders
|
||||
for idx in filled_indices.into_iter().rev() {
|
||||
let order = orders.remove(idx);
|
||||
self.order_history.write().push((order, OrderStatus::Filled));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionHandler for PaperExecutor {
|
||||
async fn submit_order(&self, order: &Order) -> Result<String, String> {
|
||||
// Basic validation
|
||||
if order.quantity <= 0.0 {
|
||||
return Err("Invalid order quantity".to_string());
|
||||
}
|
||||
|
||||
let order_id = order.id.clone();
|
||||
self.pending_orders.write().push(order.clone());
|
||||
Ok(order_id)
|
||||
}
|
||||
|
||||
async fn cancel_order(&self, order_id: &str) -> Result<(), String> {
|
||||
let mut orders = self.pending_orders.write();
|
||||
if let Some(pos) = orders.iter().position(|o| o.id == order_id) {
|
||||
let order = orders.remove(pos);
|
||||
self.order_history.write().push((order, OrderStatus::Cancelled));
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Order not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_order_status(&self, order_id: &str) -> Result<OrderStatus, String> {
|
||||
// Check pending orders
|
||||
if self.pending_orders.read().iter().any(|o| o.id == order_id) {
|
||||
return Ok(OrderStatus::Pending);
|
||||
}
|
||||
|
||||
// Check history
|
||||
if let Some((_, status)) = self.order_history.read()
|
||||
.iter()
|
||||
.find(|(o, _)| o.id == order_id) {
|
||||
return Ok(status.clone());
|
||||
}
|
||||
|
||||
Err("Order not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Real-time provider for paper trading
|
||||
struct RealTimeProvider;
|
||||
|
||||
impl TimeProvider for RealTimeProvider {
|
||||
fn now(&self) -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
}
|
||||
|
||||
fn sleep_until(&self, target: DateTime<Utc>) -> Result<(), String> {
|
||||
let now = Utc::now();
|
||||
if target > now {
|
||||
let duration = target.signed_duration_since(now);
|
||||
std::thread::sleep(duration.to_std().unwrap_or_default());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Paper trading fill simulator
|
||||
#[derive(Clone)]
|
||||
struct PaperFillSimulator {
|
||||
commission_rate: f64,
|
||||
slippage_bps: f64,
|
||||
}
|
||||
|
||||
impl PaperFillSimulator {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
commission_rate: 0.001, // 0.1%
|
||||
slippage_bps: 3.0, // 3 basis points (less than backtest)
|
||||
}
|
||||
}
|
||||
|
||||
fn try_fill(&self, order: &Order, market_update: &MarketUpdate) -> Option<Fill> {
|
||||
match &market_update.data {
|
||||
crate::MarketDataType::Quote(quote) => {
|
||||
self.simulate_fill(order, quote.bid, quote.ask - quote.bid)
|
||||
}
|
||||
crate::MarketDataType::Trade(trade) => {
|
||||
self.simulate_fill(order, trade.price, 0.0001 * trade.price)
|
||||
}
|
||||
crate::MarketDataType::Bar(bar) => {
|
||||
self.simulate_fill(order, bar.close, 0.0002 * bar.close)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FillSimulator for PaperFillSimulator {
|
||||
fn simulate_fill(&self, order: &Order, market_price: f64, spread: f64) -> Option<Fill> {
|
||||
let fill_price = match order.order_type {
|
||||
crate::OrderType::Market => {
|
||||
let slippage = market_price * self.slippage_bps / 10000.0;
|
||||
match order.side {
|
||||
crate::Side::Buy => market_price + spread/2.0 + slippage,
|
||||
crate::Side::Sell => market_price - spread/2.0 - slippage,
|
||||
}
|
||||
}
|
||||
crate::OrderType::Limit { price } => {
|
||||
match order.side {
|
||||
crate::Side::Buy if price >= market_price + spread/2.0 => price,
|
||||
crate::Side::Sell if price <= market_price - spread/2.0 => price,
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let commission = fill_price * order.quantity * self.commission_rate;
|
||||
|
||||
Some(Fill {
|
||||
timestamp: Utc::now(),
|
||||
price: fill_price,
|
||||
quantity: order.quantity,
|
||||
commission,
|
||||
})
|
||||
}
|
||||
}
|
||||
114
apps/stock/engine/src/strategies/framework/mod.rs
Normal file
114
apps/stock/engine/src/strategies/framework/mod.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use crate::domain::{MarketUpdate, Fill, Order, Side};
|
||||
use crate::indicators::IndicatorSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Context provided to strategies
|
||||
pub struct StrategyContext {
|
||||
pub portfolio_value: f64,
|
||||
pub cash: f64,
|
||||
pub positions: HashMap<String, f64>,
|
||||
pub pending_orders: Vec<Order>,
|
||||
pub indicators: IndicatorSet,
|
||||
}
|
||||
|
||||
/// Signal generated by a strategy
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Signal {
|
||||
pub symbol: String,
|
||||
pub action: SignalAction,
|
||||
pub quantity: Option<f64>,
|
||||
pub confidence: f64, // 0.0 to 1.0
|
||||
pub reason: String,
|
||||
pub metadata: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SignalAction {
|
||||
Buy,
|
||||
Sell,
|
||||
Close,
|
||||
Hold,
|
||||
}
|
||||
|
||||
/// Core strategy trait with lifecycle methods
|
||||
#[async_trait]
|
||||
pub trait Strategy: Send + Sync {
|
||||
/// Called once when strategy is initialized
|
||||
async fn init(&mut self, context: &StrategyContext) -> Result<(), String>;
|
||||
|
||||
/// Called when trading starts
|
||||
async fn on_start(&mut self) -> Result<(), String>;
|
||||
|
||||
/// Called for each market data update
|
||||
async fn on_data(&mut self, data: &MarketUpdate, context: &StrategyContext) -> Vec<Signal>;
|
||||
|
||||
/// Called when an order is filled
|
||||
async fn on_fill(&mut self, order_id: &str, fill: &Fill, context: &StrategyContext);
|
||||
|
||||
/// Called periodically (e.g., every minute)
|
||||
async fn on_timer(&mut self, context: &StrategyContext) -> Vec<Signal> {
|
||||
Vec::new() // Default: no signals on timer
|
||||
}
|
||||
|
||||
/// Called when trading stops
|
||||
async fn on_stop(&mut self) -> Result<(), String>;
|
||||
|
||||
/// Get strategy name
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Get strategy parameters
|
||||
fn parameters(&self) -> Value;
|
||||
|
||||
/// Update strategy parameters
|
||||
fn update_parameters(&mut self, params: Value) -> Result<(), String>;
|
||||
}
|
||||
|
||||
/// Base implementation that strategies can extend
|
||||
pub struct BaseStrategy {
|
||||
name: String,
|
||||
parameters: Value,
|
||||
}
|
||||
|
||||
impl BaseStrategy {
|
||||
pub fn new(name: String, parameters: Value) -> Self {
|
||||
Self { name, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Strategy for BaseStrategy {
|
||||
async fn init(&mut self, _context: &StrategyContext) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_start(&mut self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_data(&mut self, _data: &MarketUpdate, _context: &StrategyContext) -> Vec<Signal> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
async fn on_fill(&mut self, _order_id: &str, _fill: &Fill, _context: &StrategyContext) {
|
||||
// Default: no action
|
||||
}
|
||||
|
||||
async fn on_stop(&mut self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
self.parameters.clone()
|
||||
}
|
||||
|
||||
fn update_parameters(&mut self, params: Value) -> Result<(), String> {
|
||||
self.parameters = params;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
143
apps/stock/engine/tests/basic_tests.rs
Normal file
143
apps/stock/engine/tests/basic_tests.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
use engine::{Order, OrderType, Side, TimeInForce, Quote, Bar, Trade, MarketDataType};
|
||||
use chrono::Utc;
|
||||
|
||||
#[test]
|
||||
fn test_order_creation() {
|
||||
let order = Order {
|
||||
id: "test-order-1".to_string(),
|
||||
symbol: "AAPL".to_string(),
|
||||
side: Side::Buy,
|
||||
quantity: 100.0,
|
||||
order_type: OrderType::Market,
|
||||
time_in_force: TimeInForce::Day,
|
||||
};
|
||||
|
||||
assert_eq!(order.id, "test-order-1");
|
||||
assert_eq!(order.symbol, "AAPL");
|
||||
assert_eq!(order.side, Side::Buy);
|
||||
assert_eq!(order.quantity, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_order_types() {
|
||||
let market = OrderType::Market;
|
||||
let limit = OrderType::Limit { price: 150.0 };
|
||||
let stop = OrderType::Stop { stop_price: 145.0 };
|
||||
let stop_limit = OrderType::StopLimit {
|
||||
stop_price: 145.0,
|
||||
limit_price: 144.5,
|
||||
};
|
||||
|
||||
match limit {
|
||||
OrderType::Limit { price } => assert_eq!(price, 150.0),
|
||||
_ => panic!("Expected limit order"),
|
||||
}
|
||||
|
||||
match stop {
|
||||
OrderType::Stop { stop_price } => assert_eq!(stop_price, 145.0),
|
||||
_ => panic!("Expected stop order"),
|
||||
}
|
||||
|
||||
match stop_limit {
|
||||
OrderType::StopLimit { stop_price, limit_price } => {
|
||||
assert_eq!(stop_price, 145.0);
|
||||
assert_eq!(limit_price, 144.5);
|
||||
}
|
||||
_ => panic!("Expected stop limit order"),
|
||||
}
|
||||
|
||||
assert!(matches!(market, OrderType::Market));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quote_creation() {
|
||||
let quote = Quote {
|
||||
bid: 149.95,
|
||||
ask: 150.05,
|
||||
bid_size: 1000.0,
|
||||
ask_size: 800.0,
|
||||
};
|
||||
|
||||
assert_eq!(quote.bid, 149.95);
|
||||
assert_eq!(quote.ask, 150.05);
|
||||
assert_eq!(quote.bid_size, 1000.0);
|
||||
assert_eq!(quote.ask_size, 800.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_creation() {
|
||||
let bar = Bar {
|
||||
open: 150.0,
|
||||
high: 152.0,
|
||||
low: 149.0,
|
||||
close: 151.0,
|
||||
volume: 1_000_000.0,
|
||||
vwap: Some(150.5),
|
||||
};
|
||||
|
||||
assert_eq!(bar.open, 150.0);
|
||||
assert_eq!(bar.high, 152.0);
|
||||
assert_eq!(bar.low, 149.0);
|
||||
assert_eq!(bar.close, 151.0);
|
||||
assert_eq!(bar.volume, 1_000_000.0);
|
||||
assert_eq!(bar.vwap, Some(150.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trade_creation() {
|
||||
let trade = Trade {
|
||||
price: 150.0,
|
||||
size: 500.0,
|
||||
side: Side::Buy,
|
||||
};
|
||||
|
||||
assert_eq!(trade.price, 150.0);
|
||||
assert_eq!(trade.size, 500.0);
|
||||
assert_eq!(trade.side, Side::Buy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_market_data_type() {
|
||||
let quote = Quote {
|
||||
bid: 149.95,
|
||||
ask: 150.05,
|
||||
bid_size: 100.0,
|
||||
ask_size: 100.0,
|
||||
};
|
||||
|
||||
let quote_data = MarketDataType::Quote(quote.clone());
|
||||
|
||||
match quote_data {
|
||||
MarketDataType::Quote(q) => {
|
||||
assert_eq!(q.bid, quote.bid);
|
||||
assert_eq!(q.ask, quote.ask);
|
||||
}
|
||||
_ => panic!("Expected quote data"),
|
||||
}
|
||||
|
||||
let bar = Bar {
|
||||
open: 150.0,
|
||||
high: 152.0,
|
||||
low: 149.0,
|
||||
close: 151.0,
|
||||
volume: 10000.0,
|
||||
vwap: None,
|
||||
};
|
||||
|
||||
let bar_data = MarketDataType::Bar(bar.clone());
|
||||
|
||||
match bar_data {
|
||||
MarketDataType::Bar(b) => {
|
||||
assert_eq!(b.open, bar.open);
|
||||
assert_eq!(b.close, bar.close);
|
||||
}
|
||||
_ => panic!("Expected bar data"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_side_equality() {
|
||||
assert_eq!(Side::Buy, Side::Buy);
|
||||
assert_eq!(Side::Sell, Side::Sell);
|
||||
assert_ne!(Side::Buy, Side::Sell);
|
||||
}
|
||||
52
apps/stock/engine/tests/integration_test.rs
Normal file
52
apps/stock/engine/tests/integration_test.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Simple integration test that verifies basic functionality
|
||||
|
||||
use engine::{Order, OrderType, Side, TimeInForce, TradingMode};
|
||||
use chrono::Utc;
|
||||
|
||||
#[test]
|
||||
fn test_trading_mode_creation() {
|
||||
let backtest_mode = TradingMode::Backtest {
|
||||
start_time: Utc::now(),
|
||||
end_time: Utc::now(),
|
||||
speed_multiplier: 1.0,
|
||||
};
|
||||
|
||||
match backtest_mode {
|
||||
TradingMode::Backtest { speed_multiplier, .. } => {
|
||||
assert_eq!(speed_multiplier, 1.0);
|
||||
}
|
||||
_ => panic!("Expected backtest mode"),
|
||||
}
|
||||
|
||||
let paper_mode = TradingMode::Paper {
|
||||
starting_capital: 100_000.0,
|
||||
};
|
||||
|
||||
match paper_mode {
|
||||
TradingMode::Paper { starting_capital } => {
|
||||
assert_eq!(starting_capital, 100_000.0);
|
||||
}
|
||||
_ => panic!("Expected paper mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_order_with_limit_price() {
|
||||
let order = Order {
|
||||
id: "limit-order-1".to_string(),
|
||||
symbol: "AAPL".to_string(),
|
||||
side: Side::Buy,
|
||||
quantity: 100.0,
|
||||
order_type: OrderType::Limit { price: 150.0 },
|
||||
time_in_force: TimeInForce::GTC,
|
||||
};
|
||||
|
||||
assert_eq!(order.symbol, "AAPL");
|
||||
assert_eq!(order.side, Side::Buy);
|
||||
assert_eq!(order.quantity, 100.0);
|
||||
|
||||
match order.order_type {
|
||||
OrderType::Limit { price } => assert_eq!(price, 150.0),
|
||||
_ => panic!("Expected limit order"),
|
||||
}
|
||||
}
|
||||
7
apps/stock/engine/tests/lib.rs
Normal file
7
apps/stock/engine/tests/lib.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Main test file for the engine crate
|
||||
|
||||
#[cfg(test)]
|
||||
mod basic_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration_test;
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { StorageService } from './src/services/StorageService';
|
||||
import { ModeManager } from './src/core/ModeManager';
|
||||
import { MarketDataService } from './src/services/MarketDataService';
|
||||
import { ExecutionService } from './src/services/ExecutionService';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
|
||||
async function debugPerformanceValues() {
|
||||
console.log('Debugging Performance Calculation Values...\n');
|
||||
|
||||
// Create minimal service container with more logging
|
||||
const container: IServiceContainer = {
|
||||
logger: {
|
||||
info: (msg: string, ...args: any[]) => {
|
||||
// Log everything related to P&L and portfolio
|
||||
if (msg.includes('P&L') || msg.includes('portfolio') || msg.includes('Portfolio') ||
|
||||
msg.includes('equity') || msg.includes('Total') || msg.includes('pnl')) {
|
||||
console.log('[INFO]', msg, ...args);
|
||||
}
|
||||
},
|
||||
error: (msg: string, ...args: any[]) => console.error('[ERROR]', msg, ...args),
|
||||
warn: (msg: string, ...args: any[]) => console.warn('[WARN]', msg, ...args),
|
||||
debug: (msg: string, ...args: any[]) => {
|
||||
if (msg.includes('P&L') || msg.includes('portfolio') || msg.includes('pnl')) {
|
||||
console.log('[DEBUG]', msg, ...args);
|
||||
}
|
||||
},
|
||||
} as any,
|
||||
custom: {}
|
||||
};
|
||||
|
||||
// Initialize services
|
||||
const storageService = new StorageService();
|
||||
const marketDataService = new MarketDataService(container);
|
||||
const executionService = new ExecutionService(container);
|
||||
const modeManager = new ModeManager(container, marketDataService, executionService, storageService);
|
||||
const strategyManager = new StrategyManager(container);
|
||||
|
||||
// Set services in container
|
||||
container.custom = {
|
||||
MarketDataService: marketDataService,
|
||||
ExecutionService: executionService,
|
||||
ModeManager: modeManager,
|
||||
StorageService: storageService
|
||||
};
|
||||
|
||||
// Initialize backtest mode
|
||||
await modeManager.initializeMode({
|
||||
mode: 'backtest',
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-01-15T00:00:00Z', // Just 15 days
|
||||
speed: 'max',
|
||||
symbols: ['TEST'],
|
||||
initialCapital: 100000,
|
||||
dataFrequency: '1d',
|
||||
strategy: 'sma-crossover'
|
||||
});
|
||||
|
||||
// Create backtest engine
|
||||
const backtestEngine = new BacktestEngine(container, storageService, strategyManager);
|
||||
|
||||
// Run backtest
|
||||
const config = {
|
||||
mode: 'backtest',
|
||||
name: 'Debug Performance Values',
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['TEST'],
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-01-15T00:00:00Z',
|
||||
initialCapital: 100000,
|
||||
commission: 0.001,
|
||||
slippage: 0.0001,
|
||||
dataFrequency: '1d',
|
||||
speed: 'max'
|
||||
};
|
||||
|
||||
console.log('Running backtest...');
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
// Debug values
|
||||
console.log('\n=== RAW VALUES DEBUG ===');
|
||||
console.log(`Initial Capital: $${config.initialCapital}`);
|
||||
console.log(`Reported Metrics:`);
|
||||
console.log(` - Total Return: ${result.metrics.totalReturn}%`);
|
||||
console.log(` - Sharpe Ratio: ${result.metrics.sharpeRatio}`);
|
||||
console.log(` - Max Drawdown: ${result.metrics.maxDrawdown}%`);
|
||||
console.log(` - Win Rate: ${result.metrics.winRate}%`);
|
||||
console.log(` - Total Trades: ${result.metrics.totalTrades}`);
|
||||
|
||||
console.log('\n=== EQUITY CURVE VALUES ===');
|
||||
console.log(`Equity Points: ${result.equity.length}`);
|
||||
if (result.equity.length > 0) {
|
||||
const first = result.equity[0];
|
||||
const last = result.equity[result.equity.length - 1];
|
||||
console.log(`First: ${first.date} => $${first.value}`);
|
||||
console.log(`Last: ${last.date} => $${last.value}`);
|
||||
|
||||
// Manual calculation
|
||||
const manualReturn = ((last.value - first.value) / first.value) * 100;
|
||||
console.log(`\nManual Total Return: ${manualReturn.toFixed(2)}%`);
|
||||
console.log(`Difference from reported: ${Math.abs(manualReturn - result.metrics.totalReturn).toFixed(2)}%`);
|
||||
}
|
||||
|
||||
console.log('\n=== TRADE ANALYSIS ===');
|
||||
console.log(`Closed Trades: ${result.trades.length}`);
|
||||
if (result.trades.length > 0) {
|
||||
const wins = result.trades.filter(t => t.pnl > 0);
|
||||
const losses = result.trades.filter(t => t.pnl < 0);
|
||||
const manualWinRate = (wins.length / result.trades.length) * 100;
|
||||
|
||||
console.log(`Wins: ${wins.length}`);
|
||||
console.log(`Losses: ${losses.length}`);
|
||||
console.log(`Manual Win Rate: ${manualWinRate.toFixed(2)}%`);
|
||||
|
||||
// Show P&L values
|
||||
const totalPnL = result.trades.reduce((sum, t) => sum + t.pnl, 0);
|
||||
console.log(`\nTotal P&L from trades: $${totalPnL.toFixed(2)}`);
|
||||
|
||||
// Show first few trades
|
||||
console.log('\nFirst 3 trades:');
|
||||
result.trades.slice(0, 3).forEach((t, i) => {
|
||||
console.log(` ${i+1}. ${t.side} ${t.quantity} @ ${t.exitPrice} | P&L: $${t.pnl.toFixed(2)}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Check trading engine P&L
|
||||
const tradingEngine = strategyManager.getTradingEngine();
|
||||
if (tradingEngine) {
|
||||
try {
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
console.log('\n=== TRADING ENGINE P&L ===');
|
||||
console.log(`Realized P&L: $${realized.toFixed(2)}`);
|
||||
console.log(`Unrealized P&L: $${unrealized.toFixed(2)}`);
|
||||
console.log(`Total P&L: $${(realized + unrealized).toFixed(2)}`);
|
||||
console.log(`Portfolio Value: $${(config.initialCapital + realized + unrealized).toFixed(2)}`);
|
||||
} catch (e) {
|
||||
console.error('Failed to get P&L from trading engine:', e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== TEST COMPLETE ===');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
debugPerformanceValues().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ The stock-bot orchestrator includes a high-performance Technical Analysis (TA) l
|
|||
|
||||
The TA library consists of:
|
||||
1. **Rust Core**: High-performance indicator calculations in `apps/stock/core/src/indicators/`
|
||||
2. **NAPI Bindings**: TypeScript interfaces exposed through `@stock-bot/core`
|
||||
2. **NAPI Bindings**: TypeScript interfaces exposed through `@stock-bot/engine`
|
||||
3. **TypeScript Wrapper**: Convenient API in `orchestrator/src/indicators/TechnicalAnalysis.ts`
|
||||
|
||||
## Available Indicators
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Demonstrates orderbook analytics, portfolio risk, and bet sizing
|
||||
*/
|
||||
|
||||
import { TradingEngine, RiskAnalyzer, OrderbookAnalyzer } from '@stock-bot/core';
|
||||
import { OrderbookAnalyzer, RiskAnalyzer, TradingEngine } from '@stock-bot/engine';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
const logger = getLogger('AdvancedRiskExample');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TradingEngine } from '@stock-bot/core';
|
||||
import { TradingEngine } from '@stock-bot/engine';
|
||||
|
||||
async function debugRustShortTrades() {
|
||||
// Create engine config for backtest mode
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
* Examples of using the Rust-based Technical Analysis library
|
||||
*/
|
||||
|
||||
import { TechnicalIndicators, IncrementalSMA, IncrementalEMA, IncrementalRSI } from '@stock-bot/core';
|
||||
import { TechnicalAnalysis, IncrementalIndicators, SignalGenerator } from '../src/indicators/TechnicalAnalysis';
|
||||
import { IncrementalIndicators, SignalGenerator, TechnicalAnalysis } from '../src/indicators/TechnicalAnalysis';
|
||||
|
||||
// Example 1: Basic indicator calculations
|
||||
async function basicIndicatorExample() {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { BacktestEngine as RustEngine } from '@stock-bot/core';
|
||||
import { BacktestConfig, BacktestResult } from '../types';
|
||||
import { BacktestEngine as RustEngine } from '@stock-bot/engine';
|
||||
import { EventEmitter } from 'events';
|
||||
import { StorageService } from '../services/StorageService';
|
||||
import { StrategyExecutor, SMACrossoverStrategy } from '../strategies/StrategyExecutor';
|
||||
import { SMACrossoverStrategy, StrategyExecutor } from '../strategies/StrategyExecutor';
|
||||
import { BacktestConfig, BacktestResult } from '../types';
|
||||
|
||||
/**
|
||||
* Adapter that bridges the orchestrator with the Rust backtest engine
|
||||
|
|
@ -208,7 +208,7 @@ export class RustBacktestAdapter extends EventEmitter {
|
|||
}
|
||||
|
||||
private registerStrategy(strategyName: string, parameters: any): void {
|
||||
if (!this.currentEngine) return;
|
||||
if (!this.currentEngine) {return;}
|
||||
|
||||
this.container.logger.info('Registering strategy', {
|
||||
strategyName,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { BacktestEngine as RustEngine } from '@stock-bot/core';
|
||||
import { RustStrategy } from '../strategies/RustStrategy';
|
||||
import { MarketData, BacktestConfig } from '../types';
|
||||
import { StorageService } from '../services/StorageService';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { BacktestEngine as RustEngine } from '@stock-bot/engine';
|
||||
import { StorageService } from '../services/StorageService';
|
||||
import { RustStrategy } from '../strategies/RustStrategy';
|
||||
import { BacktestConfig } from '../types';
|
||||
|
||||
export interface RustBacktestConfig {
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { TradingEngine } from '@stock-bot/core';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { TradingMode, ModeConfig, BacktestConfigSchema, PaperConfigSchema, LiveConfigSchema } from '../types';
|
||||
import { MarketDataService } from '../services/MarketDataService';
|
||||
import { ExecutionService } from '../services/ExecutionService';
|
||||
import { StorageService } from '../services/StorageService';
|
||||
import { TradingEngine } from '@stock-bot/engine';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ExecutionService } from '../services/ExecutionService';
|
||||
import { MarketDataService } from '../services/MarketDataService';
|
||||
import { StorageService } from '../services/StorageService';
|
||||
import { BacktestConfigSchema, LiveConfigSchema, ModeConfig, PaperConfigSchema, TradingMode } from '../types';
|
||||
|
||||
export class ModeManager extends EventEmitter {
|
||||
private mode: TradingMode = 'paper';
|
||||
|
|
@ -148,7 +148,7 @@ export class ModeManager extends EventEmitter {
|
|||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (!this.isInitialized) return;
|
||||
if (!this.isInitialized) {return;}
|
||||
|
||||
this.container.logger.info(`Shutting down ${this.mode} mode...`);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TechnicalIndicators, IncrementalSMA, IncrementalEMA, IncrementalRSI, MacdResult, BollingerBandsResult, StochasticResult } from '@stock-bot/core';
|
||||
import { BollingerBandsResult, IncrementalEMA, IncrementalRSI, IncrementalSMA, MacdResult, StochasticResult, TechnicalIndicators } from '@stock-bot/engine';
|
||||
|
||||
/**
|
||||
* Wrapper class for the Rust TA library with TypeScript-friendly interfaces
|
||||
|
|
@ -57,7 +57,7 @@ export class TechnicalAnalysis {
|
|||
|
||||
// Helper to check for crossovers
|
||||
static crossover(series1: number[], series2: number[]): boolean {
|
||||
if (series1.length < 2 || series2.length < 2) return false;
|
||||
if (series1.length < 2 || series2.length < 2) {return false;}
|
||||
const prev1 = series1[series1.length - 2];
|
||||
const curr1 = series1[series1.length - 1];
|
||||
const prev2 = series2[series2.length - 2];
|
||||
|
|
@ -66,7 +66,7 @@ export class TechnicalAnalysis {
|
|||
}
|
||||
|
||||
static crossunder(series1: number[], series2: number[]): boolean {
|
||||
if (series1.length < 2 || series2.length < 2) return false;
|
||||
if (series1.length < 2 || series2.length < 2) {return false;}
|
||||
const prev1 = series1[series1.length - 2];
|
||||
const curr1 = series1[series1.length - 1];
|
||||
const prev2 = series2[series2.length - 2];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { TradingEngine } from '@stock-bot/engine';
|
||||
import { EventEmitter } from 'events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { ModeConfig, OrderRequest, OrderRequestSchema } from '../types';
|
||||
import { TradingEngine } from '@stock-bot/core';
|
||||
import axios from 'axios';
|
||||
import { StorageService } from './StorageService';
|
||||
|
||||
interface ExecutionReport {
|
||||
|
|
@ -233,7 +232,7 @@ export class ExecutionService extends EventEmitter {
|
|||
}
|
||||
|
||||
private async processFills(executionReport: ExecutionReport): Promise<void> {
|
||||
if (!this.tradingEngine) return;
|
||||
if (!this.tradingEngine) {return;}
|
||||
|
||||
for (const fill of executionReport.fills) {
|
||||
// Update position in engine
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BacktestEngine } from '@stock-bot/core';
|
||||
import { BacktestEngine } from '@stock-bot/engine';
|
||||
import { MarketData } from '../types';
|
||||
|
||||
export interface Signal {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
import { MarketData, StrategyConfig, OrderRequest } from '../types';
|
||||
import { TradingEngine } from '@stock-bot/engine';
|
||||
import { EventEmitter } from 'events';
|
||||
import { MarketData, OrderRequest, StrategyConfig } from '../types';
|
||||
import { BaseStrategy } from './BaseStrategy';
|
||||
import { TradingEngine } from '@stock-bot/core';
|
||||
|
||||
export class StrategyManager extends EventEmitter {
|
||||
private strategies = new Map<string, BaseStrategy>();
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
import { createContainer } from './src/simple-container';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
async function testApiResponse() {
|
||||
console.log('Testing API Response Values...\n');
|
||||
|
||||
// First, create the container and start the server
|
||||
const container = await createContainer({
|
||||
database: {
|
||||
mongodb: { enabled: false, uri: '' },
|
||||
postgres: { enabled: false, uri: '' },
|
||||
questdb: { enabled: false, host: '', port: 0 },
|
||||
dragonfly: { enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
// Run a simple backtest via API
|
||||
const backtestRequest = {
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['AAPL'],
|
||||
startDate: '2023-01-01',
|
||||
endDate: '2023-02-01',
|
||||
initialCapital: 100000,
|
||||
config: {
|
||||
commission: 0.001,
|
||||
slippage: 0.0001,
|
||||
dataFrequency: '1d'
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('Sending backtest request...');
|
||||
const response = await fetch('http://localhost:2003/api/backtest/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(backtestRequest)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('\n=== API RESPONSE ===');
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
if (result.metrics) {
|
||||
console.log('\n=== METRICS VALUES ===');
|
||||
console.log(`Total Return: ${result.metrics.totalReturn}`);
|
||||
console.log(`Sharpe Ratio: ${result.metrics.sharpeRatio}`);
|
||||
console.log(`Max Drawdown: ${result.metrics.maxDrawdown}`);
|
||||
console.log(`Win Rate: ${result.metrics.winRate}`);
|
||||
console.log(`Total Trades: ${result.metrics.totalTrades}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('API call failed:', error);
|
||||
}
|
||||
|
||||
console.log('\n=== TEST COMPLETE ===');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
testApiResponse().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Simple backtest test without full container
|
||||
*/
|
||||
|
||||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { StorageService } from './src/services/StorageService';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
|
||||
async function runSimpleBacktest() {
|
||||
console.log('Running simple backtest test...\n');
|
||||
|
||||
// Create minimal container
|
||||
const logger = getLogger('test');
|
||||
const container = {
|
||||
logger,
|
||||
custom: {}
|
||||
};
|
||||
|
||||
// Create services
|
||||
const storageService = new StorageService(container as any);
|
||||
const strategyManager = new StrategyManager(container as any);
|
||||
|
||||
// Initialize strategy
|
||||
await strategyManager.initializeStrategies([{
|
||||
id: 'test-sma',
|
||||
name: 'sma-crossover',
|
||||
enabled: true,
|
||||
symbols: ['AAPL'],
|
||||
allocation: 1.0
|
||||
}]);
|
||||
|
||||
// Create backtest engine
|
||||
const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager);
|
||||
|
||||
const config = {
|
||||
mode: 'backtest',
|
||||
name: 'Simple SMA Test',
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['AAPL'],
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-03-01T00:00:00Z', // Just 2 months
|
||||
initialCapital: 100000,
|
||||
dataFrequency: '1d',
|
||||
commission: 0.001,
|
||||
slippage: 0.0001
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
console.log('\nBacktest Results:');
|
||||
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
|
||||
console.log(`Total Trades: ${result.metrics.totalTrades}`);
|
||||
console.log(`Trades in history: ${result.trades.length}`);
|
||||
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
|
||||
|
||||
console.log('\nTrade Details:');
|
||||
result.trades.forEach((trade, i) => {
|
||||
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} -> $${trade.exitPrice.toFixed(2)} (P&L: $${trade.pnl.toFixed(2)})`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Backtest failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
runSimpleBacktest().catch(console.error);
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import { createContainer } from './src/simple-container';
|
||||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover';
|
||||
|
||||
async function runBacktest() {
|
||||
console.log('Starting backtest test...');
|
||||
|
||||
// Create container with minimal config
|
||||
const config = {
|
||||
port: 2004,
|
||||
mode: 'paper',
|
||||
enableWebSocket: false,
|
||||
database: {
|
||||
mongodb: { enabled: false },
|
||||
postgres: { enabled: false },
|
||||
questdb: { enabled: false }
|
||||
},
|
||||
redis: {
|
||||
url: 'redis://localhost:6379'
|
||||
},
|
||||
backtesting: {
|
||||
maxConcurrent: 1,
|
||||
defaultSpeed: 'max',
|
||||
dataResolutions: ['1d']
|
||||
},
|
||||
strategies: {
|
||||
maxActive: 10,
|
||||
defaultTimeout: 30000
|
||||
}
|
||||
};
|
||||
const container = await createContainer(config);
|
||||
|
||||
// Initialize strategy manager
|
||||
const strategyManager = new StrategyManager(container.executionService, container.modeManager);
|
||||
|
||||
// Create and add strategy
|
||||
const strategyConfig = {
|
||||
id: 'sma-test',
|
||||
name: 'SMA Test',
|
||||
type: 'sma-crossover',
|
||||
symbols: ['AA'],
|
||||
active: true,
|
||||
allocation: 1.0,
|
||||
riskLimit: 0.02,
|
||||
maxPositions: 10
|
||||
};
|
||||
|
||||
const strategy = new SimpleMovingAverageCrossover(strategyConfig, container.modeManager, container.executionService);
|
||||
await strategyManager.addStrategy(strategy);
|
||||
|
||||
// Create backtest engine
|
||||
const backtestEngine = new BacktestEngine(container, strategyManager);
|
||||
|
||||
// Run backtest
|
||||
const backtestConfig = {
|
||||
symbols: ['AA'],
|
||||
strategy: 'sma-crossover',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-12-31',
|
||||
initialCapital: 100000,
|
||||
commission: 0.001,
|
||||
slippage: 0.0005,
|
||||
dataFrequency: '1d'
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('Running backtest...');
|
||||
const result = await backtestEngine.runBacktest(backtestConfig);
|
||||
|
||||
console.log('\n=== Backtest Results ===');
|
||||
console.log(`Total trades: ${result.metrics.totalTrades}`);
|
||||
console.log(`Win rate: ${result.metrics.winRate.toFixed(2)}%`);
|
||||
console.log(`Total return: ${result.metrics.totalReturn.toFixed(2)}%`);
|
||||
console.log(`Sharpe ratio: ${result.metrics.sharpeRatio.toFixed(2)}`);
|
||||
console.log(`Max drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`);
|
||||
|
||||
console.log('\n=== Trade History ===');
|
||||
result.trades.forEach((trade, i) => {
|
||||
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} ${trade.symbol} @ ${trade.entryPrice.toFixed(2)}`);
|
||||
if (trade.exitDate) {
|
||||
console.log(` Exit: ${trade.exitPrice.toFixed(2)}, P&L: ${trade.pnl.toFixed(2)} (${trade.pnlPercent.toFixed(2)}%)`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nTotal trades in result: ${result.trades.length}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Backtest failed:', error);
|
||||
} finally {
|
||||
// Cleanup
|
||||
await container.shutdownManager.shutdown();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
runBacktest().catch(console.error);
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Test with very clear crossover patterns
|
||||
*/
|
||||
|
||||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { StorageService } from './src/services/StorageService';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { ModeManager } from './src/core/ModeManager';
|
||||
import { MarketDataService } from './src/services/MarketDataService';
|
||||
import { ExecutionService } from './src/services/ExecutionService';
|
||||
import { DataManager } from './src/data/DataManager';
|
||||
|
||||
async function testClearCrossovers() {
|
||||
console.log('=== Test with Clear Crossovers ===\n');
|
||||
|
||||
const logger = getLogger('test');
|
||||
const container = {
|
||||
logger,
|
||||
custom: {}
|
||||
};
|
||||
|
||||
const storageService = new StorageService(container as any);
|
||||
const marketDataService = new MarketDataService(container as any);
|
||||
const executionService = new ExecutionService(container as any);
|
||||
const modeManager = new ModeManager(container as any, marketDataService, executionService, storageService);
|
||||
|
||||
container.custom = {
|
||||
ModeManager: modeManager,
|
||||
MarketDataService: marketDataService,
|
||||
ExecutionService: executionService
|
||||
};
|
||||
|
||||
const strategyManager = new StrategyManager(container as any);
|
||||
const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager);
|
||||
|
||||
// Override data loading to provide clear patterns
|
||||
const dataManager = new DataManager(container as any, storageService);
|
||||
(backtestEngine as any).dataManager = dataManager;
|
||||
|
||||
(dataManager as any).loadHistoricalData = async (symbols: string[], startDate: Date, endDate: Date) => {
|
||||
const data = new Map();
|
||||
const bars = [];
|
||||
|
||||
console.log('Generating clear crossover patterns...');
|
||||
|
||||
// Generate 100 days of data with 3 clear crossovers
|
||||
// Pattern: Start high, go low (death cross), go high (golden cross), go low (death cross)
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
let price;
|
||||
|
||||
if (i < 20) {
|
||||
// Start at 100, slight upward trend
|
||||
price = 100 + i * 0.5;
|
||||
} else if (i < 40) {
|
||||
// Sharp downtrend from 110 to 70 (death cross around day 30)
|
||||
price = 110 - (i - 20) * 2;
|
||||
} else if (i < 60) {
|
||||
// Sharp uptrend from 70 to 110 (golden cross around day 50)
|
||||
price = 70 + (i - 40) * 2;
|
||||
} else if (i < 80) {
|
||||
// Sharp downtrend from 110 to 70 (death cross around day 70)
|
||||
price = 110 - (i - 60) * 2;
|
||||
} else {
|
||||
// Stabilize around 70
|
||||
price = 70 + Math.sin((i - 80) * 0.3) * 2;
|
||||
}
|
||||
|
||||
const timestamp = startDate.getTime() + i * 86400000;
|
||||
|
||||
bars.push({
|
||||
type: 'bar',
|
||||
data: {
|
||||
symbol: 'AAPL',
|
||||
open: price - 0.5,
|
||||
high: price + 1,
|
||||
low: price - 1,
|
||||
close: price,
|
||||
volume: 1000000,
|
||||
timestamp
|
||||
}
|
||||
});
|
||||
|
||||
if (i % 10 === 0) {
|
||||
console.log(`Day ${i + 1}: Price = $${price.toFixed(2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nExpected crossovers:');
|
||||
console.log('- Death cross around day 30');
|
||||
console.log('- Golden cross around day 50');
|
||||
console.log('- Death cross around day 70\n');
|
||||
|
||||
data.set('AAPL', bars);
|
||||
return data;
|
||||
};
|
||||
|
||||
const config = {
|
||||
mode: 'backtest' as const,
|
||||
name: 'Clear Crossovers Test',
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['AAPL'],
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-04-10T00:00:00Z', // 100 days
|
||||
initialCapital: 100000,
|
||||
dataFrequency: '1d',
|
||||
commission: 0.001,
|
||||
slippage: 0.0001,
|
||||
speed: 'max' as const
|
||||
};
|
||||
|
||||
await modeManager.initializeMode(config);
|
||||
|
||||
try {
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
console.log('\n=== Backtest Results ===');
|
||||
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
|
||||
console.log(`Total Trades: ${result.metrics.totalTrades}`);
|
||||
console.log(`Trades in history: ${result.trades.length}`);
|
||||
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
|
||||
|
||||
console.log('\nTrade Details:');
|
||||
result.trades.forEach((trade, i) => {
|
||||
const entry = new Date(trade.entryDate).toLocaleDateString();
|
||||
const exit = trade.exitDate ? new Date(trade.exitDate).toLocaleDateString() : 'OPEN';
|
||||
console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} (${entry}) -> ${exit === 'OPEN' ? 'OPEN' : `$${trade.exitPrice.toFixed(2)} (${exit})`} | P&L: ${trade.pnl.toFixed(2)}`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Backtest failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testClearCrossovers().catch(console.error);
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { StorageService } from './src/services/StorageService';
|
||||
import { ModeManager } from './src/core/ModeManager';
|
||||
import { MarketDataService } from './src/services/MarketDataService';
|
||||
import { ExecutionService } from './src/services/ExecutionService';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
|
||||
async function testCommissionDetailed() {
|
||||
// Create service container with minimal logging
|
||||
const container: IServiceContainer = {
|
||||
logger: {
|
||||
info: () => {},
|
||||
error: (msg: string, ...args: any[]) => console.error('[ERROR]', msg, ...args),
|
||||
warn: () => {},
|
||||
debug: () => {},
|
||||
} as any,
|
||||
custom: {}
|
||||
};
|
||||
|
||||
// Initialize services
|
||||
const storageService = new StorageService();
|
||||
const marketDataService = new MarketDataService(container);
|
||||
const executionService = new ExecutionService(container);
|
||||
const modeManager = new ModeManager(container, marketDataService, executionService, storageService);
|
||||
const strategyManager = new StrategyManager(container);
|
||||
|
||||
// Set services in container
|
||||
container.custom = {
|
||||
MarketDataService: marketDataService,
|
||||
ExecutionService: executionService,
|
||||
ModeManager: modeManager,
|
||||
StorageService: storageService
|
||||
};
|
||||
|
||||
// Test with moderate commission and slippage
|
||||
const config = {
|
||||
mode: 'backtest',
|
||||
name: 'Commission Test',
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['AAPL'],
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-02-28T00:00:00Z', // 2 months
|
||||
initialCapital: 10000,
|
||||
commission: 0.002, // 0.2% commission
|
||||
slippage: 0.001, // 0.1% slippage
|
||||
dataFrequency: '1d',
|
||||
speed: 'max'
|
||||
};
|
||||
|
||||
await modeManager.initializeMode(config);
|
||||
const backtestEngine = new BacktestEngine(container, storageService, strategyManager);
|
||||
|
||||
console.log('\n=== COMMISSION & SLIPPAGE TEST ===');
|
||||
console.log(`Initial Capital: $${config.initialCapital}`);
|
||||
console.log(`Commission: ${(config.commission * 100).toFixed(2)}%`);
|
||||
console.log(`Slippage: ${(config.slippage * 100).toFixed(2)}%`);
|
||||
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
// Get P&L from Rust
|
||||
const tradingEngine = strategyManager.getTradingEngine();
|
||||
const [realized, unrealized] = tradingEngine.getTotalPnl();
|
||||
|
||||
console.log('\n=== RESULTS ===');
|
||||
console.log(`Total Trades: ${result.trades.length}`);
|
||||
console.log(`Final Portfolio Value: $${result.equity[result.equity.length - 1].value.toFixed(2)}`);
|
||||
console.log(`Realized P&L: $${realized.toFixed(2)}`);
|
||||
console.log(`Unrealized P&L: $${unrealized.toFixed(2)}`);
|
||||
|
||||
if (result.trades.length > 0) {
|
||||
console.log('\n=== TRADE DETAILS ===');
|
||||
let totalCommission = 0;
|
||||
let totalSlippageCost = 0;
|
||||
|
||||
result.trades.slice(0, 5).forEach((trade, idx) => {
|
||||
const entryValue = trade.quantity * trade.entryPrice;
|
||||
const exitValue = trade.quantity * trade.exitPrice;
|
||||
|
||||
// Calculate expected slippage
|
||||
// For a buy entry, we pay more (positive slippage)
|
||||
// For a sell entry (short), we receive less (negative slippage)
|
||||
const entrySlippage = trade.side === 'buy' ?
|
||||
entryValue * config.slippage :
|
||||
-entryValue * config.slippage;
|
||||
|
||||
// For exit, it's opposite
|
||||
const exitSlippage = trade.side === 'buy' ?
|
||||
-exitValue * config.slippage :
|
||||
exitValue * config.slippage;
|
||||
|
||||
const expectedCommission = (entryValue + exitValue) * config.commission;
|
||||
const slippageCost = entrySlippage + exitSlippage;
|
||||
|
||||
totalCommission += trade.commission;
|
||||
totalSlippageCost += slippageCost;
|
||||
|
||||
console.log(`\nTrade ${idx + 1}: ${trade.symbol} ${trade.side}`);
|
||||
console.log(` Quantity: ${trade.quantity} shares`);
|
||||
console.log(` Entry: $${trade.entryPrice.toFixed(2)} (value: $${entryValue.toFixed(2)})`);
|
||||
console.log(` Exit: $${trade.exitPrice.toFixed(2)} (value: $${exitValue.toFixed(2)})`);
|
||||
console.log(` Commission: $${trade.commission.toFixed(2)} (expected: $${expectedCommission.toFixed(2)})`);
|
||||
console.log(` Slippage Cost: $${slippageCost.toFixed(2)}`);
|
||||
console.log(` Gross P&L: $${(exitValue - entryValue).toFixed(2)}`);
|
||||
console.log(` Net P&L: $${trade.pnl.toFixed(2)}`);
|
||||
});
|
||||
|
||||
console.log(`\n=== TOTALS ===`);
|
||||
console.log(`Total Commission Paid: $${totalCommission.toFixed(2)}`);
|
||||
console.log(`Total Slippage Cost: $${totalSlippageCost.toFixed(2)}`);
|
||||
console.log(`Total Trading Costs: $${(totalCommission + totalSlippageCost).toFixed(2)}`);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
testCommissionDetailed().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { StorageService } from './src/services/StorageService';
|
||||
import { ModeManager } from './src/core/ModeManager';
|
||||
import { MarketDataService } from './src/services/MarketDataService';
|
||||
import { ExecutionService } from './src/services/ExecutionService';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
|
||||
async function testCommissionSlippage() {
|
||||
console.log('Testing Commission and Slippage...\n');
|
||||
|
||||
// Create service container
|
||||
const container: IServiceContainer = {
|
||||
logger: {
|
||||
info: (msg: string, ...args: any[]) => console.log('[INFO]', msg, ...args),
|
||||
error: (msg: string, ...args: any[]) => console.error('[ERROR]', msg, ...args),
|
||||
warn: (msg: string, ...args: any[]) => console.warn('[WARN]', msg, ...args),
|
||||
debug: (msg: string, ...args: any[]) => console.log('[DEBUG]', msg, ...args),
|
||||
} as any,
|
||||
custom: {}
|
||||
};
|
||||
|
||||
// Initialize services
|
||||
const storageService = new StorageService();
|
||||
const marketDataService = new MarketDataService(container);
|
||||
const executionService = new ExecutionService(container);
|
||||
const modeManager = new ModeManager(container, marketDataService, executionService, storageService);
|
||||
const strategyManager = new StrategyManager(container);
|
||||
|
||||
// Set services in container
|
||||
container.custom = {
|
||||
MarketDataService: marketDataService,
|
||||
ExecutionService: executionService,
|
||||
ModeManager: modeManager,
|
||||
StorageService: storageService
|
||||
};
|
||||
|
||||
// Test with high commission and slippage
|
||||
const config = {
|
||||
mode: 'backtest',
|
||||
name: 'Commission Test',
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['AAPL'],
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-01-31T00:00:00Z',
|
||||
initialCapital: 10000,
|
||||
commission: 0.01, // 1% commission (very high for testing)
|
||||
slippage: 0.005, // 0.5% slippage (very high for testing)
|
||||
dataFrequency: '1d',
|
||||
speed: 'max'
|
||||
};
|
||||
|
||||
// Initialize backtest mode
|
||||
await modeManager.initializeMode(config);
|
||||
|
||||
// Create backtest engine
|
||||
const backtestEngine = new BacktestEngine(container, storageService, strategyManager);
|
||||
|
||||
console.log('Running backtest with:');
|
||||
console.log(` Initial Capital: $${config.initialCapital}`);
|
||||
console.log(` Commission: ${(config.commission * 100).toFixed(1)}%`);
|
||||
console.log(` Slippage: ${(config.slippage * 100).toFixed(1)}%`);
|
||||
|
||||
try {
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
console.log('\n=== RESULTS ===');
|
||||
console.log(`Initial Capital: $${config.initialCapital}`);
|
||||
console.log(`Final Value: $${result.equity[result.equity.length - 1].value.toFixed(2)}`);
|
||||
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
|
||||
console.log(`Total Trades: ${result.metrics.totalTrades}`);
|
||||
|
||||
// Check trades for commission
|
||||
if (result.trades.length > 0) {
|
||||
console.log('\nFirst 3 trades with commission:');
|
||||
result.trades.slice(0, 3).forEach((t, idx) => {
|
||||
const tradeValue = t.quantity * t.entryPrice;
|
||||
const expectedCommission = tradeValue * config.commission;
|
||||
const totalCost = tradeValue + expectedCommission;
|
||||
|
||||
console.log(`\n${idx + 1}. ${t.symbol} ${t.side} ${t.quantity} shares`);
|
||||
console.log(` Entry Price: $${t.entryPrice.toFixed(2)}`);
|
||||
console.log(` Exit Price: $${t.exitPrice.toFixed(2)}`);
|
||||
console.log(` Trade Value: $${tradeValue.toFixed(2)}`);
|
||||
console.log(` Commission: $${t.commission.toFixed(2)} (expected: $${expectedCommission.toFixed(2)})`);
|
||||
console.log(` P&L: $${t.pnl.toFixed(2)}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Compare with zero commission/slippage
|
||||
const zeroConfig = {
|
||||
...config,
|
||||
commission: 0,
|
||||
slippage: 0
|
||||
};
|
||||
|
||||
await modeManager.initializeMode(zeroConfig);
|
||||
const zeroResult = await backtestEngine.runBacktest(zeroConfig);
|
||||
|
||||
console.log('\n=== COMPARISON ===');
|
||||
console.log('With commission/slippage:');
|
||||
console.log(` Final Value: $${result.equity[result.equity.length - 1].value.toFixed(2)}`);
|
||||
console.log(` Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
|
||||
|
||||
console.log('\nWithout commission/slippage:');
|
||||
console.log(` Final Value: $${zeroResult.equity[zeroResult.equity.length - 1].value.toFixed(2)}`);
|
||||
console.log(` Total Return: ${zeroResult.metrics.totalReturn.toFixed(2)}%`);
|
||||
|
||||
const difference = zeroResult.equity[zeroResult.equity.length - 1].value - result.equity[result.equity.length - 1].value;
|
||||
console.log(`\nCost of commission/slippage: $${difference.toFixed(2)}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Backtest failed:', error);
|
||||
}
|
||||
|
||||
console.log('\n=== TEST COMPLETE ===');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
testCommissionSlippage().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { PerformanceAnalyzer } from './src/analytics/PerformanceAnalyzer';
|
||||
|
||||
// Test with simple data
|
||||
const analyzer = new PerformanceAnalyzer(100000);
|
||||
|
||||
// Add some equity points over 30 days
|
||||
const startDate = new Date('2023-01-01');
|
||||
const values = [
|
||||
100000, 101000, 100500, 102000, 101500,
|
||||
103000, 102500, 104000, 103500, 105000,
|
||||
104500, 106000, 105500, 107000, 106500,
|
||||
108000, 107500, 109000, 108500, 110000
|
||||
];
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const date = new Date(startDate);
|
||||
date.setDate(date.getDate() + i);
|
||||
analyzer.addEquityPoint(date, values[i]);
|
||||
console.log(`Day ${i + 1}: ${date.toISOString().split('T')[0]} => $${values[i]}`);
|
||||
}
|
||||
|
||||
console.log('\n=== Analyzing Performance ===');
|
||||
const metrics = analyzer.analyze();
|
||||
|
||||
console.log(`\nTotal Return: ${metrics.totalReturn.toFixed(2)}%`);
|
||||
console.log(`Annualized Return: ${metrics.annualizedReturn.toFixed(2)}%`);
|
||||
console.log(`Volatility: ${metrics.volatility.toFixed(2)}%`);
|
||||
console.log(`Sharpe Ratio: ${metrics.sharpeRatio.toFixed(3)}`);
|
||||
|
||||
// Debug calculations
|
||||
const finalValue = 110000;
|
||||
const initialValue = 100000;
|
||||
const totalReturn = ((finalValue - initialValue) / initialValue) * 100;
|
||||
const days = 19;
|
||||
const years = days / 365;
|
||||
const annualizedReturn = (Math.pow(1 + totalReturn/100, 1/years) - 1) * 100;
|
||||
|
||||
console.log('\n=== Manual Calculations ===');
|
||||
console.log(`Total Return: ${totalReturn.toFixed(2)}%`);
|
||||
console.log(`Years: ${years.toFixed(4)}`);
|
||||
console.log(`Annualized Return: ${annualizedReturn.toFixed(2)}%`);
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { StorageService } from './src/services/StorageService';
|
||||
import { ModeManager } from './src/core/ModeManager';
|
||||
import { MarketDataService } from './src/services/MarketDataService';
|
||||
import { ExecutionService } from './src/services/ExecutionService';
|
||||
import { IServiceContainer } from '@stock-bot/di';
|
||||
|
||||
async function debugEquityCurve() {
|
||||
// Create service container
|
||||
const container: IServiceContainer = {
|
||||
logger: {
|
||||
info: (msg: string, ...args: any[]) => console.log('[INFO]', msg, ...args),
|
||||
error: (msg: string, ...args: any[]) => console.error('[ERROR]', msg, ...args),
|
||||
warn: (msg: string, ...args: any[]) => console.warn('[WARN]', msg, ...args),
|
||||
debug: (msg: string, ...args: any[]) => console.log('[DEBUG]', msg, ...args),
|
||||
} as any,
|
||||
custom: {}
|
||||
};
|
||||
|
||||
// Initialize services
|
||||
const storageService = new StorageService();
|
||||
const marketDataService = new MarketDataService(container);
|
||||
const executionService = new ExecutionService(container);
|
||||
const modeManager = new ModeManager(container, marketDataService, executionService, storageService);
|
||||
const strategyManager = new StrategyManager(container);
|
||||
|
||||
// Set services in container
|
||||
container.custom = {
|
||||
MarketDataService: marketDataService,
|
||||
ExecutionService: executionService,
|
||||
ModeManager: modeManager,
|
||||
StorageService: storageService
|
||||
};
|
||||
|
||||
// Test with 5000 initial capital
|
||||
const config = {
|
||||
mode: 'backtest',
|
||||
name: 'Debug',
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['AAPL'],
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-01-05T00:00:00Z', // Just 5 days
|
||||
initialCapital: 5000,
|
||||
commission: 0.001,
|
||||
slippage: 0.0001,
|
||||
dataFrequency: '1d',
|
||||
speed: 'max'
|
||||
};
|
||||
|
||||
await modeManager.initializeMode(config);
|
||||
const backtestEngine = new BacktestEngine(container, storageService, strategyManager);
|
||||
|
||||
console.log('Before runBacktest - checking backtestEngine state...');
|
||||
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
console.log('\n=== EQUITY CURVE DEBUG ===');
|
||||
console.log(`Config Initial Capital: $${config.initialCapital}`);
|
||||
console.log(`Number of equity points: ${result.equity.length}`);
|
||||
|
||||
// Show all equity points
|
||||
result.equity.forEach((point, idx) => {
|
||||
console.log(` ${idx}: ${point.date} -> $${point.value.toFixed(2)}`);
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
debugEquityCurve().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { createContainer } from './src/simple-container';
|
||||
import { BacktestEngine } from './src/backtest/BacktestEngine';
|
||||
import { StrategyManager } from './src/strategies/StrategyManager';
|
||||
import { StorageService } from './src/services/StorageService';
|
||||
|
||||
async function testEquityCurveSpikeFixture() {
|
||||
console.log('Testing equity curve spike fix...\n');
|
||||
|
||||
// Create minimal container
|
||||
const container = await createContainer({
|
||||
database: {
|
||||
mongodb: { enabled: false, uri: '' },
|
||||
postgres: { enabled: false, uri: '' },
|
||||
questdb: { enabled: false, host: '', port: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
// Create services
|
||||
const storageService = new StorageService(container);
|
||||
const strategyManager = new StrategyManager(container);
|
||||
|
||||
// Create backtest engine
|
||||
const backtestEngine = new BacktestEngine(container, storageService, strategyManager);
|
||||
|
||||
// Run a quick backtest
|
||||
const config = {
|
||||
mode: 'backtest',
|
||||
name: 'Equity Curve Spike Test',
|
||||
strategy: 'sma-crossover',
|
||||
symbols: ['AAPL'],
|
||||
startDate: '2023-01-01T00:00:00Z',
|
||||
endDate: '2023-01-31T00:00:00Z', // Just one month
|
||||
initialCapital: 100000,
|
||||
commission: 0.001,
|
||||
slippage: 0.0001,
|
||||
dataFrequency: '1d',
|
||||
speed: 'max'
|
||||
};
|
||||
|
||||
console.log('Running backtest...');
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
// Check the equity curve
|
||||
console.log('\n=== Equity Curve Analysis ===');
|
||||
console.log(`Initial Capital: $${config.initialCapital}`);
|
||||
console.log(`Total equity points: ${result.equity.length}`);
|
||||
|
||||
if (result.equity.length > 0) {
|
||||
// Check first few points
|
||||
console.log('\nFirst 5 equity curve points:');
|
||||
result.equity.slice(0, 5).forEach((point, index) => {
|
||||
console.log(`${index + 1}. ${point.date}: $${point.value.toFixed(2)}`);
|
||||
});
|
||||
|
||||
// Check for spike
|
||||
const firstValue = result.equity[0].value;
|
||||
const secondValue = result.equity.length > 1 ? result.equity[1].value : firstValue;
|
||||
|
||||
console.log(`\nFirst equity value: $${firstValue.toFixed(2)}`);
|
||||
console.log(`Second equity value: $${secondValue.toFixed(2)}`);
|
||||
|
||||
// Check if there's a spike from 0 to initial capital
|
||||
if (firstValue === 0 || firstValue < config.initialCapital * 0.5) {
|
||||
console.log('\n❌ SPIKE DETECTED: First equity value is too low!');
|
||||
} else if (Math.abs(firstValue - config.initialCapital) < 1) {
|
||||
console.log('\n✅ NO SPIKE: First equity value correctly starts at initial capital!');
|
||||
} else {
|
||||
console.log(`\n⚠️ First equity value differs from initial capital by $${Math.abs(firstValue - config.initialCapital).toFixed(2)}`);
|
||||
}
|
||||
|
||||
// Check for duplicate timestamps
|
||||
const timestamps = new Set();
|
||||
let duplicates = 0;
|
||||
result.equity.forEach(point => {
|
||||
if (timestamps.has(point.date)) {
|
||||
duplicates++;
|
||||
}
|
||||
timestamps.add(point.date);
|
||||
});
|
||||
|
||||
if (duplicates > 0) {
|
||||
console.log(`\n⚠️ Found ${duplicates} duplicate timestamps in equity curve`);
|
||||
} else {
|
||||
console.log('\n✅ No duplicate timestamps found');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Test Complete ===');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
testEquityCurveSpikeFixture().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue