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]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.27"
|
version = "1.2.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
|
checksum = "4ad45f4f74e4e20eaa392913b7b33a7091c87e59628f4dd27888205ad888843c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
@ -151,32 +151,6 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
@ -262,6 +236,32 @@ dependencies = [
|
||||||
"parking_lot_core",
|
"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]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
@ -321,6 +321,17 @@ dependencies = [
|
||||||
"cc",
|
"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]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
|
@ -876,6 +887,12 @@ dependencies = [
|
||||||
"wide",
|
"wide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
|
|
@ -958,17 +975,19 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.45.1"
|
version = "1.46.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"io-uring",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
"slab",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"apps/stock/core"
|
"apps/stock/engine"
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "core"
|
name = "engine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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,
|
"lockfileVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@stock-bot/core",
|
"name": "@stock-bot/engine",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@napi-rs/cli": "^2.16.3",
|
"@napi-rs/cli": "^2.16.3",
|
||||||
"cargo-cp-artifact": "^0.1",
|
"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",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"index.node"
|
"index.node"
|
||||||
],
|
],
|
||||||
"napi": {
|
"napi": {
|
||||||
"name": "core",
|
"name": "engine",
|
||||||
"triples": {
|
"triples": {
|
||||||
"additional": [
|
"additional": [
|
||||||
"x86_64-pc-windows-msvc",
|
"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)]
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
|
// Existing modules
|
||||||
pub mod core;
|
pub mod core;
|
||||||
|
pub mod adapters;
|
||||||
pub mod orderbook;
|
pub mod orderbook;
|
||||||
pub mod risk;
|
pub mod risk;
|
||||||
pub mod positions;
|
pub mod positions;
|
||||||
|
#[cfg(not(test))]
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod analytics;
|
pub mod analytics;
|
||||||
pub mod indicators;
|
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:
|
The TA library consists of:
|
||||||
1. **Rust Core**: High-performance indicator calculations in `apps/stock/core/src/indicators/`
|
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`
|
3. **TypeScript Wrapper**: Convenient API in `orchestrator/src/indicators/TechnicalAnalysis.ts`
|
||||||
|
|
||||||
## Available Indicators
|
## Available Indicators
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Demonstrates orderbook analytics, portfolio risk, and bet sizing
|
* 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';
|
import { getLogger } from '@stock-bot/logger';
|
||||||
|
|
||||||
const logger = getLogger('AdvancedRiskExample');
|
const logger = getLogger('AdvancedRiskExample');
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { TradingEngine } from '@stock-bot/core';
|
import { TradingEngine } from '@stock-bot/engine';
|
||||||
|
|
||||||
async function debugRustShortTrades() {
|
async function debugRustShortTrades() {
|
||||||
// Create engine config for backtest mode
|
// Create engine config for backtest mode
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
* Examples of using the Rust-based Technical Analysis library
|
* Examples of using the Rust-based Technical Analysis library
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TechnicalIndicators, IncrementalSMA, IncrementalEMA, IncrementalRSI } from '@stock-bot/core';
|
import { IncrementalIndicators, SignalGenerator, TechnicalAnalysis } from '../src/indicators/TechnicalAnalysis';
|
||||||
import { TechnicalAnalysis, IncrementalIndicators, SignalGenerator } from '../src/indicators/TechnicalAnalysis';
|
|
||||||
|
|
||||||
// Example 1: Basic indicator calculations
|
// Example 1: Basic indicator calculations
|
||||||
async function basicIndicatorExample() {
|
async function basicIndicatorExample() {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { IServiceContainer } from '@stock-bot/di';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { BacktestEngine as RustEngine } from '@stock-bot/core';
|
import { BacktestEngine as RustEngine } from '@stock-bot/engine';
|
||||||
import { BacktestConfig, BacktestResult } from '../types';
|
import { EventEmitter } from 'events';
|
||||||
import { StorageService } from '../services/StorageService';
|
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
|
* 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 {
|
private registerStrategy(strategyName: string, parameters: any): void {
|
||||||
if (!this.currentEngine) return;
|
if (!this.currentEngine) {return;}
|
||||||
|
|
||||||
this.container.logger.info('Registering strategy', {
|
this.container.logger.info('Registering strategy', {
|
||||||
strategyName,
|
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 { 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 {
|
export interface RustBacktestConfig {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { TradingEngine } from '@stock-bot/core';
|
|
||||||
import { IServiceContainer } from '@stock-bot/di';
|
import { IServiceContainer } from '@stock-bot/di';
|
||||||
import { TradingMode, ModeConfig, BacktestConfigSchema, PaperConfigSchema, LiveConfigSchema } from '../types';
|
import { TradingEngine } from '@stock-bot/engine';
|
||||||
import { MarketDataService } from '../services/MarketDataService';
|
|
||||||
import { ExecutionService } from '../services/ExecutionService';
|
|
||||||
import { StorageService } from '../services/StorageService';
|
|
||||||
import { EventEmitter } from 'events';
|
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 {
|
export class ModeManager extends EventEmitter {
|
||||||
private mode: TradingMode = 'paper';
|
private mode: TradingMode = 'paper';
|
||||||
|
|
@ -148,7 +148,7 @@ export class ModeManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
if (!this.isInitialized) return;
|
if (!this.isInitialized) {return;}
|
||||||
|
|
||||||
this.container.logger.info(`Shutting down ${this.mode} mode...`);
|
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
|
* Wrapper class for the Rust TA library with TypeScript-friendly interfaces
|
||||||
|
|
@ -57,7 +57,7 @@ export class TechnicalAnalysis {
|
||||||
|
|
||||||
// Helper to check for crossovers
|
// Helper to check for crossovers
|
||||||
static crossover(series1: number[], series2: number[]): boolean {
|
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 prev1 = series1[series1.length - 2];
|
||||||
const curr1 = series1[series1.length - 1];
|
const curr1 = series1[series1.length - 1];
|
||||||
const prev2 = series2[series2.length - 2];
|
const prev2 = series2[series2.length - 2];
|
||||||
|
|
@ -66,7 +66,7 @@ export class TechnicalAnalysis {
|
||||||
}
|
}
|
||||||
|
|
||||||
static crossunder(series1: number[], series2: number[]): boolean {
|
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 prev1 = series1[series1.length - 2];
|
||||||
const curr1 = series1[series1.length - 1];
|
const curr1 = series1[series1.length - 1];
|
||||||
const prev2 = series2[series2.length - 2];
|
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 { EventEmitter } from 'events';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { IServiceContainer } from '@stock-bot/di';
|
|
||||||
import { ModeConfig, OrderRequest, OrderRequestSchema } from '../types';
|
import { ModeConfig, OrderRequest, OrderRequestSchema } from '../types';
|
||||||
import { TradingEngine } from '@stock-bot/core';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { StorageService } from './StorageService';
|
import { StorageService } from './StorageService';
|
||||||
|
|
||||||
interface ExecutionReport {
|
interface ExecutionReport {
|
||||||
|
|
@ -233,7 +232,7 @@ export class ExecutionService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processFills(executionReport: ExecutionReport): Promise<void> {
|
private async processFills(executionReport: ExecutionReport): Promise<void> {
|
||||||
if (!this.tradingEngine) return;
|
if (!this.tradingEngine) {return;}
|
||||||
|
|
||||||
for (const fill of executionReport.fills) {
|
for (const fill of executionReport.fills) {
|
||||||
// Update position in engine
|
// Update position in engine
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { BacktestEngine } from '@stock-bot/core';
|
import { BacktestEngine } from '@stock-bot/engine';
|
||||||
import { MarketData } from '../types';
|
import { MarketData } from '../types';
|
||||||
|
|
||||||
export interface Signal {
|
export interface Signal {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { IServiceContainer } from '@stock-bot/di';
|
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 { BaseStrategy } from './BaseStrategy';
|
||||||
import { TradingEngine } from '@stock-bot/core';
|
|
||||||
|
|
||||||
export class StrategyManager extends EventEmitter {
|
export class StrategyManager extends EventEmitter {
|
||||||
private strategies = new Map<string, BaseStrategy>();
|
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