├── src-tauri ├── build.rs ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 32x32.png │ ├── icon.icns │ ├── 128x128.png │ ├── StoreLogo.png │ ├── 128x128@2x.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── .gitignore ├── capabilities │ └── default.json ├── Cargo.toml ├── tauri.conf.json ├── src │ ├── main.rs │ ├── types.rs │ ├── state.rs │ └── commands.rs └── README.md ├── .gitignore ├── trading-core ├── src │ ├── data │ │ ├── mod.rs │ │ ├── types.rs │ │ └── cache.rs │ ├── lib.rs │ ├── live_trading │ │ ├── mod.rs │ │ └── paper_trading.rs │ ├── service │ │ ├── mod.rs │ │ ├── errors.rs │ │ └── types.rs │ ├── backtest │ │ ├── mod.rs │ │ ├── strategy │ │ │ ├── base.rs │ │ │ ├── mod.rs │ │ │ ├── sma.rs │ │ │ └── rsi.rs │ │ ├── portfolio.rs │ │ └── metrics.rs │ ├── exchange │ │ ├── mod.rs │ │ ├── traits.rs │ │ ├── errors.rs │ │ ├── types.rs │ │ ├── utils.rs │ │ └── binance.rs │ └── config.rs ├── CLOSE_FLOW.md ├── Cargo.toml ├── benches │ └── repository_bench.rs └── README.md ├── assets ├── version1.png ├── backtestPage1.png ├── backtestPage2.png ├── backtestPage3.png ├── data_flow01.png ├── version2NFT.png ├── substrate_node.png └── version2Strategy.png ├── frontend ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── trading │ │ │ └── page.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── globals.css │ ├── lib │ │ └── utils.ts │ ├── components │ │ ├── layout │ │ │ ├── Header.tsx │ │ │ └── Sidebar.tsx │ │ └── ui │ │ │ ├── input.tsx │ │ │ ├── badge.tsx │ │ │ ├── popover.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── datetime-picker.tsx │ │ │ └── calendar.tsx │ └── types │ │ └── backtest.ts ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg ├── postcss.config.mjs ├── next.config.ts ├── eslint.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── tailwind.config.ts ├── Cargo.toml ├── config ├── development.toml ├── live_strategy_log.sql └── schema.sql ├── LICENSE └── README.md /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | config/local.toml 4 | Cargo.lock 5 | .idea 6 | .vscode -------------------------------------------------------------------------------- /trading-core/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod repository; 3 | pub mod types; 4 | -------------------------------------------------------------------------------- /assets/version1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/version1.png -------------------------------------------------------------------------------- /assets/backtestPage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/backtestPage1.png -------------------------------------------------------------------------------- /assets/backtestPage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/backtestPage2.png -------------------------------------------------------------------------------- /assets/backtestPage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/backtestPage3.png -------------------------------------------------------------------------------- /assets/data_flow01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/data_flow01.png -------------------------------------------------------------------------------- /assets/version2NFT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/version2NFT.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /assets/substrate_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/substrate_node.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /trading-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod backtest; 2 | pub mod config; 3 | pub mod data; 4 | pub mod live_trading; 5 | -------------------------------------------------------------------------------- /assets/version2Strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/assets/version2Strategy.png -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/src/app/trading/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Trading() { 2 | return
Trading Page
; 3 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /trading-core/src/live_trading/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod paper_trading; 2 | 3 | pub use paper_trading::PaperTradingProcessor; 4 | -------------------------------------------------------------------------------- /frontend/src/app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Settings() { 2 | return
Settings Page
; 3 | } -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /gen/schemas 5 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Erio-Harrison/rust-trade/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "trading-core", 4 | "src-tauri" 5 | ] 6 | resolver = "2" 7 | 8 | [workspace.package] 9 | version = "0.1.0" 10 | license = "MIT" 11 | authors = ["Harrison"] -------------------------------------------------------------------------------- /frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /trading-core/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod market_data; 3 | pub mod types; 4 | 5 | // Re-export main interfaces 6 | pub use errors::ServiceError; 7 | pub use market_data::MarketDataService; 8 | pub use types::*; 9 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "enables the default permissions", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /trading-core/src/backtest/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod engine; 2 | pub mod metrics; 3 | pub mod portfolio; 4 | pub mod strategy; 5 | 6 | pub use engine::{BacktestConfig, BacktestEngine, BacktestResult}; 7 | pub use portfolio::{Portfolio, Position, Trade}; 8 | pub use strategy::{create_strategy, list_strategies, Signal, Strategy, StrategyInfo}; 9 | -------------------------------------------------------------------------------- /frontend/src/components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Header = () => { 4 | return ( 5 |
6 |

Rust Trading System

7 |
8 | ) 9 | } 10 | 11 | export default Header -------------------------------------------------------------------------------- /trading-core/src/exchange/mod.rs: -------------------------------------------------------------------------------- 1 | // exchange/mod.rs 2 | pub mod binance; 3 | pub mod errors; 4 | pub mod traits; 5 | pub mod types; 6 | pub mod utils; 7 | 8 | // Re-export main interfaces for easy access 9 | pub use binance::BinanceExchange; 10 | pub use errors::ExchangeError; 11 | pub use traits::Exchange; 12 | pub use types::*; 13 | -------------------------------------------------------------------------------- /frontend/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | const nextConfig = { 2 | output: 'export', 3 | images: { 4 | unoptimized: true, 5 | }, 6 | assetPrefix: './', 7 | trailingSlash: true, 8 | webpack: (config: { resolve: { fallback: any; }; }) => { 9 | config.resolve.fallback = { 10 | ...(config.resolve.fallback || {}), 11 | fs: false, 12 | path: false, 13 | }; 14 | return config; 15 | }, 16 | }; 17 | 18 | module.exports = nextConfig; 19 | -------------------------------------------------------------------------------- /frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /config/development.toml: -------------------------------------------------------------------------------- 1 | # Trading pairs to monitor 2 | symbols = ["BTCUSDT", "ETHUSDT", "ADAUSDT"] 3 | 4 | [server] 5 | host = "0.0.0.0" 6 | port = 8080 7 | 8 | [database] 9 | max_connections = 5 10 | min_connections = 1 11 | max_lifetime = 1800 12 | 13 | [cache] 14 | [cache.memory] 15 | max_ticks_per_symbol = 1000 16 | ttl_seconds = 300 17 | 18 | [cache.redis] 19 | pool_size = 10 20 | ttl_seconds = 3600 21 | max_ticks_per_symbol = 10000 22 | 23 | [paper_trading] 24 | enabled = true 25 | strategy = "rsi" 26 | initial_capital = 10000.0 -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | /package-lock.json 13 | 14 | # testing 15 | /coverage 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # env files (can opt-in for committing if needed) 35 | .env* 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } -------------------------------------------------------------------------------- /config/live_strategy_log.sql: -------------------------------------------------------------------------------- 1 | -- Simple real-time strategy log table 2 | CREATE TABLE live_strategy_log ( 3 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 4 | timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), 5 | strategy_id VARCHAR(50) NOT NULL, 6 | symbol VARCHAR(20) NOT NULL, 7 | current_price DECIMAL(18,8) NOT NULL, 8 | signal_type VARCHAR(10) NOT NULL, -- BUY/SELL/HOLD 9 | portfolio_value DECIMAL(18,8) NOT NULL, 10 | total_pnl DECIMAL(18,8) NOT NULL DEFAULT 0, 11 | cache_hit BOOLEAN DEFAULT TRUE, -- Mark whether to get data from cache 12 | processing_time_us INTEGER -- Processing time (microseconds), reflecting cache value 13 | ); 14 | 15 | -- Basic index 16 | CREATE INDEX idx_live_strategy_time ON live_strategy_log(timestamp DESC); 17 | CREATE INDEX idx_live_strategy_symbol ON live_strategy_log(strategy_id, symbol); -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trading-desktop" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | 7 | [build-dependencies] 8 | tauri-build = { version = "2", features = [] } 9 | 10 | [dependencies] 11 | serde_json = "1.0" 12 | chrono = { version = "0.4", features = ["serde"] } 13 | log = "0.4" 14 | sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "rust_decimal"] } 15 | serde = { version = "1.0", features = ["derive"] } 16 | tauri = { version = "2", features = [] } 17 | tauri-plugin-shell = "2" 18 | trading-core = { path = "../trading-core" } 19 | tokio = { version = "1.0", features = ["full"] } 20 | tracing = "0.1" 21 | tracing-subscriber = "0.3" 22 | rust_decimal = { version = "1.32", features = ["serde"] } 23 | dotenvy = "0.15" 24 | 25 | [features] 26 | custom-protocol = ["tauri/custom-protocol"] 27 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | // src/app/layout.tsx 2 | import './globals.css' 3 | import Header from '@/components/layout/Header' 4 | import Sidebar from '@/components/layout/Sidebar' 5 | 6 | export const metadata = { 7 | title: 'Rust Trading System', 8 | description: 'Advanced trading system built with Rust, Tauri and Next.js', 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | 19 |
20 |
21 |
22 | 23 |
24 | {children} 25 |
26 |
27 |
28 | 29 | 30 | ) 31 | } -------------------------------------------------------------------------------- /frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "rust-trade", 4 | "version": "0.1.0", 5 | "identifier": "com.rust-trade.dev", 6 | "build": { 7 | "beforeDevCommand": "cd ../frontend && npm run dev", 8 | "beforeBuildCommand": "cd ../frontend && npm run build", 9 | "devUrl": "http://localhost:3000", 10 | "frontendDist": "../frontend/out" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "Rust Trade", 16 | "width": 1200, 17 | "height": 800, 18 | "resizable": true 19 | } 20 | ], 21 | "security": { 22 | "csp": null 23 | } 24 | }, 25 | "bundle": { 26 | "active": true, 27 | "targets": "all", 28 | "icon": [ 29 | "icons/32x32.png", 30 | "icons/128x128.png", 31 | "icons/128x128@2x.png", 32 | "icons/icon.icns", 33 | "icons/icon.ico" 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /frontend/src/components/layout/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | 4 | const Sidebar = () => { 5 | const menuItems = [ 6 | { label: 'Dashboard', path: '/' }, 7 | { label: 'Trading', path: '/trading' }, 8 | { label: 'Backtest', path: '/backtest' }, 9 | { label: 'Settings', path: '/settings' }, 10 | ] 11 | 12 | return ( 13 | 29 | ) 30 | } 31 | 32 | export default Sidebar -------------------------------------------------------------------------------- /frontend/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /trading-core/src/backtest/strategy/base.rs: -------------------------------------------------------------------------------- 1 | use crate::data::types::{OHLCData, TickData}; 2 | use rust_decimal::Decimal; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum Signal { 7 | Buy { symbol: String, quantity: Decimal }, 8 | Sell { symbol: String, quantity: Decimal }, 9 | Hold, 10 | } 11 | 12 | pub trait Strategy: Send + Sync { 13 | fn name(&self) -> &str; 14 | fn on_tick(&mut self, tick: &TickData) -> Signal; 15 | fn initialize(&mut self, params: HashMap) -> Result<(), String>; 16 | 17 | /// Reset strategy state for new backtest 18 | fn reset(&mut self) { 19 | // Default implementation does nothing 20 | // Strategies can override if needed 21 | } 22 | 23 | fn on_ohlc(&mut self, ohlc: &OHLCData) -> Signal { 24 | Signal::Hold 25 | } 26 | fn supports_ohlc(&self) -> bool { 27 | false 28 | } 29 | fn preferred_timeframe(&self) -> Option { 30 | None 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /trading-core/CLOSE_FLOW.md: -------------------------------------------------------------------------------- 1 | # Complete Shutdown Process 2 | 3 | 1. **Signal Capture** - The user presses Ctrl+C, and the signal handler in main.rs catches the signal. 4 | 5 | 2. **Signal Forwarding** - The signal handler calls `service_shutdown_tx.send()` to send the shutdown signal to the service. 6 | 7 | 3. **WebSocket Graceful Shutdown** - The Exchange layer receives the shutdown signal: 8 | - Sends a Close frame to the Binance server 9 | - Gracefully disconnects the WebSocket connection 10 | - Returns `Ok(())` and does not reconnect. 11 | 12 | 4. **Data Processing Completed** - Data Processing Pipeline: 13 | - Saves remaining data in the buffer 14 | - Closes the data processing pipeline 15 | 16 | 5. **Service Layer Coordinated Shutdown** - MarketDataService: 17 | - Waits for data collection and processing tasks to complete. 18 | - "Market data service stopped normally" 19 | 20 | 6. **Application Layer Completion** - main.rs: 21 | - service.start() returns normally. 22 | - "Service stopped successfully" 23 | - "Application stopped gracefully" -------------------------------------------------------------------------------- /trading-core/src/exchange/traits.rs: -------------------------------------------------------------------------------- 1 | // ================================================================= 2 | // exchange/traits.rs - Exchange Interface Definition 3 | // ================================================================= 4 | 5 | use super::{ExchangeError, HistoricalTradeParams}; 6 | use crate::data::types::TickData; 7 | use async_trait::async_trait; 8 | 9 | /// Main exchange interface that all exchange implementations must follow 10 | #[async_trait] 11 | pub trait Exchange: Send + Sync { 12 | /// Subscribe to real-time trade data streams 13 | async fn subscribe_trades( 14 | &self, 15 | symbols: &[String], 16 | callback: Box, 17 | shutdown_rx: tokio::sync::broadcast::Receiver<()>, 18 | ) -> Result<(), ExchangeError>; 19 | 20 | /// Fetch historical trade data for a specific symbol and time range 21 | async fn get_historical_trades( 22 | &self, 23 | params: HistoricalTradeParams, 24 | ) -> Result, ExchangeError>; 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Harrison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /trading-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trading-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tokio = { version = "1.0", features = ["full"] } 8 | tokio-tungstenite = { version = "0.20", features = ["native-tls"] } 9 | futures-util = "0.3" 10 | reqwest = { version = "0.11", features = ["json", "blocking"] } 11 | sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "rust_decimal"] } 12 | rust_decimal = { version = "1.32", features = ["serde"] } 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | config = "0.13" 16 | tracing = "0.1" 17 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 18 | dotenv = "0.15" 19 | chrono = { version = "0.4.35", features = ["serde"] } 20 | uuid = { version = "1.0", features = ["serde", "v4"] } 21 | anyhow = "1.0" 22 | thiserror = "1.0" 23 | async-trait = "0.1" 24 | clap = { version = "4.4", features = ["derive"] } 25 | rust_decimal_macros = "1.8" 26 | redis = "0.23.0" 27 | 28 | [dev-dependencies] 29 | criterion = { version = "0.5", features = ["async_tokio"] } 30 | 31 | [[bench]] 32 | name = "repository_bench" 33 | harness = false -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-popover": "^1.1.4", 14 | "@radix-ui/react-slot": "^1.2.3", 15 | "@tauri-apps/api": "^2.8.0", 16 | "@tauri-apps/plugin-shell": "^2.3.0", 17 | "class-variance-authority": "^0.7.1", 18 | "clsx": "^2.1.1", 19 | "date-fns": "^3.6.0", 20 | "lightweight-charts": "^4.2.2", 21 | "lucide-react": "^0.469.0", 22 | "next": "15.1.2", 23 | "react": "^18.3.1", 24 | "react-day-picker": "^8.10.1", 25 | "react-dom": "^18.3.1", 26 | "recharts": "^2.15.0", 27 | "tailwind-merge": "^2.6.0", 28 | "tailwindcss-animate": "^1.0.7" 29 | }, 30 | "devDependencies": { 31 | "@eslint/eslintrc": "^3", 32 | "@tauri-apps/cli": "^2", 33 | "@types/node": "^20", 34 | "@types/react": "^19", 35 | "@types/react-dom": "^19", 36 | "eslint": "^9", 37 | "eslint-config-next": "15.1.2", 38 | "postcss": "^8", 39 | "tailwindcss": "^3.4.1", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /trading-core/src/service/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::data::types::DataError; 2 | use crate::exchange::ExchangeError; 3 | use thiserror::Error; 4 | 5 | /// Service layer error types 6 | #[derive(Error, Debug)] 7 | pub enum ServiceError { 8 | #[error("Exchange error: {0}")] 9 | Exchange(#[from] ExchangeError), 10 | 11 | #[error("Data error: {0}")] 12 | Data(#[from] DataError), 13 | 14 | #[error("Configuration error: {0}")] 15 | Config(String), 16 | 17 | #[error("Task error: {0}")] 18 | Task(String), 19 | 20 | #[error("Validation error: {0}")] 21 | Validation(String), 22 | 23 | #[error("Service shutdown")] 24 | Shutdown, 25 | 26 | #[error("Retry limit exceeded: {0}")] 27 | RetryLimitExceeded(String), 28 | } 29 | 30 | impl ServiceError { 31 | /// Check if error is recoverable 32 | pub fn is_recoverable(&self) -> bool { 33 | match self { 34 | ServiceError::Exchange(e) => true, 35 | ServiceError::Data(_) => true, // Data errors are usually recoverable 36 | ServiceError::Task(_) => true, 37 | ServiceError::Shutdown => false, 38 | ServiceError::Config(_) => false, 39 | ServiceError::Validation(_) => false, 40 | ServiceError::RetryLimitExceeded(_) => false, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /trading-core/src/backtest/strategy/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod base; 2 | mod rsi; 3 | mod sma; 4 | 5 | pub use base::{Signal, Strategy}; 6 | use rsi::RsiStrategy; 7 | use sma::SmaStrategy; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct StrategyInfo { 11 | pub id: String, 12 | pub name: String, 13 | pub description: String, 14 | } 15 | 16 | pub fn create_strategy(strategy_id: &str) -> Result, String> { 17 | match strategy_id { 18 | "sma" => Ok(Box::new(SmaStrategy::new())), 19 | "rsi" => Ok(Box::new(RsiStrategy::new())), 20 | _ => Err(format!("Unknown strategy: {}", strategy_id)), 21 | } 22 | } 23 | 24 | pub fn list_strategies() -> Vec { 25 | vec![ 26 | StrategyInfo { 27 | id: "sma".to_string(), 28 | name: "Simple Moving Average".to_string(), 29 | description: "Trading strategy based on short and long-term moving average crossover" 30 | .to_string(), 31 | }, 32 | StrategyInfo { 33 | id: "rsi".to_string(), 34 | name: "RSI Strategy".to_string(), 35 | description: "Trading strategy based on Relative Strength Index (RSI)".to_string(), 36 | }, 37 | ] 38 | } 39 | 40 | pub fn get_strategy_info(strategy_id: &str) -> Option { 41 | list_strategies() 42 | .into_iter() 43 | .find(|info| info.id == strategy_id) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /trading-core/src/exchange/errors.rs: -------------------------------------------------------------------------------- 1 | // ================================================================= 2 | // exchange/errors.rs - Error Types 3 | // ================================================================= 4 | 5 | use thiserror::Error; 6 | 7 | /// Error types for exchange operations 8 | #[derive(Error, Debug)] 9 | pub enum ExchangeError { 10 | #[error("Network error: {0}")] 11 | NetworkError(String), 12 | 13 | #[error("WebSocket error: {0}")] 14 | WebSocketError(String), 15 | 16 | #[error("Rate limit exceeded: {0}")] 17 | RateLimit(String), 18 | 19 | #[error("Invalid symbol: {0}")] 20 | InvalidSymbol(String), 21 | 22 | #[error("Data parsing error: {0}")] 23 | ParseError(String), 24 | 25 | #[error("Connection timeout")] 26 | Timeout, 27 | 28 | #[error("Exchange API error: {0}")] 29 | ApiError(String), 30 | } 31 | 32 | // Convert from common error types 33 | impl From for ExchangeError { 34 | fn from(err: serde_json::Error) -> Self { 35 | ExchangeError::ParseError(err.to_string()) 36 | } 37 | } 38 | 39 | impl From for ExchangeError { 40 | fn from(err: tokio_tungstenite::tungstenite::Error) -> Self { 41 | ExchangeError::WebSocketError(err.to_string()) 42 | } 43 | } 44 | 45 | impl From for ExchangeError { 46 | fn from(err: reqwest::Error) -> Self { 47 | if err.is_timeout() { 48 | ExchangeError::Timeout 49 | } else if err.is_connect() { 50 | ExchangeError::NetworkError(err.to_string()) 51 | } else { 52 | ExchangeError::ApiError(err.to_string()) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/types/backtest.ts: -------------------------------------------------------------------------------- 1 | // src/types/backtest.ts 2 | export interface DataInfoResponse { 3 | total_records: number; 4 | symbols_count: number; 5 | earliest_time?: string; 6 | latest_time?: string; 7 | symbol_info: SymbolInfo[]; 8 | } 9 | 10 | export interface SymbolInfo { 11 | symbol: string; 12 | records_count: number; 13 | earliest_time?: string; 14 | latest_time?: string; 15 | min_price?: string; 16 | max_price?: string; 17 | } 18 | 19 | export interface StrategyInfo { 20 | id: string; 21 | name: string; 22 | description: string; 23 | } 24 | 25 | export interface BacktestRequest { 26 | strategy_id: string; 27 | symbol: string; 28 | data_count: number; 29 | initial_capital: string; 30 | commission_rate: string; 31 | strategy_params: Record; 32 | } 33 | 34 | export interface BacktestResponse { 35 | strategy_name: string; 36 | initial_capital: string; 37 | final_value: string; 38 | total_pnl: string; 39 | return_percentage: string; 40 | total_trades: number; 41 | winning_trades: number; 42 | losing_trades: number; 43 | max_drawdown: string; 44 | sharpe_ratio: string; 45 | volatility: string; 46 | win_rate: string; 47 | profit_factor: string; 48 | total_commission: string; 49 | trades: TradeInfo[]; 50 | equity_curve: string[]; 51 | data_source: string; 52 | } 53 | 54 | export interface TradeInfo { 55 | timestamp: string; 56 | symbol: string; 57 | side: string; 58 | quantity: string; 59 | price: string; 60 | realized_pnl?: string; 61 | commission: string; 62 | } 63 | 64 | export interface HistoricalDataRequest { 65 | symbol: string; 66 | limit?: number; 67 | } 68 | 69 | export interface TickDataResponse { 70 | timestamp: string; 71 | symbol: string; 72 | price: string; 73 | quantity: string; 74 | side: string; 75 | } -------------------------------------------------------------------------------- /trading-core/src/service/types.rs: -------------------------------------------------------------------------------- 1 | use crate::data::types::TickData; 2 | use chrono::{DateTime, Utc}; 3 | 4 | /// Batch processing configuration 5 | #[derive(Debug, Clone)] 6 | pub struct BatchConfig { 7 | /// Maximum number of ticks in a batch 8 | pub max_batch_size: usize, 9 | /// Maximum time to wait before flushing batch (in seconds) 10 | pub max_batch_time: u64, 11 | /// Maximum retry attempts for failed batches 12 | pub max_retry_attempts: u32, 13 | /// Delay between retry attempts (in milliseconds) 14 | pub retry_delay_ms: u64, 15 | } 16 | 17 | impl Default for BatchConfig { 18 | fn default() -> Self { 19 | Self { 20 | max_batch_size: 100, 21 | max_batch_time: 1, 22 | max_retry_attempts: 3, 23 | retry_delay_ms: 1000, 24 | } 25 | } 26 | } 27 | 28 | /// Batch processing statistics 29 | #[derive(Debug, Clone, Default)] 30 | pub struct BatchStats { 31 | /// Total ticks processed 32 | pub total_ticks_processed: u64, 33 | /// Total batches flushed 34 | pub total_batches_flushed: u64, 35 | /// Total retry attempts 36 | pub total_retry_attempts: u64, 37 | /// Failed batches (after all retries) 38 | pub total_failed_batches: u64, 39 | /// Cache update failures 40 | pub cache_update_failures: u64, 41 | /// Last flush time 42 | pub last_flush_time: Option>, 43 | } 44 | 45 | /// Data processing metrics 46 | #[derive(Debug, Clone)] 47 | pub struct ProcessingMetrics { 48 | /// Ticks per second 49 | pub ticks_per_second: f64, 50 | /// Current batch size 51 | pub current_batch_size: usize, 52 | /// Time since last batch flush 53 | pub time_since_last_flush: Option, 54 | /// Overall processing statistics 55 | pub batch_stats: BatchStats, 56 | } 57 | -------------------------------------------------------------------------------- /frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ['class'], 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/features/**/*.{js,ts,jsx,tsx,mdx}', 9 | ], 10 | theme: { 11 | extend: { 12 | borderRadius: { 13 | lg: 'var(--radius)', 14 | md: 'calc(var(--radius) - 2px)', 15 | sm: 'calc(var(--radius) - 4px)' 16 | }, 17 | colors: { 18 | background: 'hsl(var(--background))', 19 | foreground: 'hsl(var(--foreground))', 20 | card: { 21 | DEFAULT: 'hsl(var(--card))', 22 | foreground: 'hsl(var(--card-foreground))' 23 | }, 24 | popover: { 25 | DEFAULT: 'hsl(var(--popover))', 26 | foreground: 'hsl(var(--popover-foreground))' 27 | }, 28 | primary: { 29 | DEFAULT: 'hsl(var(--primary))', 30 | foreground: 'hsl(var(--primary-foreground))' 31 | }, 32 | secondary: { 33 | DEFAULT: 'hsl(var(--secondary))', 34 | foreground: 'hsl(var(--secondary-foreground))' 35 | }, 36 | muted: { 37 | DEFAULT: 'hsl(var(--muted))', 38 | foreground: 'hsl(var(--muted-foreground))' 39 | }, 40 | accent: { 41 | DEFAULT: 'hsl(var(--accent))', 42 | foreground: 'hsl(var(--accent-foreground))' 43 | }, 44 | destructive: { 45 | DEFAULT: 'hsl(var(--destructive))', 46 | foreground: 'hsl(var(--destructive-foreground))' 47 | }, 48 | border: 'hsl(var(--border))', 49 | input: 'hsl(var(--input))', 50 | ring: 'hsl(var(--ring))', 51 | chart: { 52 | '1': 'hsl(var(--chart-1))', 53 | '2': 'hsl(var(--chart-2))', 54 | '3': 'hsl(var(--chart-3))', 55 | '4': 'hsl(var(--chart-4))', 56 | '5': 'hsl(var(--chart-5))' 57 | } 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | } -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 0 0% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 0 0% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 0 0% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 0 0% 9%; 46 | --secondary: 0 0% 14.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 0 0% 14.9%; 49 | --muted-foreground: 0 0% 63.9%; 50 | --accent: 0 0% 14.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 0 0% 14.9%; 55 | --input: 0 0% 14.9%; 56 | --ring: 0 0% 83.1%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /trading-core/src/config.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, ConfigError, File}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Debug, Deserialize)] 5 | pub struct Database { 6 | pub url: String, 7 | pub max_connections: u32, 8 | pub min_connections: u32, 9 | pub max_lifetime: u64, 10 | } 11 | 12 | #[derive(Debug, Deserialize)] 13 | pub struct Server { 14 | pub host: String, 15 | pub port: u32, 16 | } 17 | 18 | #[derive(Debug, Deserialize)] 19 | pub struct MemoryCache { 20 | pub max_ticks_per_symbol: usize, 21 | pub ttl_seconds: u64, 22 | } 23 | 24 | #[derive(Debug, Deserialize)] 25 | pub struct RedisCache { 26 | pub url: String, 27 | pub pool_size: u32, 28 | pub ttl_seconds: u64, 29 | pub max_ticks_per_symbol: usize, 30 | } 31 | 32 | #[derive(Debug, Deserialize)] 33 | pub struct Cache { 34 | pub memory: MemoryCache, 35 | pub redis: RedisCache, 36 | } 37 | 38 | #[derive(Debug, Deserialize)] 39 | pub struct PaperTrading { 40 | pub enabled: bool, 41 | pub strategy: String, 42 | pub initial_capital: f64, 43 | } 44 | 45 | #[derive(Debug, Deserialize)] 46 | pub struct Settings { 47 | pub database: Database, 48 | pub server: Server, 49 | pub cache: Cache, 50 | pub symbols: Vec, 51 | pub paper_trading: PaperTrading, 52 | } 53 | 54 | impl Settings { 55 | pub fn new() -> Result { 56 | let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); 57 | 58 | let mut builder = Config::builder() 59 | .add_source(File::with_name(&format!("../config/{}", run_mode)).required(true)); 60 | 61 | if let Ok(database_url) = std::env::var("DATABASE_URL") { 62 | builder = builder.set_override("database.url", database_url)?; 63 | } 64 | 65 | if let Ok(redis_url) = std::env::var("REDIS_URL") { 66 | builder = builder.set_override("cache.redis.url", redis_url)?; 67 | } 68 | 69 | let s = builder.build()?; 70 | s.try_deserialize() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /frontend/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /frontend/src/components/ui/datetime-picker.tsx: -------------------------------------------------------------------------------- 1 | // components/ui/datetime-picker.tsx 2 | "use client" 3 | 4 | import * as React from "react" 5 | import { Calendar as CalendarIcon } from "lucide-react" 6 | import { format } from "date-fns" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Button } from "@/components/ui/button" 10 | import { Calendar } from "@/components/ui/calendar" 11 | import { 12 | Popover, 13 | PopoverContent, 14 | PopoverTrigger, 15 | } from "@/components/ui/popover" 16 | import { Input } from "@/components/ui/input" 17 | 18 | interface DateTimePickerProps { 19 | date: Date 20 | setDate: (date: Date) => void 21 | } 22 | 23 | export function DateTimePicker({ date, setDate }: DateTimePickerProps) { 24 | const [selectedDateTime, setSelectedDateTime] = React.useState(date) 25 | 26 | const handleSelect = (date: Date | undefined) => { 27 | if (date) { 28 | const hours = selectedDateTime.getHours() 29 | const minutes = selectedDateTime.getMinutes() 30 | 31 | date.setHours(hours) 32 | date.setMinutes(minutes) 33 | 34 | setSelectedDateTime(date) 35 | setDate(date) 36 | } 37 | } 38 | 39 | const handleTimeChange = (e: React.ChangeEvent) => { 40 | const [hours, minutes] = e.target.value.split(':').map(Number) 41 | const newDate = new Date(selectedDateTime) 42 | newDate.setHours(hours) 43 | newDate.setMinutes(minutes) 44 | setSelectedDateTime(newDate) 45 | setDate(newDate) 46 | } 47 | 48 | return ( 49 |
50 | 51 | 52 | 62 | 63 | 64 | 70 | 71 | 72 | 78 |
79 | ) 80 | } -------------------------------------------------------------------------------- /trading-core/src/exchange/types.rs: -------------------------------------------------------------------------------- 1 | // ================================================================= 2 | // exchange/types.rs - Data Structures 3 | // ================================================================= 4 | 5 | use chrono::{DateTime, Utc}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Parameters for querying historical trade data 9 | #[derive(Debug, Clone)] 10 | pub struct HistoricalTradeParams { 11 | pub symbol: String, 12 | pub start_time: Option>, 13 | pub end_time: Option>, 14 | pub limit: Option, 15 | } 16 | 17 | impl HistoricalTradeParams { 18 | pub fn new(symbol: String) -> Self { 19 | Self { 20 | symbol: symbol.to_uppercase(), 21 | start_time: None, 22 | end_time: None, 23 | limit: None, 24 | } 25 | } 26 | 27 | pub fn with_time_range(mut self, start: DateTime, end: DateTime) -> Self { 28 | self.start_time = Some(start); 29 | self.end_time = Some(end); 30 | self 31 | } 32 | 33 | pub fn with_limit(mut self, limit: u32) -> Self { 34 | self.limit = Some(limit); 35 | self 36 | } 37 | } 38 | 39 | /// Binance specific trade message format 40 | #[derive(Debug, Deserialize, Clone)] 41 | pub struct BinanceTradeMessage { 42 | /// Symbol 43 | #[serde(rename = "s")] 44 | pub symbol: String, 45 | 46 | /// Trade ID 47 | #[serde(rename = "t")] 48 | pub trade_id: u64, 49 | 50 | /// Price 51 | #[serde(rename = "p")] 52 | pub price: String, 53 | 54 | /// Quantity 55 | #[serde(rename = "q")] 56 | pub quantity: String, 57 | 58 | /// Trade time 59 | #[serde(rename = "T")] 60 | pub trade_time: u64, 61 | 62 | /// Is the buyer the market maker? 63 | #[serde(rename = "m")] 64 | pub is_buyer_maker: bool, 65 | } 66 | 67 | /// Binance WebSocket stream wrapper for combined streams 68 | #[derive(Debug, Deserialize)] 69 | pub struct BinanceStreamMessage { 70 | /// Stream name (e.g., "btcusdt@trade") 71 | pub stream: String, 72 | 73 | /// The actual trade data 74 | pub data: BinanceTradeMessage, 75 | } 76 | 77 | /// Binance subscription message format 78 | #[derive(Debug, Serialize)] 79 | pub struct BinanceSubscribeMessage { 80 | pub method: String, 81 | pub params: Vec, 82 | pub id: u32, 83 | } 84 | 85 | impl BinanceSubscribeMessage { 86 | pub fn new(streams: Vec) -> Self { 87 | Self { 88 | method: "SUBSCRIBE".to_string(), 89 | params: streams, 90 | id: 1, 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /frontend/src/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | ( 58 | 59 | ), 60 | IconRight: ({ className, ...props }) => ( 61 | 62 | ), 63 | }} 64 | {...props} 65 | /> 66 | ) 67 | } 68 | Calendar.displayName = "Calendar" 69 | 70 | export { Calendar } 71 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | mod commands; 7 | mod state; 8 | mod types; 9 | 10 | use commands::*; 11 | use state::AppState; 12 | 13 | fn main() { 14 | if let Err(_) = dotenvy::dotenv() { 15 | println!("Warning: .env file not found, using environment variables"); 16 | } 17 | 18 | tracing_subscriber::fmt() 19 | .with_max_level(tracing::Level::INFO) 20 | .with_file(true) 21 | .with_line_number(true) 22 | .init(); 23 | 24 | tracing::info!("Trading Core Tauri Application starting..."); 25 | 26 | let runtime = match tokio::runtime::Runtime::new() { 27 | Ok(rt) => { 28 | tracing::info!("Tokio runtime created successfully"); 29 | rt 30 | } 31 | Err(e) => { 32 | tracing::error!("Failed to create Tokio runtime: {}", e); 33 | std::process::exit(1); 34 | } 35 | }; 36 | 37 | let app_state = runtime.block_on(async { 38 | match AppState::new().await { 39 | Ok(state) => { 40 | tracing::info!("App state initialized successfully"); 41 | state 42 | } 43 | Err(e) => { 44 | tracing::error!("Failed to initialize app state: {}", e); 45 | tracing::error!("Please check your configuration:"); 46 | tracing::error!("1. Ensure .env file exists with DATABASE_URL and REDIS_URL"); 47 | tracing::error!("2. Ensure PostgreSQL is running and accessible"); 48 | tracing::error!("3. Ensure Redis is running (optional but recommended)"); 49 | tracing::error!("4. Ensure trading_core database and tick_data table exist"); 50 | std::process::exit(1); 51 | } 52 | } 53 | }); 54 | 55 | let result = tauri::Builder::default() 56 | .manage(app_state) 57 | .invoke_handler(tauri::generate_handler![ 58 | get_data_info, 59 | get_available_strategies, 60 | run_backtest, 61 | get_historical_data, 62 | validate_backtest_config, 63 | get_strategy_capabilities, 64 | get_ohlc_preview 65 | ]) 66 | .setup(|app| { 67 | tracing::info!("Tauri setup started"); 68 | #[cfg(debug_assertions)] 69 | { 70 | let app_handle = app.handle(); 71 | if let Err(e) = app_handle.plugin(tauri_plugin_shell::init()) { 72 | tracing::warn!("Failed to initialize shell plugin: {}", e); 73 | } 74 | tracing::info!("Debug plugins initialized"); 75 | } 76 | tracing::info!("Tauri setup completed"); 77 | Ok(()) 78 | }) 79 | .run(tauri::generate_context!()); 80 | 81 | match result { 82 | Ok(_) => tracing::info!("Application exited normally"), 83 | Err(e) => { 84 | tracing::error!("Application error: {}", e); 85 | std::process::exit(1); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src-tauri/src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct DataInfoResponse { 6 | pub total_records: u64, 7 | pub symbols_count: u64, 8 | pub earliest_time: Option, 9 | pub latest_time: Option, 10 | pub symbol_info: Vec, 11 | } 12 | 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct SymbolInfo { 15 | pub symbol: String, 16 | pub records_count: u64, 17 | pub earliest_time: Option, 18 | pub latest_time: Option, 19 | pub min_price: Option, 20 | pub max_price: Option, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | pub struct StrategyInfo { 25 | pub id: String, 26 | pub name: String, 27 | pub description: String, 28 | } 29 | 30 | #[derive(Debug, Serialize, Deserialize)] 31 | pub struct BacktestRequest { 32 | pub strategy_id: String, 33 | pub symbol: String, 34 | pub data_count: i64, 35 | pub initial_capital: String, 36 | pub commission_rate: String, 37 | pub strategy_params: HashMap, 38 | } 39 | 40 | #[derive(Debug, Serialize, Deserialize)] 41 | pub struct BacktestResponse { 42 | pub strategy_name: String, 43 | pub initial_capital: String, 44 | pub final_value: String, 45 | pub total_pnl: String, 46 | pub return_percentage: String, 47 | pub total_trades: usize, 48 | pub winning_trades: usize, 49 | pub losing_trades: usize, 50 | pub max_drawdown: String, 51 | pub sharpe_ratio: String, 52 | pub volatility: String, 53 | pub win_rate: String, 54 | pub profit_factor: String, 55 | pub total_commission: String, 56 | pub trades: Vec, 57 | pub equity_curve: Vec, 58 | pub data_source: String, 59 | } 60 | 61 | #[derive(Debug, Serialize, Deserialize)] 62 | pub struct TradeInfo { 63 | pub timestamp: String, 64 | pub symbol: String, 65 | pub side: String, 66 | pub quantity: String, 67 | pub price: String, 68 | pub realized_pnl: Option, 69 | pub commission: String, 70 | } 71 | 72 | #[derive(Debug, Serialize, Deserialize)] 73 | pub struct HistoricalDataRequest { 74 | pub symbol: String, 75 | pub limit: Option, 76 | } 77 | 78 | #[derive(Debug, Serialize, Deserialize)] 79 | pub struct TickDataResponse { 80 | pub timestamp: String, 81 | pub symbol: String, 82 | pub price: String, 83 | pub quantity: String, 84 | pub side: String, 85 | } 86 | 87 | 88 | #[derive(Debug, Serialize, Deserialize)] 89 | pub struct StrategyCapability { 90 | pub id: String, 91 | pub name: String, 92 | pub description: String, 93 | pub supports_ohlc: bool, 94 | pub preferred_timeframe: Option, 95 | } 96 | 97 | #[derive(Debug, Serialize, Deserialize)] 98 | pub struct OHLCPreview { 99 | pub timestamp: String, 100 | pub symbol: String, 101 | pub open: String, 102 | pub high: String, 103 | pub low: String, 104 | pub close: String, 105 | pub volume: String, 106 | pub trade_count: u64, 107 | } 108 | 109 | #[derive(Debug, Serialize, Deserialize)] 110 | pub struct OHLCRequest { 111 | pub symbol: String, 112 | pub timeframe: String, 113 | pub count: u32, 114 | } -------------------------------------------------------------------------------- /src-tauri/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use trading_core::data::{repository::TickDataRepository, cache::TieredCache}; 3 | use sqlx::PgPool; 4 | use std::time::Duration; 5 | 6 | pub struct AppState { 7 | pub repository: Arc, 8 | } 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct DatabaseSettings { 12 | pub database_url: String, 13 | pub redis_url: String, 14 | pub max_connections: u32, 15 | pub min_connections: u32, 16 | pub max_lifetime: u64, 17 | } 18 | 19 | impl AppState { 20 | pub async fn new() -> Result> { 21 | tracing::info!("Initializing Trading Core application state..."); 22 | 23 | let settings = create_settings_from_env()?; 24 | tracing::info!("Configuration loaded successfully"); 25 | 26 | let pool = create_database_pool(&settings).await?; 27 | tracing::info!("Database connection established"); 28 | 29 | test_database_connection(&pool).await?; 30 | tracing::info!("Database validation passed"); 31 | 32 | let cache = create_gui_cache(&settings).await?; 33 | tracing::info!("Cache initialized"); 34 | 35 | let repository = TickDataRepository::new(pool, cache); 36 | 37 | Ok(Self { 38 | repository: Arc::new(repository), 39 | }) 40 | } 41 | } 42 | 43 | fn create_settings_from_env() -> Result> { 44 | let database_url = std::env::var("DATABASE_URL") 45 | .map_err(|_| "DATABASE_URL environment variable is required")?; 46 | 47 | let redis_url = std::env::var("REDIS_URL") 48 | .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); 49 | 50 | Ok(DatabaseSettings { 51 | database_url, 52 | redis_url, 53 | max_connections: 5, 54 | min_connections: 1, 55 | max_lifetime: 1800, 56 | }) 57 | } 58 | 59 | async fn create_database_pool(settings: &DatabaseSettings) -> Result> { 60 | let pool = sqlx::postgres::PgPoolOptions::new() 61 | .max_connections(settings.max_connections) 62 | .min_connections(settings.min_connections) 63 | .max_lifetime(Duration::from_secs(settings.max_lifetime)) 64 | .acquire_timeout(Duration::from_secs(30)) 65 | .idle_timeout(Duration::from_secs(600)) 66 | .connect(&settings.database_url) 67 | .await?; 68 | 69 | Ok(pool) 70 | } 71 | 72 | async fn test_database_connection(pool: &PgPool) -> Result<(), Box> { 73 | sqlx::query("SELECT 1") 74 | .execute(pool) 75 | .await?; 76 | 77 | let table_exists = sqlx::query_scalar::<_, bool>( 78 | "SELECT EXISTS ( 79 | SELECT FROM information_schema.tables 80 | WHERE table_schema = 'public' 81 | AND table_name = 'tick_data' 82 | )" 83 | ) 84 | .fetch_one(pool) 85 | .await?; 86 | 87 | if !table_exists { 88 | tracing::error!("Required table 'tick_data' does not exist in database"); 89 | return Err("Database schema validation failed: tick_data table not found".into()); 90 | } 91 | 92 | tracing::info!("Database schema validation passed"); 93 | Ok(()) 94 | } 95 | 96 | async fn create_gui_cache(settings: &DatabaseSettings) -> Result> { 97 | let memory_config = (50, 300); 98 | let redis_config = ( 99 | settings.redis_url.as_str(), 100 | 100, 101 | 600 102 | ); 103 | 104 | match TieredCache::new(memory_config, redis_config).await { 105 | Ok(cache) => { 106 | tracing::info!("Cache initialized successfully"); 107 | Ok(cache) 108 | }, 109 | Err(e) => { 110 | tracing::warn!("Failed to initialize full cache, using minimal cache: {}", e); 111 | create_minimal_cache().await 112 | } 113 | } 114 | } 115 | 116 | async fn create_minimal_cache() -> Result> { 117 | let memory_config = (10, 60); 118 | let redis_config = ("redis://127.0.0.1:6379", 10, 60); 119 | 120 | TieredCache::new(memory_config, redis_config).await 121 | .map_err(|e| e.into()) 122 | } -------------------------------------------------------------------------------- /trading-core/src/exchange/utils.rs: -------------------------------------------------------------------------------- 1 | // ================================================================= 2 | // exchange/utils.rs - Utility Functions 3 | // ================================================================= 4 | 5 | use super::{BinanceTradeMessage, ExchangeError}; 6 | use crate::data::types::{TickData, TradeSide}; 7 | use chrono::{DateTime, Utc}; 8 | use rust_decimal::Decimal; 9 | use std::str::FromStr; 10 | 11 | /// Convert Binance trade message to standard TickData format 12 | pub fn convert_binance_to_tick_data(msg: BinanceTradeMessage) -> Result { 13 | // Convert timestamp from milliseconds to DateTime 14 | let timestamp = DateTime::from_timestamp_millis(msg.trade_time as i64) 15 | .ok_or_else(|| ExchangeError::ParseError("Invalid timestamp".to_string()))?; 16 | 17 | // Parse price and quantity as Decimal for precision 18 | let price = Decimal::from_str(&msg.price) 19 | .map_err(|e| ExchangeError::ParseError(format!("Invalid price '{}': {}", msg.price, e)))?; 20 | 21 | let quantity = Decimal::from_str(&msg.quantity).map_err(|e| { 22 | ExchangeError::ParseError(format!("Invalid quantity '{}': {}", msg.quantity, e)) 23 | })?; 24 | 25 | // Validate parsed values 26 | if price <= Decimal::ZERO { 27 | return Err(ExchangeError::ParseError( 28 | "Price must be positive".to_string(), 29 | )); 30 | } 31 | 32 | if quantity <= Decimal::ZERO { 33 | return Err(ExchangeError::ParseError( 34 | "Quantity must be positive".to_string(), 35 | )); 36 | } 37 | 38 | // Determine trade side based on maker flag 39 | // If buyer is maker, it means a sell order was filled (seller was taker) 40 | // If buyer is not maker, it means a buy order was filled (buyer was taker) 41 | let side = if msg.is_buyer_maker { 42 | TradeSide::Sell 43 | } else { 44 | TradeSide::Buy 45 | }; 46 | 47 | Ok(TickData::new( 48 | timestamp, 49 | msg.symbol, 50 | price, 51 | quantity, 52 | side, 53 | msg.trade_id.to_string(), 54 | msg.is_buyer_maker, 55 | )) 56 | } 57 | 58 | /// Validate symbol format for Binance 59 | pub fn validate_binance_symbol(symbol: &str) -> Result { 60 | if symbol.is_empty() { 61 | return Err(ExchangeError::InvalidSymbol( 62 | "Symbol cannot be empty".to_string(), 63 | )); 64 | } 65 | 66 | let symbol = symbol.to_uppercase(); 67 | 68 | // Basic validation: should be alphanumeric and reasonable length 69 | if !symbol.chars().all(char::is_alphanumeric) { 70 | return Err(ExchangeError::InvalidSymbol(format!( 71 | "Symbol '{}' contains invalid characters", 72 | symbol 73 | ))); 74 | } 75 | 76 | if symbol.len() < 3 || symbol.len() > 20 { 77 | return Err(ExchangeError::InvalidSymbol(format!( 78 | "Symbol '{}' has invalid length", 79 | symbol 80 | ))); 81 | } 82 | 83 | Ok(symbol) 84 | } 85 | 86 | /// Build WebSocket subscription streams for Binance 87 | pub fn build_binance_trade_streams(symbols: &[String]) -> Result, ExchangeError> { 88 | if symbols.is_empty() { 89 | return Err(ExchangeError::InvalidSymbol( 90 | "No symbols provided".to_string(), 91 | )); 92 | } 93 | 94 | let mut streams = Vec::with_capacity(symbols.len()); 95 | 96 | for symbol in symbols { 97 | let validated_symbol = validate_binance_symbol(symbol)?; 98 | streams.push(format!("{}@trade", validated_symbol.to_lowercase())); 99 | } 100 | 101 | Ok(streams) 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use super::*; 107 | 108 | #[test] 109 | fn test_symbol_validation() { 110 | assert!(validate_binance_symbol("BTCUSDT").is_ok()); 111 | assert!(validate_binance_symbol("btcusdt").is_ok()); 112 | assert!(validate_binance_symbol("").is_err()); 113 | assert!(validate_binance_symbol("BTC-USDT").is_err()); 114 | } 115 | 116 | #[test] 117 | fn test_stream_building() { 118 | let symbols = vec!["BTCUSDT".to_string(), "ETHUSDT".to_string()]; 119 | let streams = build_binance_trade_streams(&symbols).unwrap(); 120 | 121 | assert_eq!(streams.len(), 2); 122 | assert_eq!(streams[0], "btcusdt@trade"); 123 | assert_eq!(streams[1], "ethusdt@trade"); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src-tauri/README.md: -------------------------------------------------------------------------------- 1 | # Trading Desktop 2 | 3 | A Tauri-based desktop application for cryptocurrency backtesting, built on top of Trading Core. 4 | 5 | ## 🏗️ Architecture 6 | 7 | ### **Project Structure** 8 | ``` 9 | src-tauri/ 10 | ├── src/ 11 | │ ├── main.rs # Application entry point and setup 12 | │ ├── state.rs # Application state management and database initialization 13 | │ ├── types.rs # Frontend interface types and serialization 14 | │ └── commands.rs # Tauri command handlers for frontend communication 15 | ├── Cargo.toml # Dependencies and Tauri configuration 16 | └── .env # Environment configuration 17 | ``` 18 | 19 | ### **Core Components** 20 | 21 | #### **State Management (`state.rs`)** 22 | - **AppState**: Manages Trading Core repository instance 23 | - **Database Connection**: PostgreSQL connection pool management 24 | - **Cache Initialization**: Simplified caching for GUI applications 25 | - **Configuration Loading**: Environment-based settings with fallbacks 26 | 27 | #### **Command Handlers (`commands.rs`)** 28 | - **`get_data_info`**: Retrieve database statistics and available symbols 29 | - **`get_available_strategies`**: List all implemented trading strategies 30 | - **`validate_backtest_config`**: Validate backtest parameters before execution 31 | - **`get_historical_data`**: Preview historical data for selected symbols 32 | - **`run_backtest`**: Execute complete backtesting with strategy and parameters 33 | 34 | #### **Type Definitions (`types.rs`)** 35 | - **Request Types**: Structured input from frontend (BacktestRequest, HistoricalDataRequest) 36 | - **Response Types**: Formatted output to frontend (BacktestResponse, DataInfoResponse) 37 | - **Serde Integration**: JSON serialization for seamless frontend communication 38 | 39 | ## 🚀 Features 40 | 41 | ### **Backtesting Capabilities** 42 | - **Strategy Selection**: Choose from built-in SMA and RSI strategies 43 | - **Parameter Configuration**: Customizable strategy parameters via GUI 44 | - **Historical Data Access**: Direct access to Trading Core's tick data 45 | - **Performance Metrics**: Comprehensive analysis including Sharpe ratio, drawdown, win rate 46 | - **Trade Analysis**: Detailed trade-by-trade breakdown with P&L tracking 47 | 48 | ### **Data Management** 49 | - **Real-time Validation**: Parameter validation before backtest execution 50 | - **Data Statistics**: Database overview with symbol information and date ranges 51 | - **Error Handling**: Robust error propagation from backend to frontend 52 | 53 | ## ⚙️ Configuration 54 | 55 | ### **Environment Variables** 56 | ```bash 57 | DATABASE_URL=postgresql://username:password@localhost/trading_core 58 | REDIS_URL=redis://127.0.0.1:6379 59 | ``` 60 | 61 | ### **Dependencies** 62 | - **Tauri 2.0**: Desktop application framework 63 | - **Trading Core**: Backend trading infrastructure 64 | - **SQLx**: Database connectivity 65 | - **Serde**: JSON serialization 66 | - **Tokio**: Async runtime 67 | 68 | ## 🔧 Development 69 | 70 | ### **Setup** 71 | ```bash 72 | # Install dependencies 73 | cargo build 74 | 75 | # Run in development mode 76 | cargo tauri dev 77 | 78 | # Build for production 79 | cargo tauri build 80 | ``` 81 | 82 | ### **Requirements** 83 | - Trading Core project at `../trading-core` 84 | - PostgreSQL with `trading_core` database 85 | - Redis server (optional but recommended) 86 | - Rust 1.70+ 87 | 88 | ## 📊 API Interface 89 | 90 | ### **Frontend Commands** 91 | ```typescript 92 | // Get database information 93 | invoke('get_data_info') 94 | 95 | // Run backtest 96 | invoke('run_backtest', { 97 | request: BacktestRequest 98 | }) 99 | 100 | // Validate configuration 101 | invoke('validate_backtest_config', { 102 | symbol: string, 103 | data_count: number 104 | }) 105 | ``` 106 | 107 | ### **Data Flow** 108 | ``` 109 | Frontend → Tauri Commands → Trading Core Repository → PostgreSQL 110 | ↓ 111 | Frontend ← JSON Response ← Backtest Engine ← Historical Data 112 | ``` 113 | 114 | ## 🎯 Integration 115 | 116 | This Tauri application serves as the desktop GUI layer for Trading Core, providing: 117 | - **Seamless Integration**: Direct access to Trading Core's backtesting engine 118 | - **Type Safety**: Rust-based backend with TypeScript frontend compatibility 119 | - **Performance**: Native desktop performance with web-based UI flexibility 120 | - **Cross-Platform**: Windows, macOS, and Linux support through Tauri 121 | 122 | The application maintains full compatibility with Trading Core's CLI interface while offering an enhanced user experience through its graphical interface. -------------------------------------------------------------------------------- /trading-core/src/backtest/strategy/sma.rs: -------------------------------------------------------------------------------- 1 | use super::base::{Signal, Strategy}; 2 | use crate::data::types::{OHLCData, TickData}; 3 | use rust_decimal::Decimal; 4 | use std::collections::{HashMap, VecDeque}; 5 | 6 | pub struct SmaStrategy { 7 | short_period: usize, 8 | long_period: usize, 9 | prices: VecDeque, 10 | last_signal: Option, 11 | } 12 | 13 | impl SmaStrategy { 14 | pub fn new() -> Self { 15 | Self { 16 | short_period: 5, 17 | long_period: 20, 18 | prices: VecDeque::new(), 19 | last_signal: None, 20 | } 21 | } 22 | 23 | fn calculate_sma(&self, period: usize) -> Option { 24 | if self.prices.len() < period { 25 | return None; 26 | } 27 | let sum: Decimal = self.prices.iter().rev().take(period).sum(); 28 | Some(sum / Decimal::from(period)) 29 | } 30 | } 31 | 32 | impl Strategy for SmaStrategy { 33 | fn name(&self) -> &str { 34 | "Simple Moving Average" 35 | } 36 | 37 | fn initialize(&mut self, params: HashMap) -> Result<(), String> { 38 | if let Some(short) = params.get("short_period") { 39 | self.short_period = short.parse().map_err(|_| "Invalid short_period")?; 40 | } 41 | if let Some(long) = params.get("long_period") { 42 | self.long_period = long.parse().map_err(|_| "Invalid long_period")?; 43 | } 44 | 45 | if self.short_period >= self.long_period { 46 | return Err("Short period must be less than long period".to_string()); 47 | } 48 | 49 | println!( 50 | "SMA Strategy initialized: short={}, long={}", 51 | self.short_period, self.long_period 52 | ); 53 | Ok(()) 54 | } 55 | 56 | fn reset(&mut self) { 57 | self.prices.clear(); 58 | self.last_signal = None; 59 | } 60 | 61 | fn on_tick(&mut self, tick: &TickData) -> Signal { 62 | self.prices.push_back(tick.price); 63 | 64 | // Keep reasonable history length 65 | if self.prices.len() > self.long_period * 2 { 66 | self.prices.pop_front(); 67 | } 68 | 69 | if let (Some(short_sma), Some(long_sma)) = ( 70 | self.calculate_sma(self.short_period), 71 | self.calculate_sma(self.long_period), 72 | ) { 73 | // Golden cross: short MA crosses above long MA 74 | if short_sma > long_sma && !matches!(self.last_signal, Some(Signal::Buy { .. })) { 75 | let signal = Signal::Buy { 76 | symbol: tick.symbol.clone(), 77 | quantity: Decimal::from(100), 78 | }; 79 | self.last_signal = Some(signal.clone()); 80 | return signal; 81 | } 82 | // Death cross: short MA crosses below long MA 83 | else if short_sma < long_sma && matches!(self.last_signal, Some(Signal::Buy { .. })) { 84 | let signal = Signal::Sell { 85 | symbol: tick.symbol.clone(), 86 | quantity: Decimal::from(100), 87 | }; 88 | self.last_signal = Some(signal.clone()); 89 | return signal; 90 | } 91 | } 92 | 93 | Signal::Hold 94 | } 95 | 96 | fn on_ohlc(&mut self, ohlc: &OHLCData) -> Signal { 97 | self.prices.push_back(ohlc.close); 98 | 99 | if self.prices.len() > self.long_period * 2 { 100 | self.prices.pop_front(); 101 | } 102 | 103 | if let (Some(short_sma), Some(long_sma)) = ( 104 | self.calculate_sma(self.short_period), 105 | self.calculate_sma(self.long_period), 106 | ) { 107 | if short_sma > long_sma && !matches!(self.last_signal, Some(Signal::Buy { .. })) { 108 | let signal = Signal::Buy { 109 | symbol: ohlc.symbol.clone(), 110 | quantity: Decimal::from(100), 111 | }; 112 | self.last_signal = Some(signal.clone()); 113 | return signal; 114 | } else if short_sma < long_sma && matches!(self.last_signal, Some(Signal::Buy { .. })) { 115 | let signal = Signal::Sell { 116 | symbol: ohlc.symbol.clone(), 117 | quantity: Decimal::from(100), 118 | }; 119 | self.last_signal = Some(signal.clone()); 120 | return signal; 121 | } 122 | } 123 | 124 | Signal::Hold 125 | } 126 | 127 | fn supports_ohlc(&self) -> bool { 128 | false 129 | } 130 | fn preferred_timeframe(&self) -> Option { 131 | Some(crate::data::types::Timeframe::OneMinute) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /config/schema.sql: -------------------------------------------------------------------------------- 1 | -- ================================================================= 2 | -- Core Data Table for Quantitative Trading System: Tick Data 3 | -- Design Principles: Single table storage, high-performance queries, data integrity 4 | -- ================================================================= 5 | 6 | CREATE TABLE tick_data ( 7 | -- 【Timestamp】UTC time, supports millisecond precision 8 | -- Why use TIMESTAMP WITH TIME ZONE: 9 | -- 1. Global markets require a unified timezone (UTC) 10 | -- 2. Supports millisecond-level precision for high-frequency trading needs 11 | -- 3. Time zone info avoids issues like daylight saving time 12 | timestamp TIMESTAMP WITH TIME ZONE NOT NULL, 13 | 14 | -- 【Trading Pair】e.g., 'BTCUSDT', 'ETHUSDT' 15 | -- Why use VARCHAR(20): 16 | -- 1. Cryptocurrency trading pairs are typically 8-15 characters 17 | -- 2. Reserved space for future new trading pairs 18 | -- 3. Fixed length storage offers better performance 19 | symbol VARCHAR(20) NOT NULL, 20 | 21 | -- 【Trade Price】Use DECIMAL to ensure precision 22 | -- Why use DECIMAL(20, 8): 23 | -- 1. Total 20 digits: supports prices in the trillions 24 | -- 2. 8 decimal places: meets cryptocurrency precision requirements (Bitcoin has 8 decimals) 25 | -- 3. Avoids floating point precision loss 26 | price DECIMAL(20, 8) NOT NULL, 27 | 28 | -- 【Trade Quantity】Also uses DECIMAL to ensure precision 29 | -- Why use DECIMAL(20, 8): 30 | -- 1. Trade volume calculations require high precision 31 | -- 2. Consistent precision with price 32 | quantity DECIMAL(20, 8) NOT NULL, 33 | 34 | -- 【Trade Side】Buy or Sell 35 | -- Why use VARCHAR(4) + CHECK constraint: 36 | -- 1. 'BUY'/'SELL' is more intuitive than boolean 37 | -- 2. CHECK constraint enforces data validity 38 | -- 3. Facilitates SQL querying and reporting 39 | side VARCHAR(4) NOT NULL CHECK (side IN ('BUY', 'SELL')), 40 | 41 | -- 【Trade ID】Original trade identifier from exchange 42 | -- Why use VARCHAR(50): 43 | -- 1. Different exchanges have different ID formats (numeric, alphanumeric, UUID, etc.) 44 | -- 2. Used for deduplication and traceability 45 | -- 3. Supports various exchange ID lengths 46 | trade_id VARCHAR(50) NOT NULL, 47 | 48 | -- 【Maker Flag】Whether the buyer is the maker (order placer) 49 | -- Why this field is needed: 50 | -- 1. Distinguish between aggressive and passive trades 51 | -- 2. Calculate market liquidity metrics 52 | -- 3. Basis for fee calculation 53 | is_buyer_maker BOOLEAN NOT NULL 54 | ); 55 | 56 | -- ================================================================= 57 | -- Index Strategy: Optimized for different query scenarios 58 | -- Principle: Balance query performance and write efficiency 59 | -- ================================================================= 60 | 61 | -- 【Index 1】Real-time trading query index 62 | -- Use cases: 63 | -- - Fetch the latest price for a trading pair: WHERE symbol = 'BTCUSDT' ORDER BY timestamp DESC LIMIT 1 64 | -- - Get recent N minutes data of a trading pair: WHERE symbol = 'BTCUSDT' AND timestamp >= NOW() - INTERVAL '5 minutes' 65 | -- - Real-time price push, risk control checks, and other high-frequency operations 66 | -- Design notes: 67 | -- - Composite index (symbol, timestamp DESC): group by symbol first, then order by time descending 68 | -- - DESC order: prioritizes newest data, aligns with real-time query needs 69 | -- - Supports index-only scans to avoid heap fetches and improve performance 70 | CREATE INDEX idx_tick_symbol_time ON tick_data(symbol, timestamp DESC); 71 | 72 | -- 【Index 2】Data integrity unique index 73 | -- Use cases: 74 | -- - Prevent duplicate data insertion due to network retransmission or program restart (idempotency) 75 | -- - Data consistency checks to ensure no duplicated trade records 76 | -- Design notes: 77 | -- - Unique constraint on three fields: same symbol + same trade_id + same timestamp = unique record 78 | -- - Unique constraint implicitly creates corresponding unique index to support fast duplicate checks 79 | -- - Business logic aligns with financial system requirement of no duplicate and no missing data 80 | CREATE UNIQUE INDEX idx_tick_unique ON tick_data(symbol, trade_id, timestamp); 81 | 82 | -- 【Index 3】Backtesting time index 83 | -- Use cases: 84 | -- - Multi-symbol backtesting: WHERE timestamp BETWEEN '2025-01-01' AND '2025-01-02' AND symbol IN (...) 85 | -- - Market-wide statistics: WHERE timestamp >= '2025-01-01' GROUP BY symbol 86 | -- - Time-range data export: batch processing historical data by time intervals 87 | -- Design notes: 88 | -- - Single-column time index: more efficient than composite index when queries do not filter by symbol 89 | -- - Supports range queries: BETWEEN operation fully utilizes B-tree index 90 | -- - Essential for backtesting: ensures performance of historical data analysis 91 | CREATE INDEX idx_tick_timestamp ON tick_data(timestamp); 92 | 93 | -- ================================================================= 94 | -- Design Validation and Performance Testing 95 | -- ================================================================= 96 | 97 | -- Verify table structure 98 | \d tick_data 99 | 100 | -- View index information and sizes 101 | SELECT 102 | indexname, 103 | indexdef, 104 | pg_size_pretty(pg_relation_size(indexname::regclass)) AS size 105 | FROM pg_indexes 106 | WHERE tablename = 'tick_data' 107 | ORDER BY indexname; 108 | 109 | -- Performance test queries (run after data is inserted) 110 | /* 111 | -- Real-time query test 112 | EXPLAIN (ANALYZE, BUFFERS) 113 | SELECT * FROM tick_data 114 | WHERE symbol = 'BTCUSDT' 115 | ORDER BY timestamp DESC 116 | LIMIT 10; 117 | 118 | -- Backtesting query test 119 | EXPLAIN (ANALYZE, BUFFERS) 120 | SELECT COUNT(*), AVG(price) 121 | FROM tick_data 122 | WHERE timestamp BETWEEN NOW() - INTERVAL '1 day' AND NOW() 123 | AND symbol IN ('BTCUSDT', 'ETHUSDT'); 124 | */ 125 | -------------------------------------------------------------------------------- /trading-core/src/backtest/strategy/rsi.rs: -------------------------------------------------------------------------------- 1 | use super::base::{Signal, Strategy}; 2 | use crate::data::types::{OHLCData, TickData, Timeframe}; 3 | use rust_decimal::Decimal; 4 | use std::collections::{HashMap, VecDeque}; 5 | 6 | pub struct RsiStrategy { 7 | period: usize, 8 | oversold: Decimal, 9 | overbought: Decimal, 10 | prices: VecDeque, 11 | gains: VecDeque, 12 | losses: VecDeque, 13 | last_signal: Option, 14 | } 15 | 16 | impl RsiStrategy { 17 | pub fn new() -> Self { 18 | Self { 19 | period: 14, 20 | oversold: Decimal::from(30), 21 | overbought: Decimal::from(70), 22 | prices: VecDeque::new(), 23 | gains: VecDeque::new(), 24 | losses: VecDeque::new(), 25 | last_signal: None, 26 | } 27 | } 28 | 29 | fn calculate_rsi(&self) -> Option { 30 | if self.gains.len() < self.period || self.losses.len() < self.period { 31 | return None; 32 | } 33 | 34 | let avg_gain: Decimal = self.gains.iter().sum::() / Decimal::from(self.period); 35 | let avg_loss: Decimal = self.losses.iter().sum::() / Decimal::from(self.period); 36 | 37 | if avg_loss == Decimal::ZERO { 38 | return Some(Decimal::from(100)); 39 | } 40 | 41 | let rs = avg_gain / avg_loss; 42 | let rsi = Decimal::from(100) - (Decimal::from(100) / (Decimal::from(1) + rs)); 43 | 44 | Some(rsi) 45 | } 46 | } 47 | 48 | impl Strategy for RsiStrategy { 49 | fn name(&self) -> &str { 50 | "RSI Strategy" 51 | } 52 | 53 | fn initialize(&mut self, params: HashMap) -> Result<(), String> { 54 | if let Some(period) = params.get("period") { 55 | self.period = period.parse().map_err(|_| "Invalid period")?; 56 | } 57 | if let Some(oversold) = params.get("oversold") { 58 | self.oversold = oversold.parse().map_err(|_| "Invalid oversold")?; 59 | } 60 | if let Some(overbought) = params.get("overbought") { 61 | self.overbought = overbought.parse().map_err(|_| "Invalid overbought")?; 62 | } 63 | 64 | if self.oversold >= self.overbought { 65 | return Err("Oversold level must be less than overbought level".to_string()); 66 | } 67 | 68 | println!( 69 | "RSI Strategy initialized: period={}, oversold={}, overbought={}", 70 | self.period, self.oversold, self.overbought 71 | ); 72 | Ok(()) 73 | } 74 | 75 | fn reset(&mut self) { 76 | self.prices.clear(); 77 | self.gains.clear(); 78 | self.losses.clear(); 79 | self.last_signal = None; 80 | } 81 | 82 | fn on_tick(&mut self, tick: &TickData) -> Signal { 83 | if let Some(last_price) = self.prices.back() { 84 | let change = tick.price - last_price; 85 | 86 | if change > Decimal::ZERO { 87 | self.gains.push_back(change); 88 | self.losses.push_back(Decimal::ZERO); 89 | } else { 90 | self.gains.push_back(Decimal::ZERO); 91 | self.losses.push_back(-change); 92 | } 93 | 94 | // Keep fixed length 95 | if self.gains.len() > self.period { 96 | self.gains.pop_front(); 97 | self.losses.pop_front(); 98 | } 99 | } 100 | 101 | self.prices.push_back(tick.price); 102 | if self.prices.len() > self.period + 1 { 103 | self.prices.pop_front(); 104 | } 105 | 106 | if let Some(rsi) = self.calculate_rsi() { 107 | // Buy signal when RSI is oversold and we don't have a buy position 108 | if rsi < self.oversold && !matches!(self.last_signal, Some(Signal::Buy { .. })) { 109 | let signal = Signal::Buy { 110 | symbol: tick.symbol.clone(), 111 | quantity: Decimal::from(100), 112 | }; 113 | self.last_signal = Some(signal.clone()); 114 | return signal; 115 | } 116 | // Sell signal when RSI is overbought and we have a buy position 117 | else if rsi > self.overbought && matches!(self.last_signal, Some(Signal::Buy { .. })) 118 | { 119 | let signal = Signal::Sell { 120 | symbol: tick.symbol.clone(), 121 | quantity: Decimal::from(100), 122 | }; 123 | self.last_signal = Some(signal.clone()); 124 | return signal; 125 | } 126 | } 127 | 128 | Signal::Hold 129 | } 130 | 131 | fn on_ohlc(&mut self, ohlc: &OHLCData) -> Signal { 132 | if let Some(last_price) = self.prices.back() { 133 | let change = ohlc.close - last_price; 134 | 135 | if change > Decimal::ZERO { 136 | self.gains.push_back(change); 137 | self.losses.push_back(Decimal::ZERO); 138 | } else { 139 | self.gains.push_back(Decimal::ZERO); 140 | self.losses.push_back(-change); 141 | } 142 | 143 | if self.gains.len() > self.period { 144 | self.gains.pop_front(); 145 | self.losses.pop_front(); 146 | } 147 | } 148 | 149 | self.prices.push_back(ohlc.close); 150 | if self.prices.len() > self.period + 1 { 151 | self.prices.pop_front(); 152 | } 153 | 154 | if let Some(rsi) = self.calculate_rsi() { 155 | if rsi < self.oversold && !matches!(self.last_signal, Some(Signal::Buy { .. })) { 156 | let signal = Signal::Buy { 157 | symbol: ohlc.symbol.clone(), 158 | quantity: Decimal::from(100), 159 | }; 160 | self.last_signal = Some(signal.clone()); 161 | return signal; 162 | } else if rsi > self.overbought && matches!(self.last_signal, Some(Signal::Buy { .. })) 163 | { 164 | let signal = Signal::Sell { 165 | symbol: ohlc.symbol.clone(), 166 | quantity: Decimal::from(100), 167 | }; 168 | self.last_signal = Some(signal.clone()); 169 | return signal; 170 | } 171 | } 172 | 173 | Signal::Hold 174 | } 175 | 176 | fn supports_ohlc(&self) -> bool { 177 | true 178 | } 179 | fn preferred_timeframe(&self) -> Option { 180 | Some(Timeframe::OneDay) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /trading-core/benches/repository_bench.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Duration, Utc}; 2 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 3 | use dotenv::dotenv; 4 | use rust_decimal::Decimal; 5 | use sqlx::PgPool; 6 | use std::str::FromStr; 7 | use tokio::runtime::Runtime; 8 | use trading_core::data::{ 9 | cache::{TickDataCache, TieredCache}, 10 | repository::TickDataRepository, 11 | types::{DataResult, TickData, TickQuery, TradeSide}, 12 | }; 13 | 14 | fn create_test_tick( 15 | symbol: &str, 16 | price: &str, 17 | trade_id: &str, 18 | timestamp: DateTime, 19 | ) -> TickData { 20 | TickData::new( 21 | timestamp, 22 | symbol.to_string(), 23 | Decimal::from_str(price).unwrap(), 24 | Decimal::from_str("1.0").unwrap(), 25 | TradeSide::Buy, 26 | trade_id.to_string(), 27 | false, 28 | ) 29 | } 30 | 31 | async fn setup_repository() -> DataResult { 32 | dotenv().ok(); 33 | let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); 34 | let redis_url = std::env::var("REDIS_URL").expect("REDIS_URL must be set"); 35 | let pool = PgPool::connect(&database_url) 36 | .await 37 | .map_err(|e| trading_core::data::types::DataError::Database(e))?; 38 | let cache = TieredCache::new((1000, 300), (&redis_url, 10000, 3600)).await?; 39 | Ok(TickDataRepository::new(pool, cache)) 40 | } 41 | 42 | async fn setup_cache() -> DataResult { 43 | dotenv().ok(); 44 | let redis_url = std::env::var("REDIS_URL").expect("REDIS_URL must be set"); 45 | TieredCache::new((1000, 300), (&redis_url, 10000, 3600)).await 46 | } 47 | 48 | async fn cleanup_database(pool: &PgPool, symbol: &str) -> DataResult<()> { 49 | sqlx::query!("DELETE FROM tick_data WHERE symbol = $1", symbol) 50 | .execute(pool) 51 | .await 52 | .map_err(|e| trading_core::data::types::DataError::Database(e))?; 53 | Ok(()) 54 | } 55 | 56 | fn repository_benchmarks(c: &mut Criterion) { 57 | let rt = Runtime::new().unwrap(); 58 | let symbol = "BTCUSDT_BENCH"; 59 | 60 | // Setup repository and cache 61 | let repo = rt 62 | .block_on(setup_repository()) 63 | .expect("Failed to setup repository"); 64 | let cache = rt.block_on(setup_cache()).expect("Failed to setup cache"); 65 | let pool = repo.get_pool(); 66 | rt.block_on(cleanup_database(pool, symbol)) 67 | .expect("Failed to cleanup database"); 68 | 69 | // Benchmark: Single tick insertion 70 | c.bench_function("insert_tick", |b| { 71 | b.to_async(&rt).iter(|| async { 72 | let tick = create_test_tick(symbol, "50000.0", "bench_single", Utc::now()); 73 | repo.insert_tick(black_box(&tick)).await.unwrap(); 74 | }); 75 | }); 76 | 77 | // Benchmark: Batch insertion (100 ticks) 78 | c.bench_function("batch_insert_100", |b| { 79 | b.to_async(&rt).iter(|| async { 80 | let ticks: Vec = (0..100) 81 | .map(|i| { 82 | create_test_tick(symbol, "50000.0", &format!("bench_batch_{}", i), Utc::now()) 83 | }) 84 | .collect(); 85 | repo.batch_insert(black_box(ticks)).await.unwrap(); 86 | }); 87 | }); 88 | 89 | // Benchmark: Batch insertion (1000 ticks) 90 | c.bench_function("batch_insert_1000", |b| { 91 | b.to_async(&rt).iter(|| async { 92 | let ticks: Vec = (0..1000) 93 | .map(|i| { 94 | create_test_tick(symbol, "50000.0", &format!("bench_batch_{}", i), Utc::now()) 95 | }) 96 | .collect(); 97 | repo.batch_insert(black_box(ticks)).await.unwrap(); 98 | }); 99 | }); 100 | 101 | // Pre-populate data for query benchmarks 102 | rt.block_on(async { 103 | let ticks: Vec = (0..1000) 104 | .map(|i| { 105 | create_test_tick( 106 | symbol, 107 | "50000.0", 108 | &format!("bench_query_{}", i), 109 | Utc::now() - Duration::seconds(1000 - i as i64), 110 | ) 111 | }) 112 | .collect(); 113 | repo.batch_insert(ticks).await.unwrap(); 114 | }); 115 | 116 | // Benchmark: Query ticks (cache hit) 117 | c.bench_function("get_ticks_cache_hit", |b| { 118 | b.to_async(&rt).iter(|| async { 119 | let query = TickQuery { 120 | symbol: symbol.to_string(), 121 | limit: Some(100), 122 | start_time: Some(Utc::now() - Duration::minutes(30)), 123 | end_time: None, 124 | trade_side: None, 125 | }; 126 | repo.get_ticks(black_box(&query)).await.unwrap(); 127 | }); 128 | }); 129 | 130 | // Benchmark: Query ticks (cache miss) 131 | c.bench_function("get_ticks_cache_miss", |b| { 132 | b.to_async(&rt).iter(|| async { 133 | let query = TickQuery { 134 | symbol: symbol.to_string(), 135 | limit: Some(100), 136 | start_time: Some(Utc::now() - Duration::days(1)), 137 | end_time: None, 138 | trade_side: None, 139 | }; 140 | repo.get_ticks(black_box(&query)).await.unwrap(); 141 | }); 142 | }); 143 | 144 | // Benchmark: Latest price 145 | c.bench_function("get_latest_price", |b| { 146 | b.to_async(&rt).iter(|| async { 147 | repo.get_latest_price(black_box(symbol)).await.unwrap(); 148 | }); 149 | }); 150 | 151 | // Benchmark: Historical data for backtest 152 | c.bench_function("get_historical_data_for_backtest", |b| { 153 | b.to_async(&rt).iter(|| async { 154 | let start_time = Utc::now() - Duration::hours(1); 155 | let end_time = Utc::now(); 156 | repo.get_historical_data_for_backtest( 157 | black_box(symbol), 158 | start_time, 159 | end_time, 160 | Some(100), 161 | ) 162 | .await 163 | .unwrap(); 164 | }); 165 | }); 166 | 167 | // Benchmark: Cache push_tick 168 | c.bench_function("cache_push_tick", |b| { 169 | b.to_async(&rt).iter(|| async { 170 | let tick = create_test_tick(symbol, "50000.0", "cache_bench", Utc::now()); 171 | cache.push_tick(black_box(&tick)).await.unwrap(); 172 | }); 173 | }); 174 | 175 | // Benchmark: Cache get_recent_ticks 176 | c.bench_function("cache_get_recent_ticks", |b| { 177 | b.to_async(&rt).iter(|| async { 178 | cache 179 | .get_recent_ticks(black_box(symbol), 100) 180 | .await 181 | .unwrap(); 182 | }); 183 | }); 184 | 185 | // Cleanup after benchmarks 186 | rt.block_on(cleanup_database(pool, symbol)) 187 | .expect("Failed to cleanup database"); 188 | rt.block_on(cache.clear_symbol(symbol)) 189 | .expect("Failed to clear cache"); 190 | } 191 | 192 | criterion_group!(benches, repository_benchmarks); 193 | criterion_main!(benches); 194 | -------------------------------------------------------------------------------- /trading-core/src/live_trading/paper_trading.rs: -------------------------------------------------------------------------------- 1 | // src/live_trading/paper_trading.rs 2 | use rust_decimal::Decimal; 3 | use std::sync::Arc; 4 | use std::time::Instant; 5 | use tracing::debug; 6 | 7 | use crate::backtest::strategy::base::{Signal, Strategy}; 8 | use crate::data::cache::TickDataCache; 9 | use crate::data::repository::TickDataRepository; 10 | use crate::data::types::{LiveStrategyLog, TickData}; 11 | 12 | pub struct PaperTradingProcessor { 13 | strategy: Box, 14 | repository: Arc, 15 | initial_capital: Decimal, 16 | 17 | //Simple status tracking 18 | cash: Decimal, 19 | position: Decimal, 20 | avg_cost: Decimal, 21 | total_trades: u64, 22 | } 23 | 24 | impl PaperTradingProcessor { 25 | pub fn new( 26 | strategy: Box, 27 | repository: Arc, 28 | initial_capital: Decimal, 29 | ) -> Self { 30 | Self { 31 | strategy, 32 | repository, 33 | initial_capital, 34 | cash: initial_capital, 35 | position: Decimal::ZERO, 36 | avg_cost: Decimal::ZERO, 37 | total_trades: 0, 38 | } 39 | } 40 | 41 | pub async fn process_tick(&mut self, tick: &TickData) -> Result<(), String> { 42 | let start_time = Instant::now(); 43 | 44 | // 1. Get data from cache 45 | let cache_start = Instant::now(); 46 | let recent_ticks = self 47 | .repository 48 | .get_cache() 49 | .get_recent_ticks(&tick.symbol, 20) 50 | .await 51 | .map_err(|e| format!("Cache error: {}", e))?; 52 | let cache_hit = !recent_ticks.is_empty(); 53 | let cache_time = cache_start.elapsed().as_micros() as u64; 54 | 55 | // 2. Policy Handle - Using Existing Policies 56 | let signal = self.strategy.on_tick(tick); 57 | 58 | // 3. Execution of trading signals 59 | let signal_type = self.execute_signal(&signal, tick)?; 60 | 61 | // 4. Calculate Portfolio Value 62 | let portfolio_value = self.calculate_portfolio_value(tick.price); 63 | let total_pnl = portfolio_value - self.initial_capital; 64 | 65 | // 5. Record to database 66 | let processing_time = start_time.elapsed().as_micros() as u64; 67 | let log = LiveStrategyLog { 68 | timestamp: tick.timestamp, 69 | strategy_id: self.strategy.name().to_string(), 70 | symbol: tick.symbol.clone(), 71 | current_price: tick.price, 72 | signal_type: signal_type.clone(), 73 | portfolio_value, 74 | total_pnl, 75 | cache_hit, 76 | processing_time_us: processing_time, 77 | }; 78 | 79 | self.repository 80 | .insert_live_strategy_log(&log) 81 | .await 82 | .map_err(|e| format!("Database error: {}", e))?; 83 | 84 | // 6. Real-time output 85 | self.log_activity( 86 | &signal_type, 87 | tick, 88 | portfolio_value, 89 | total_pnl, 90 | cache_hit, 91 | cache_time, 92 | processing_time, 93 | ); 94 | 95 | Ok(()) 96 | } 97 | 98 | fn execute_signal(&mut self, signal: &Signal, tick: &TickData) -> Result { 99 | match signal { 100 | Signal::Buy { quantity, .. } => { 101 | let cost = quantity * tick.price; 102 | 103 | if cost <= self.cash { 104 | if self.position == Decimal::ZERO { 105 | self.position = *quantity; 106 | self.avg_cost = tick.price; 107 | } else { 108 | let total_cost = (self.position * self.avg_cost) + cost; 109 | self.position += quantity; 110 | self.avg_cost = total_cost / self.position; 111 | } 112 | 113 | self.cash -= cost; 114 | self.total_trades += 1; 115 | 116 | debug!( 117 | "BUY executed: {} @ {}, position: {}, cash: {}", 118 | quantity, tick.price, self.position, self.cash 119 | ); 120 | return Ok("BUY".to_string()); 121 | } else { 122 | debug!( 123 | "BUY signal ignored: insufficient cash ({} needed, {} available)", 124 | cost, self.cash 125 | ); 126 | } 127 | } 128 | 129 | Signal::Sell { quantity, .. } => { 130 | if *quantity <= self.position { 131 | let proceeds = quantity * tick.price; 132 | self.cash += proceeds; 133 | self.position -= quantity; 134 | self.total_trades += 1; 135 | 136 | if self.position == Decimal::ZERO { 137 | self.avg_cost = Decimal::ZERO; 138 | } 139 | 140 | debug!( 141 | "SELL executed: {} @ {}, position: {}, cash: {}", 142 | quantity, tick.price, self.position, self.cash 143 | ); 144 | return Ok("SELL".to_string()); 145 | } else { 146 | debug!( 147 | "SELL signal ignored: insufficient position ({} needed, {} available)", 148 | quantity, self.position 149 | ); 150 | } 151 | } 152 | 153 | Signal::Hold => return Ok("HOLD".to_string()), 154 | } 155 | 156 | Ok("HOLD".to_string()) 157 | } 158 | 159 | fn calculate_portfolio_value(&self, current_price: Decimal) -> Decimal { 160 | self.cash + (self.position * current_price) 161 | } 162 | 163 | fn log_activity( 164 | &self, 165 | signal_type: &str, 166 | tick: &TickData, 167 | portfolio_value: Decimal, 168 | total_pnl: Decimal, 169 | cache_hit: bool, 170 | cache_time_us: u64, 171 | total_time_us: u64, 172 | ) { 173 | if signal_type != "HOLD" { 174 | let return_pct = if self.initial_capital > Decimal::ZERO { 175 | total_pnl / self.initial_capital * Decimal::from(100) 176 | } else { 177 | Decimal::ZERO 178 | }; 179 | 180 | println!("🎯 {} {} @ ${} | Portfolio: ${} | P&L: ${} ({:.2}%) | Position: {} | Cash: ${} | Trades: {} | Cache: {} ({}μs) | Total: {}μs", 181 | signal_type, 182 | tick.symbol, 183 | tick.price, 184 | portfolio_value, 185 | total_pnl, 186 | return_pct, 187 | self.position, 188 | self.cash, 189 | self.total_trades, 190 | if cache_hit { "HIT" } else { "MISS" }, 191 | cache_time_us, 192 | total_time_us); 193 | } else { 194 | if tick.timestamp.timestamp() % 10 == 0 { 195 | println!( 196 | "📊 {} {} @ ${} | Portfolio: ${} | P&L: ${} | Cache: {} ({}μs)", 197 | tick.symbol, 198 | if cache_hit { "HIT" } else { "MISS" }, 199 | tick.price, 200 | portfolio_value, 201 | total_pnl, 202 | if cache_hit { "✓" } else { "✗" }, 203 | cache_time_us 204 | ); 205 | } 206 | } 207 | } 208 | 209 | pub fn get_status(&self) -> PaperTradingStatus { 210 | PaperTradingStatus { 211 | strategy_name: self.strategy.name().to_string(), 212 | cash: self.cash, 213 | position: self.position, 214 | avg_cost: self.avg_cost, 215 | total_trades: self.total_trades, 216 | } 217 | } 218 | } 219 | 220 | #[derive(Debug, Clone)] 221 | pub struct PaperTradingStatus { 222 | pub strategy_name: String, 223 | pub cash: Decimal, 224 | pub position: Decimal, 225 | pub avg_cost: Decimal, 226 | pub total_trades: u64, 227 | } 228 | -------------------------------------------------------------------------------- /trading-core/src/backtest/portfolio.rs: -------------------------------------------------------------------------------- 1 | use crate::data::types::TradeSide; 2 | use chrono::{DateTime, Utc}; 3 | use rust_decimal::Decimal; 4 | use std::collections::HashMap; 5 | use std::str::FromStr; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct Position { 9 | pub symbol: String, 10 | pub quantity: Decimal, 11 | pub avg_price: Decimal, 12 | pub market_value: Decimal, 13 | pub unrealized_pnl: Decimal, 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct Trade { 18 | pub symbol: String, 19 | pub side: TradeSide, 20 | pub quantity: Decimal, 21 | pub price: Decimal, 22 | pub timestamp: DateTime, 23 | pub realized_pnl: Option, 24 | pub commission: Decimal, 25 | } 26 | 27 | pub struct Portfolio { 28 | pub initial_capital: Decimal, 29 | pub cash: Decimal, 30 | pub positions: HashMap, 31 | pub trades: Vec, 32 | pub current_prices: HashMap, 33 | pub commission_rate: Decimal, // e.g., 0.001 for 0.1% 34 | } 35 | 36 | impl Portfolio { 37 | pub fn new(initial_capital: Decimal) -> Self { 38 | Self { 39 | initial_capital, 40 | cash: initial_capital, 41 | positions: HashMap::new(), 42 | trades: Vec::new(), 43 | current_prices: HashMap::new(), 44 | commission_rate: Decimal::from_str("0.001").unwrap_or(Decimal::ZERO), // 0.1% default 45 | } 46 | } 47 | 48 | pub fn with_commission_rate(mut self, rate: Decimal) -> Self { 49 | self.commission_rate = rate; 50 | self 51 | } 52 | 53 | pub fn update_price(&mut self, symbol: &str, price: Decimal) { 54 | self.current_prices.insert(symbol.to_string(), price); 55 | 56 | // Update position market value and unrealized PnL 57 | if let Some(position) = self.positions.get_mut(symbol) { 58 | position.market_value = position.quantity * price; 59 | position.unrealized_pnl = (price - position.avg_price) * position.quantity; 60 | } 61 | } 62 | 63 | pub fn execute_buy( 64 | &mut self, 65 | symbol: String, 66 | quantity: Decimal, 67 | price: Decimal, 68 | ) -> Result<(), String> { 69 | let cost = quantity * price; 70 | let commission = cost * self.commission_rate; 71 | let total_cost = cost + commission; 72 | 73 | if total_cost > self.cash { 74 | return Err(format!( 75 | "Insufficient funds: need ${}, available ${}", 76 | total_cost, self.cash 77 | )); 78 | } 79 | 80 | self.cash -= total_cost; 81 | 82 | match self.positions.get_mut(&symbol) { 83 | Some(position) => { 84 | let total_quantity = position.quantity + quantity; 85 | let total_cost = position.quantity * position.avg_price + cost; 86 | position.avg_price = total_cost / total_quantity; 87 | position.quantity = total_quantity; 88 | position.market_value = total_quantity * price; 89 | position.unrealized_pnl = (price - position.avg_price) * total_quantity; 90 | } 91 | None => { 92 | self.positions.insert( 93 | symbol.clone(), 94 | Position { 95 | symbol: symbol.clone(), 96 | quantity, 97 | avg_price: price, 98 | market_value: quantity * price, 99 | unrealized_pnl: Decimal::ZERO, 100 | }, 101 | ); 102 | } 103 | } 104 | 105 | self.trades.push(Trade { 106 | symbol, 107 | side: TradeSide::Buy, 108 | quantity, 109 | price, 110 | timestamp: Utc::now(), 111 | realized_pnl: None, 112 | commission, 113 | }); 114 | 115 | Ok(()) 116 | } 117 | 118 | pub fn execute_sell( 119 | &mut self, 120 | symbol: String, 121 | quantity: Decimal, 122 | price: Decimal, 123 | ) -> Result<(), String> { 124 | let position = self 125 | .positions 126 | .get_mut(&symbol) 127 | .ok_or("No position to sell")?; 128 | 129 | if quantity > position.quantity { 130 | return Err(format!( 131 | "Insufficient position: need {}, available {}", 132 | quantity, position.quantity 133 | )); 134 | } 135 | 136 | let proceeds = quantity * price; 137 | let commission = proceeds * self.commission_rate; 138 | let net_proceeds = proceeds - commission; 139 | 140 | self.cash += net_proceeds; 141 | 142 | // Calculate realized PnL 143 | let realized_pnl = (price - position.avg_price) * quantity - commission; 144 | 145 | position.quantity -= quantity; 146 | if position.quantity == Decimal::ZERO { 147 | self.positions.remove(&symbol); 148 | } else { 149 | position.market_value = position.quantity * price; 150 | position.unrealized_pnl = (price - position.avg_price) * position.quantity; 151 | } 152 | 153 | self.trades.push(Trade { 154 | symbol, 155 | side: TradeSide::Sell, 156 | quantity, 157 | price, 158 | timestamp: Utc::now(), 159 | realized_pnl: Some(realized_pnl), 160 | commission, 161 | }); 162 | 163 | Ok(()) 164 | } 165 | 166 | pub fn total_value(&self) -> Decimal { 167 | let mut total = self.cash; 168 | 169 | for position in self.positions.values() { 170 | total += position.market_value; 171 | } 172 | 173 | total 174 | } 175 | 176 | pub fn total_realized_pnl(&self) -> Decimal { 177 | self.trades 178 | .iter() 179 | .filter_map(|trade| trade.realized_pnl) 180 | .sum() 181 | } 182 | 183 | pub fn total_unrealized_pnl(&self) -> Decimal { 184 | self.positions.values().map(|pos| pos.unrealized_pnl).sum() 185 | } 186 | 187 | pub fn total_pnl(&self) -> Decimal { 188 | self.total_realized_pnl() + self.total_unrealized_pnl() 189 | } 190 | 191 | pub fn total_commission(&self) -> Decimal { 192 | self.trades.iter().map(|trade| trade.commission).sum() 193 | } 194 | 195 | pub fn has_position(&self, symbol: &str) -> bool { 196 | self.positions.contains_key(symbol) 197 | && self.positions.get(symbol).unwrap().quantity > Decimal::ZERO 198 | } 199 | 200 | pub fn get_equity_curve(&self) -> Vec { 201 | let mut equity_curve = vec![self.initial_capital]; 202 | let mut running_cash = self.initial_capital; 203 | let mut running_positions: HashMap = HashMap::new(); // (quantity, avg_price) 204 | 205 | for trade in &self.trades { 206 | match trade.side { 207 | TradeSide::Buy => { 208 | running_cash -= trade.quantity * trade.price + trade.commission; 209 | let (curr_qty, curr_avg) = running_positions 210 | .get(&trade.symbol) 211 | .unwrap_or(&(Decimal::ZERO, Decimal::ZERO)); 212 | let new_qty = curr_qty + trade.quantity; 213 | let new_avg = if new_qty > Decimal::ZERO { 214 | (curr_qty * curr_avg + trade.quantity * trade.price) / new_qty 215 | } else { 216 | Decimal::ZERO 217 | }; 218 | running_positions.insert(trade.symbol.clone(), (new_qty, new_avg)); 219 | } 220 | TradeSide::Sell => { 221 | running_cash += trade.quantity * trade.price - trade.commission; 222 | if let Some((curr_qty, curr_avg)) = running_positions.get_mut(&trade.symbol) { 223 | *curr_qty -= trade.quantity; 224 | if *curr_qty <= Decimal::ZERO { 225 | running_positions.remove(&trade.symbol); 226 | } 227 | } 228 | } 229 | } 230 | 231 | // Calculate current portfolio value 232 | let mut portfolio_value = running_cash; 233 | for (symbol, (quantity, _)) in &running_positions { 234 | if let Some(current_price) = self.current_prices.get(symbol) { 235 | portfolio_value += quantity * current_price; 236 | } 237 | } 238 | equity_curve.push(portfolio_value); 239 | } 240 | 241 | equity_curve 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /trading-core/src/backtest/metrics.rs: -------------------------------------------------------------------------------- 1 | use rust_decimal::prelude::*; 2 | use rust_decimal::Decimal; 3 | use std::collections::HashMap; 4 | 5 | pub struct BacktestMetrics; 6 | 7 | impl BacktestMetrics { 8 | /// Calculate Sharpe ratio 9 | /// Sharpe Ratio = (Mean Return - Risk Free Rate) / Standard Deviation of Returns 10 | pub fn calculate_sharpe_ratio(returns: &[Decimal], risk_free_rate: Decimal) -> Decimal { 11 | if returns.is_empty() { 12 | return Decimal::ZERO; 13 | } 14 | 15 | let mean_return = Self::calculate_mean(returns); 16 | let std_dev = Self::calculate_standard_deviation(returns); 17 | 18 | if std_dev == Decimal::ZERO { 19 | return Decimal::ZERO; 20 | } 21 | 22 | (mean_return - risk_free_rate) / std_dev 23 | } 24 | 25 | /// Calculate maximum drawdown 26 | /// Max Drawdown = Max((Peak - Trough) / Peak) over all time periods 27 | pub fn calculate_max_drawdown(equity_curve: &[Decimal]) -> Decimal { 28 | if equity_curve.len() < 2 { 29 | return Decimal::ZERO; 30 | } 31 | 32 | let mut max_drawdown = Decimal::ZERO; 33 | let mut peak = equity_curve[0]; 34 | 35 | for &value in equity_curve.iter().skip(1) { 36 | if value > peak { 37 | peak = value; 38 | } 39 | 40 | let drawdown = if peak > Decimal::ZERO { 41 | (peak - value) / peak 42 | } else { 43 | Decimal::ZERO 44 | }; 45 | 46 | if drawdown > max_drawdown { 47 | max_drawdown = drawdown; 48 | } 49 | } 50 | 51 | max_drawdown 52 | } 53 | 54 | /// Calculate volatility (standard deviation of returns) 55 | pub fn calculate_volatility(returns: &[Decimal]) -> Decimal { 56 | Self::calculate_standard_deviation(returns) 57 | } 58 | 59 | /// Calculate Calmar ratio (Annual Return / Max Drawdown) 60 | pub fn calculate_calmar_ratio(annual_return: Decimal, max_drawdown: Decimal) -> Decimal { 61 | if max_drawdown == Decimal::ZERO { 62 | return Decimal::ZERO; 63 | } 64 | annual_return / max_drawdown 65 | } 66 | 67 | /// Calculate Sortino ratio (uses downside deviation instead of total volatility) 68 | pub fn calculate_sortino_ratio( 69 | returns: &[Decimal], 70 | risk_free_rate: Decimal, 71 | target_return: Decimal, 72 | ) -> Decimal { 73 | if returns.is_empty() { 74 | return Decimal::ZERO; 75 | } 76 | 77 | let mean_return = Self::calculate_mean(returns); 78 | let downside_deviation = Self::calculate_downside_deviation(returns, target_return); 79 | 80 | if downside_deviation == Decimal::ZERO { 81 | return Decimal::ZERO; 82 | } 83 | 84 | (mean_return - risk_free_rate) / downside_deviation 85 | } 86 | 87 | /// Calculate Value at Risk (VaR) at given confidence level 88 | pub fn calculate_var(returns: &[Decimal], confidence_level: Decimal) -> Decimal { 89 | if returns.is_empty() { 90 | return Decimal::ZERO; 91 | } 92 | 93 | let mut sorted_returns = returns.to_vec(); 94 | sorted_returns.sort(); 95 | 96 | let index = ((Decimal::ONE - confidence_level) * Decimal::from(sorted_returns.len())) 97 | .to_usize() 98 | .unwrap_or(0); 99 | if index < sorted_returns.len() { 100 | sorted_returns[index] 101 | } else { 102 | Decimal::ZERO 103 | } 104 | } 105 | 106 | /// Calculate information ratio 107 | pub fn calculate_information_ratio( 108 | portfolio_returns: &[Decimal], 109 | benchmark_returns: &[Decimal], 110 | ) -> Decimal { 111 | if portfolio_returns.len() != benchmark_returns.len() || portfolio_returns.is_empty() { 112 | return Decimal::ZERO; 113 | } 114 | 115 | let excess_returns: Vec = portfolio_returns 116 | .iter() 117 | .zip(benchmark_returns.iter()) 118 | .map(|(p, b)| p - b) 119 | .collect(); 120 | 121 | let mean_excess_return = Self::calculate_mean(&excess_returns); 122 | let tracking_error = Self::calculate_standard_deviation(&excess_returns); 123 | 124 | if tracking_error == Decimal::ZERO { 125 | return Decimal::ZERO; 126 | } 127 | 128 | mean_excess_return / tracking_error 129 | } 130 | 131 | /// Calculate win rate (percentage of profitable trades) 132 | pub fn calculate_win_rate(trades: &[crate::backtest::portfolio::Trade]) -> Decimal { 133 | if trades.is_empty() { 134 | return Decimal::ZERO; 135 | } 136 | 137 | let profitable_trades = trades 138 | .iter() 139 | .filter(|trade| trade.realized_pnl.map_or(false, |pnl| pnl > Decimal::ZERO)) 140 | .count(); 141 | 142 | Decimal::from(profitable_trades) / Decimal::from(trades.len()) * Decimal::from(100) 143 | } 144 | 145 | /// Calculate profit factor (total profit / total loss) 146 | pub fn calculate_profit_factor(trades: &[crate::backtest::portfolio::Trade]) -> Decimal { 147 | let (total_profit, total_loss) = trades.iter().filter_map(|trade| trade.realized_pnl).fold( 148 | (Decimal::ZERO, Decimal::ZERO), 149 | |(profit, loss), pnl| { 150 | if pnl > Decimal::ZERO { 151 | (profit + pnl, loss) 152 | } else { 153 | (profit, loss - pnl) 154 | } 155 | }, 156 | ); 157 | 158 | if total_loss == Decimal::ZERO { 159 | return if total_profit > Decimal::ZERO { 160 | Decimal::MAX 161 | } else { 162 | Decimal::ZERO 163 | }; 164 | } 165 | 166 | total_profit / total_loss 167 | } 168 | 169 | /// Calculate average trade duration 170 | pub fn calculate_average_trade_duration(trades: &[crate::backtest::portfolio::Trade]) -> f64 { 171 | if trades.len() < 2 { 172 | return 0.0; 173 | } 174 | 175 | let mut durations = Vec::new(); 176 | let mut open_positions: HashMap> = HashMap::new(); 177 | 178 | for trade in trades { 179 | match trade.side { 180 | crate::data::types::TradeSide::Buy => { 181 | open_positions.insert(trade.symbol.clone(), trade.timestamp); 182 | } 183 | crate::data::types::TradeSide::Sell => { 184 | if let Some(open_time) = open_positions.remove(&trade.symbol) { 185 | let duration = trade.timestamp.signed_duration_since(open_time); 186 | durations.push(duration.num_seconds() as f64); 187 | } 188 | } 189 | } 190 | } 191 | 192 | if durations.is_empty() { 193 | 0.0 194 | } else { 195 | durations.iter().sum::() / durations.len() as f64 196 | } 197 | } 198 | 199 | // Helper functions 200 | 201 | fn calculate_mean(values: &[Decimal]) -> Decimal { 202 | if values.is_empty() { 203 | return Decimal::ZERO; 204 | } 205 | values.iter().sum::() / Decimal::from(values.len()) 206 | } 207 | 208 | fn calculate_standard_deviation(values: &[Decimal]) -> Decimal { 209 | if values.len() < 2 { 210 | return Decimal::ZERO; 211 | } 212 | 213 | let mean = Self::calculate_mean(values); 214 | let variance = values 215 | .iter() 216 | .map(|x| (x - mean) * (x - mean)) 217 | .sum::() 218 | / Decimal::from(values.len() - 1); 219 | 220 | // Approximate square root using Newton's method 221 | Self::decimal_sqrt(variance) 222 | } 223 | 224 | fn calculate_downside_deviation(returns: &[Decimal], target_return: Decimal) -> Decimal { 225 | if returns.is_empty() { 226 | return Decimal::ZERO; 227 | } 228 | 229 | let downside_returns: Vec = returns 230 | .iter() 231 | .filter(|&&r| r < target_return) 232 | .map(|&r| r - target_return) 233 | .collect(); 234 | 235 | if downside_returns.is_empty() { 236 | return Decimal::ZERO; 237 | } 238 | 239 | let downside_variance = downside_returns.iter().map(|x| x * x).sum::() 240 | / Decimal::from(downside_returns.len()); 241 | 242 | Self::decimal_sqrt(downside_variance) 243 | } 244 | 245 | /// Approximate square root for Decimal using Newton's method 246 | fn decimal_sqrt(value: Decimal) -> Decimal { 247 | if value <= Decimal::ZERO { 248 | return Decimal::ZERO; 249 | } 250 | 251 | let mut x = value / Decimal::from(2); 252 | let tolerance = Decimal::from_str("0.000001").unwrap(); 253 | 254 | for _ in 0..50 { 255 | // Max iterations 256 | let new_x = (x + value / x) / Decimal::from(2); 257 | if (new_x - x).abs() < tolerance { 258 | break; 259 | } 260 | x = new_x; 261 | } 262 | 263 | x 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /trading-core/README.md: -------------------------------------------------------------------------------- 1 | # Trading Core 2 | 3 | A professional-grade cryptocurrency data collection and backtesting system built in Rust, designed for real-time market data processing, storage, and quantitative strategy analysis. 4 | 5 | ## 🏗️ Architecture 6 | 7 | ### **System Overview** 8 | ``` 9 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 10 | │ Exchange │───▶│ Service │───▶│ Repository │ 11 | │ (WebSocket) │ │ (Processing) │ │ (Storage) │ 12 | └─────────────────┘ └─────────────────┘ └─────────────────┘ 13 | │ │ │ 14 | │ ▼ ▼ 15 | Binance API ┌─────────────┐ ┌─────────────┐ 16 | - Real-time data │ Cache │ │ PostgreSQL │ 17 | - Historical data │ (L1 + L2) │ │ Database │ 18 | └─────────────┘ └─────────────┘ 19 | │ 20 | ▼ 21 | ┌─────────────────┐ 22 | │ Backtest │ 23 | │ Engine │ 24 | └─────────────────┘ 25 | ``` 26 | 27 | ### **Dual-Mode Operation** 28 | 29 | #### **Live Trading Mode** 30 | ``` 31 | Exchange API → Service → Repository → Database + Cache 32 | ``` 33 | 34 | #### **Backtesting Mode** 35 | ``` 36 | Database → Repository → Backtest Engine → Strategy → Portfolio → Metrics 37 | ``` 38 | 39 | 40 | ## ✨ Features 41 | 42 | ### 🚀 **High Performance** 43 | - **Asynchronous Architecture**: Built with Tokio for maximum concurrency 44 | - **Optimized Database Operations**: 45 | - Single tick insert: ~390µs 46 | - Batch insert (100 ticks): ~13ms 47 | - Batch insert (1000 ticks): ~116ms 48 | - **Multi-level Caching**: L1 (Memory) + L2 (Redis) with microsecond access times 49 | - **Smart Query Optimization**: Cache hit ~10µs vs cache miss ~11.6ms 50 | 51 | ### 🛡️ **Reliability** 52 | - **Automatic Retry**: Database failures with exponential backoff 53 | - **Data Integrity**: Duplicate detection using unique constraints 54 | - **Graceful Shutdown**: Zero data loss during termination 55 | - **Error Isolation**: Cache failures don't impact main data flow 56 | 57 | ### 📊 **Backtesting System** 58 | - **Multi-Strategy Framework**: Built-in SMA and RSI strategies 59 | - **Professional Metrics**: Sharpe ratio, max drawdown, win rate, profit factor 60 | - **Portfolio Management**: Real-time P&L tracking and position management 61 | - **Interactive CLI**: User-friendly backtesting interface 62 | - **Historical Data Processing**: ~450µs per query with optimized indexing 63 | 64 | ### 🔧 **Flexible Configuration** 65 | - **Dual Mode Operation**: Live data collection and backtesting 66 | - **Multi-Environment Support**: Development, production configurations 67 | - **Environment Variable Overrides**: Secure configuration management 68 | - **Symbol Configuration**: Easily configure trading pairs to monitor 69 | 70 | ## 🚀 Quick Start 71 | 72 | ### **Prerequisites** 73 | - Rust 1.70+ 74 | - PostgreSQL 12+ 75 | - Redis 6+ 76 | 77 | ### **Installation** 78 | 79 | 1. **Clone and setup** 80 | ```bash 81 | git clone https://github.com/Erio-Harrison/rust-trade.git 82 | cd trading-core 83 | ``` 84 | 85 | 2. **Database setup** 86 | ```sql 87 | CREATE DATABASE trading_core; 88 | \i database/schema.sql 89 | ``` 90 | 91 | 3. **Environment configuration** 92 | ```bash 93 | # .env file 94 | DATABASE_URL=postgresql://user:password@localhost/trading_core 95 | REDIS_URL=redis://127.0.0.1:6379 96 | RUN_MODE=development 97 | ``` 98 | 99 | 4. **Symbol configuration** 100 | ```toml 101 | # config/development.toml 102 | symbols = ["BTCUSDT", "ETHUSDT", "ADAUSDT"] 103 | ``` 104 | 105 | ### **Running the Application** 106 | 107 | #### **Live Data Collection** 108 | ```bash 109 | # Start real-time data collection 110 | cargo run 111 | # or explicitly 112 | cargo run live 113 | ``` 114 | 115 | #### **Backtesting** 116 | ```bash 117 | # Start interactive backtesting 118 | cargo run backtest 119 | ``` 120 | 121 | #### **Help** 122 | ```bash 123 | cargo run -- --help 124 | ``` 125 | 126 | ## 📊 Performance Benchmarks 127 | 128 | Based on comprehensive benchmarking results: 129 | 130 | | Operation | Performance | Notes | 131 | |-----------|-------------|-------| 132 | | Single tick insert | ~390µs | Individual database writes | 133 | | Batch insert (100) | ~13ms | Optimized bulk operations | 134 | | Batch insert (1000) | ~116ms | Large batch processing | 135 | | Cache hit | ~10µs | Memory/Redis retrieval | 136 | | Cache miss | ~11.6ms | Database fallback | 137 | | Historical query | ~450µs | Backtest data retrieval | 138 | | Cache operations | ~17-104µs | Push/pull operations | 139 | 140 | ## 🏗️ Project Structure 141 | 142 | ``` 143 | src/ 144 | ├── main.rs # Application entry point with live/backtest modes 145 | ├── config.rs # Configuration management (Settings, env vars) 146 | ├── data/ # Data layer 147 | │ ├── mod.rs # Module exports 148 | │ ├── types.rs # Core data types (TickData, TradeSide, BacktestDataInfo, errors) 149 | │ ├── repository.rs # Database operations, query logic, and backtest data queries 150 | │ └── cache.rs # Multi-level caching implementation 151 | ├── exchange/ # Exchange integrations 152 | │ ├── mod.rs # Module exports 153 | │ ├── traits.rs # Exchange interface definition 154 | │ ├── types.rs # Exchange-specific data structures 155 | │ ├── errors.rs # Exchange error types 156 | │ ├── utils.rs # Conversion and validation utilities 157 | │ └── binance.rs # Binance WebSocket and REST API implementation 158 | ├── service/ # Business logic layer (Live trading) 159 | │ ├── mod.rs # Module exports 160 | │ ├── types.rs # Service types (BatchConfig, metrics) 161 | │ ├── errors.rs # Service error types 162 | │ └── market_data.rs # Main data processing service 163 | ├── backtest/ # Backtesting system 164 | │ ├── mod.rs # Module exports and public interface 165 | │ ├── engine.rs # Core backtesting engine and execution logic 166 | │ ├── portfolio.rs # Portfolio management, position tracking, P&L calculation 167 | │ ├── metrics.rs # Performance metrics calculation (Sharpe, drawdown, etc.) 168 | │ └── strategy/ # Trading strategies 169 | │ ├── mod.rs # Strategy factory and management 170 | │ ├── base.rs # Strategy trait definition and interfaces 171 | │ ├── sma.rs # Simple Moving Average strategy 172 | │ └── rsi.rs # RSI (Relative Strength Index) strategy 173 | └── live_trading/ # Live trading system 174 | ├── mod.rs # Module exports 175 | └── paper_trading.rs # Paper trading implementation 176 | ``` 177 | 178 | ## ⚙️ Configuration 179 | 180 | ### **Environment Variables** 181 | | Variable | Description | Example | 182 | |----------|-------------|---------| 183 | | `DATABASE_URL` | PostgreSQL connection | `postgresql://user:pass@localhost/trading_core` | 184 | | `REDIS_URL` | Redis connection | `redis://127.0.0.1:6379` | 185 | | `RUN_MODE` | Environment mode | `development` / `production` | 186 | | `RUST_LOG` | Logging level | `trading_core=info` | 187 | 188 | ### **Configuration Structure** 189 | ``` 190 | config/ 191 | ├── development.toml # Development settings 192 | ├── production.toml # Production settings 193 | └── test.toml # Test environment 194 | ``` 195 | 196 | ## 🔧 Backtesting Usage 197 | 198 | ### **Interactive Flow** 199 | 1. **Data Analysis**: View available symbols and data ranges 200 | 2. **Strategy Selection**: Choose from built-in strategies (SMA, RSI) 201 | 3. **Parameter Configuration**: Set initial capital, commission rates, data range 202 | 4. **Execution**: Real-time progress tracking and results 203 | 5. **Analysis**: Comprehensive performance metrics and trade analysis 204 | 205 | ### **Example Session** 206 | ```bash 207 | $ cargo run backtest 208 | 209 | 🎯 TRADING CORE BACKTESTING SYSTEM 210 | ================================================ 211 | 📊 Loading data statistics... 212 | 213 | 📈 Available Data: 214 | Total Records: 1,245,678 215 | Available Symbols: 15 216 | Earliest Data: 2024-01-01 00:00:00 UTC 217 | Latest Data: 2024-08-09 23:59:59 UTC 218 | 219 | 🎯 Available Strategies: 220 | 1) Simple Moving Average - Trading strategy based on moving average crossover 221 | 2) RSI Strategy - Trading strategy based on Relative Strength Index (RSI) 222 | 223 | Select strategy (1-2): 1 224 | ✅ Selected Strategy: Simple Moving Average 225 | 226 | 📊 Symbol Selection: 227 | 1) BTCUSDT (456,789 records) 228 | 2) ETHUSDT (234,567 records) 229 | ... 230 | 231 | Select symbol: 1 232 | ✅ Selected Symbol: BTCUSDT 233 | 234 | Enter initial capital (default: $10000): $50000 235 | Enter commission rate % (default: 0.1%): 0.1 236 | 237 | 🔍 Loading historical data: BTCUSDT latest 10000 records... 238 | ✅ Loaded 10000 data points 239 | 240 | Starting backtest... 241 | Strategy: Simple Moving Average 242 | Initial capital: $50000 243 | Progress: 100% (10000/10000) | Portfolio Value: $52,450 | P&L: $2,450 244 | 245 | BACKTEST RESULTS SUMMARY 246 | ============================================================ 247 | Strategy: Simple Moving Average 248 | Initial Capital: $50000 249 | Final Value: $52450 250 | Total P&L: $2450 251 | Return: 4.90% 252 | ... 253 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Trade 2 | 3 | A comprehensive cryptocurrency trading system with real-time data collection, advanced backtesting capabilities, and a professional desktop interface. 4 | 5 | [![Rust](https://img.shields.io/badge/rust-1.70+-orange.svg)](https://www.rust-lang.org/) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 7 | [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://tauri.app/) 8 | 9 | ## 🎯 Overview 10 | 11 | Rust Trade combines high-performance market data processing with sophisticated backtesting tools, delivering a complete solution for cryptocurrency quantitative trading. The system features real-time data collection from exchanges, a powerful backtesting engine with multiple strategies, and an intuitive desktop interface. 12 | 13 | ## 🏗️ Architecture 14 | 15 | ### **Live Data Collection Mode** 16 | ``` 17 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 18 | │ Exchange │───▶│ Service │───▶│ Repository │ 19 | │ (WebSocket) │ │ (Processing) │ │ (Storage) │ 20 | └─────────────────┘ └─────────────────┘ └─────────────────┘ 21 | │ │ │ 22 | │ ▼ ▼ 23 | Binance API ┌─────────────┐ ┌─────────────┐ 24 | - Real-time data │ Multi-Level │ │ PostgreSQL │ 25 | - Paper trading │ Cache │ │ Database │ 26 | │ (L1 + L2) │ │ │ 27 | └─────────────┘ └─────────────┘ 28 | │ 29 | ▼ 30 | ┌─────────────────┐ 31 | │ Paper Trading │ 32 | │ Engine │ 33 | └─────────────────┘ 34 | ``` 35 | 36 | ### **Desktop Application Mode** 37 | ``` 38 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 39 | │ Next.js │───▶│ Tauri Commands │───▶│ Trading Core │ 40 | │ Frontend │ │ (src-tauri) │ │ (Library) │ 41 | └─────────────────┘ └─────────────────┘ └─────────────────┘ 42 | │ │ 43 | │ ▼ 44 | │ ┌─────────────────┐ 45 | │ │ Repository │ 46 | │ │ + Database │ 47 | │ └─────────────────┘ 48 | ▼ 49 | ┌─────────────────┐ 50 | │ Backtest Engine │ 51 | │ + Strategies │ 52 | └─────────────────┘ 53 | ``` 54 | 55 | ## 📁 Project Structure 56 | ``` 57 | rust-trade/ 58 | ├── assets/ # Project assets and screenshots 59 | ├── config/ # Global configuration files 60 | │ ├── development.toml # Development environment config 61 | │ ├── production.toml # Production environment config 62 | │ └── test.toml # Test environment config 63 | ├── frontend/ # Next.js frontend application 64 | │ ├── src/ # Frontend source code 65 | │ │ ├── app/ # App router pages 66 | │ │ │ ├── page.tsx # Dashboard homepage 67 | │ │ │ └── backtest/ # Backtesting interface 68 | │ │ ├── components/ # Reusable UI components 69 | │ │ │ ├── layout/ # Layout components 70 | │ │ │ └── ui/ # shadcn/ui components 71 | │ │ └── types/ # TypeScript type definitions 72 | │ ├── tailwind.config.js # Tailwind CSS configuration 73 | │ └── package.json # Frontend dependencies 74 | ├── src-tauri/ # Desktop application backend 75 | │ ├── src/ # Tauri command handlers and state management 76 | │ │ ├── commands.rs # Tauri command implementations 77 | │ │ ├── main.rs # Application entry point 78 | │ │ ├── state.rs # Application state management 79 | │ │ └── types.rs # Frontend interface types 80 | │ ├── Cargo.toml # Tauri dependencies 81 | │ └── tauri.conf.json # Tauri configuration 82 | ├── trading-core/ # Core Rust trading system 83 | │ ├── src/ # Trading engine source code 84 | │ │ ├── backtest/ # Backtesting engine and strategies 85 | │ │ │ ├── engine.rs # Core backtesting logic 86 | │ │ │ ├── metrics.rs # Performance calculations 87 | │ │ │ ├── portfolio.rs # Portfolio management 88 | │ │ │ └── strategy/ # Trading strategies (RSI, SMA) 89 | │ │ ├── data/ # Data layer 90 | │ │ │ ├── cache.rs # Multi-level caching system 91 | │ │ │ ├── repository.rs # Database operations 92 | │ │ │ └── types.rs # Core data structures 93 | │ │ ├── exchange/ # Exchange integrations 94 | │ │ │ └── binance.rs # Binance WebSocket client 95 | │ │ ├── live_trading/ # Paper trading system 96 | │ │ │ └── paper_trading.rs # Real-time strategy execution 97 | │ │ ├── service/ # Business logic layer 98 | │ │ │ └── market_data.rs # Data processing service 99 | │ │ ├── config.rs # Configuration management 100 | │ │ ├── lib.rs # Library entry point 101 | │ │ └── main.rs # CLI application entry point 102 | │ ├── config/ # Configuration files 103 | │ ├── database/ # Database schema and migrations 104 | │ │ └── schema.sql # PostgreSQL table definitions 105 | │ ├── benches/ # Performance benchmarks 106 | │ ├── Cargo.toml # Core dependencies 107 | │ └── README.md # Core system documentation 108 | └── README.md # This file 109 | ``` 110 | 111 | ## 🚀 Quick Start 112 | 113 | ### Prerequisites 114 | 115 | - **Rust 1.70+** - [Install Rust](https://rustup.rs/) 116 | - **Node.js 18+** - [Install Node.js](https://nodejs.org/) 117 | - **PostgreSQL 12+** - [Install PostgreSQL](https://www.postgresql.org/download/) 118 | - **Redis 6+** - [Install Redis](https://redis.io/download/) (optional but recommended) 119 | 120 | ### 1. Clone the Repository 121 | 122 | ```bash 123 | git clone https://github.com/Erio-Harrison/rust-trade.git 124 | cd rust-trade 125 | ``` 126 | 127 | ### 2. Database Setup 128 | 129 | ```bash 130 | # Create database 131 | createdb trading_core 132 | 133 | # Set up schema 134 | Run the SQL commands found in the config folder to create the database tables. 135 | ``` 136 | 137 | ### 3. Environment Configuration 138 | 139 | Create `.env` files in both root directory and `trading-core/`: 140 | 141 | ```bash 142 | # .env 143 | DATABASE_URL=postgresql://username:password@localhost/trading_core 144 | REDIS_URL=redis://127.0.0.1:6379 145 | RUN_MODE=development 146 | ``` 147 | 148 | ### 4. Install Dependencies 149 | 150 | ```bash 151 | # Install Rust dependencies 152 | cd trading-core 153 | cargo build 154 | cd .. 155 | 156 | # Install frontend dependencies 157 | cd frontend 158 | npm install 159 | cd .. 160 | 161 | # Install Tauri dependencies 162 | cd src-tauri 163 | cargo build 164 | cd .. 165 | ``` 166 | 167 | PS: 168 | 169 | ## 🎮 Running the Application 170 | 171 | ### Option 1: Desktop Application (Recommended) 172 | 173 | ```bash 174 | # Development mode with hot reload 175 | cd frontend && npm run tauri dev 176 | # or alternatively 177 | cd frontend && cargo tauri dev 178 | 179 | # Production build 180 | cd frontend && npm run tauri build 181 | # or alternatively 182 | cd frontend && cargo tauri build 183 | ``` 184 | 185 | ### Option 2: Core Trading System (CLI) 186 | 187 | ```bash 188 | cd trading-core 189 | 190 | # Start live data collection 191 | cargo run live 192 | 193 | # Start live data collection with paper trading 194 | cargo run live --paper-trading 195 | 196 | # Run backtesting interface 197 | cargo run backtest 198 | 199 | # Show help 200 | cargo run -- --help 201 | ``` 202 | 203 | ### Option 3: Web Interface Only 204 | 205 | ```bash 206 | cd frontend 207 | 208 | # Development server 209 | npm run dev 210 | 211 | # Production build 212 | npm run build 213 | npm start 214 | ``` 215 | 216 | ## 📊 Features 217 | 218 | ### **Live Data Collection** 219 | - Real-time WebSocket connections to cryptocurrency exchanges 220 | - High-performance data processing (~390µs single insert, ~13ms batch) 221 | - Multi-level caching with Redis and in-memory storage 222 | - Automatic retry mechanisms and error handling 223 | 224 | ### **Advanced Backtesting** 225 | - Multiple trading strategies (SMA, RSI) 226 | - Professional performance metrics (Sharpe ratio, drawdown, win rate) 227 | - Portfolio management with P&L tracking 228 | - Interactive parameter configuration 229 | 230 | ### **Desktop Interface** 231 | - Real-time data visualization 232 | - Intuitive strategy configuration 233 | - Comprehensive result analysis 234 | - Cross-platform support (Windows, macOS, Linux) 235 | 236 | ## 🖼️ Screenshots 237 | 238 | ### Backtest Configuration 239 | ![Backtest Configuration](assets/backtestPage1.png) 240 | 241 | ### Results Dashboard 242 | ![Results Dashboard](assets/backtestPage2.png) 243 | 244 | ### Trade Analysis 245 | ![Trade Analysis](assets/backtestPage3.png) 246 | 247 | ## ⚙️ Configuration 248 | 249 | ### Trading Symbols 250 | 251 | Edit `config/development.toml`: 252 | 253 | ```toml 254 | # Trading pairs to monitor 255 | symbols = ["BTCUSDT", "ETHUSDT", "ADAUSDT"] 256 | 257 | [server] 258 | host = "0.0.0.0" 259 | port = 8080 260 | 261 | [database] 262 | max_connections = 5 263 | min_connections = 1 264 | max_lifetime = 1800 265 | 266 | [cache] 267 | [cache.memory] 268 | max_ticks_per_symbol = 1000 269 | ttl_seconds = 300 270 | 271 | [cache.redis] 272 | pool_size = 10 273 | ttl_seconds = 3600 274 | max_ticks_per_symbol = 10000 275 | ``` 276 | 277 | ### Logging 278 | 279 | Set log levels via environment variables: 280 | 281 | ```bash 282 | # Application logs 283 | RUST_LOG=trading_core=info 284 | 285 | # Debug mode 286 | RUST_LOG=trading_core=debug,sqlx=info 287 | ``` 288 | 289 | ## 📈 Performance 290 | 291 | Based on comprehensive benchmarks: 292 | 293 | | Operation | Performance | Use Case | 294 | |-----------|-------------|----------| 295 | | Single tick insert | ~390µs | Real-time data | 296 | | Batch insert (100) | ~13ms | Bulk processing | 297 | | Cache hit | ~10µs | Data retrieval | 298 | | Historical query | ~450µs | Backtesting | 299 | 300 | ## 🔧 Development 301 | 302 | ### Running Tests 303 | 304 | ```bash 305 | # Core system tests 306 | cd trading-core 307 | cargo test 308 | 309 | # Benchmarks 310 | cargo bench 311 | 312 | # Frontend tests 313 | cd frontend 314 | npm test 315 | ``` 316 | 317 | ### Building for Production 318 | 319 | ```bash 320 | # Build trading core 321 | cd trading-core 322 | cargo build --release 323 | 324 | # Build desktop app 325 | cd ../frontend 326 | npm run tauri build 327 | 328 | # Build web interface 329 | npm run build 330 | ``` 331 | 332 | ## 📚 Documentation 333 | 334 | - **Trading Core**: See `trading-core/README.md` for detailed backend documentation 335 | - **Desktop App**: See `src-tauri/README.md` for Tauri application details 336 | 337 | ## 🤝 Contributing 338 | 339 | 1. Fork the repository 340 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 341 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 342 | 4. Push to the branch (`git push origin feature/amazing-feature`) 343 | 5. Open a Pull Request 344 | 345 | ## 📄 License 346 | 347 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 348 | 349 | ## 👨‍💻 Author 350 | 351 | **Erio Harrison** - [GitHub](https://github.com/Erio-Harrison) 352 | 353 | 354 | --- 355 | 356 | Built with ❤️ using Rust, Tauri, and Next.js -------------------------------------------------------------------------------- /trading-core/src/data/types.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Datelike, Duration, Timelike, Utc}; 2 | use rust_decimal::Decimal; 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | 6 | // ================================================================= 7 | // Core data type: completely corresponds to the tick_data table structure 8 | // ================================================================= 9 | 10 | /// Trading direction enumeration 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 12 | #[serde(rename_all = "UPPERCASE")] 13 | pub enum TradeSide { 14 | Buy, 15 | Sell, 16 | } 17 | 18 | /// Standard trading data structure - corresponds one-to-one with the tick_data table fields 19 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 20 | pub struct TickData { 21 | /// UTC timestamp, supports millisecond precision 22 | pub timestamp: DateTime, 23 | 24 | /// Trading pair, such as "BTCUSDT" 25 | pub symbol: String, 26 | 27 | /// Trading price 28 | pub price: Decimal, 29 | 30 | /// Trading quantity 31 | pub quantity: Decimal, 32 | 33 | /// Trading direction 34 | pub side: TradeSide, 35 | 36 | /// Original transaction ID 37 | pub trade_id: String, 38 | 39 | /// Whether the buyer is the maker 40 | pub is_buyer_maker: bool, 41 | } 42 | 43 | impl TickData { 44 | /// New TickData 45 | pub fn new( 46 | timestamp: DateTime, 47 | symbol: String, 48 | price: Decimal, 49 | quantity: Decimal, 50 | side: TradeSide, 51 | trade_id: String, 52 | is_buyer_maker: bool, 53 | ) -> Self { 54 | Self { 55 | timestamp, 56 | symbol, 57 | price, 58 | quantity, 59 | side, 60 | trade_id, 61 | is_buyer_maker, 62 | } 63 | } 64 | } 65 | 66 | // ================================================================= 67 | // Helper Types 68 | // ================================================================= 69 | 70 | /// Database statistics 71 | #[derive(Debug, Clone)] 72 | pub struct DbStats { 73 | pub symbol: Option, 74 | pub total_records: u64, 75 | pub earliest_timestamp: Option>, 76 | pub latest_timestamp: Option>, 77 | } 78 | 79 | // ================================================================= 80 | // TradeSide Implementation for Database Integration 81 | // ================================================================= 82 | 83 | impl TradeSide { 84 | /// Convert to database string representation 85 | pub fn as_db_str(&self) -> &'static str { 86 | match self { 87 | TradeSide::Buy => "BUY", 88 | TradeSide::Sell => "SELL", 89 | } 90 | } 91 | } 92 | 93 | // ================================================================= 94 | // Query parameter type 95 | // ================================================================= 96 | 97 | /// TickData Query parameters 98 | #[derive(Debug, Clone)] 99 | pub struct TickQuery { 100 | pub symbol: String, 101 | pub start_time: Option>, 102 | pub end_time: Option>, 103 | pub limit: Option, 104 | pub trade_side: Option, 105 | } 106 | 107 | impl TickQuery { 108 | pub fn new(symbol: String) -> Self { 109 | Self { 110 | symbol, 111 | start_time: None, 112 | end_time: None, 113 | limit: None, 114 | trade_side: None, 115 | } 116 | } 117 | } 118 | 119 | // ================================================================= 120 | // Error type definition 121 | // ================================================================= 122 | 123 | #[derive(Error, Debug)] 124 | pub enum DataError { 125 | #[error("Database error: {0}")] 126 | Database(#[from] sqlx::Error), 127 | 128 | #[error("Invalid data format: {0}")] 129 | InvalidFormat(String), 130 | 131 | #[error("Data not found: {0}")] 132 | NotFound(String), 133 | 134 | #[error("Validation error: {0}")] 135 | Validation(String), 136 | 137 | #[error("Serialization error: {0}")] 138 | Serialization(#[from] serde_json::Error), 139 | 140 | #[error("Decimal conversion error: {0}")] 141 | DecimalConversion(#[from] rust_decimal::Error), 142 | 143 | #[error("Cache error: {0}")] 144 | Cache(String), 145 | 146 | #[error("Configuration error: {0}")] 147 | Config(String), 148 | } 149 | 150 | pub type DataResult = Result; 151 | 152 | /// Backtest data information for user selection 153 | #[derive(Debug, Clone)] 154 | pub struct BacktestDataInfo { 155 | pub total_records: u64, 156 | pub symbols_count: u64, 157 | pub earliest_time: Option>, 158 | pub latest_time: Option>, 159 | pub symbol_info: Vec, 160 | } 161 | 162 | /// Per-symbol data information 163 | #[derive(Debug, Clone)] 164 | pub struct SymbolDataInfo { 165 | pub symbol: String, 166 | pub records_count: u64, 167 | pub earliest_time: Option>, 168 | pub latest_time: Option>, 169 | pub min_price: Option, 170 | pub max_price: Option, 171 | } 172 | 173 | impl BacktestDataInfo { 174 | /// Get information for a specific symbol 175 | pub fn get_symbol_info(&self, symbol: &str) -> Option<&SymbolDataInfo> { 176 | self.symbol_info.iter().find(|info| info.symbol == symbol) 177 | } 178 | 179 | /// Get available symbols 180 | pub fn get_available_symbols(&self) -> Vec { 181 | self.symbol_info 182 | .iter() 183 | .map(|info| info.symbol.clone()) 184 | .collect() 185 | } 186 | 187 | /// Check if has sufficient data for backtesting 188 | pub fn has_sufficient_data(&self, symbol: &str, min_records: u64) -> bool { 189 | self.get_symbol_info(symbol) 190 | .map(|info| info.records_count >= min_records) 191 | .unwrap_or(false) 192 | } 193 | } 194 | 195 | #[derive(Debug, Clone, Serialize, Deserialize)] 196 | pub struct LiveStrategyLog { 197 | pub timestamp: DateTime, 198 | pub strategy_id: String, 199 | pub symbol: String, 200 | pub current_price: Decimal, 201 | pub signal_type: String, // BUY/SELL/HOLD 202 | pub portfolio_value: Decimal, 203 | pub total_pnl: Decimal, 204 | pub cache_hit: bool, 205 | pub processing_time_us: u64, 206 | } 207 | 208 | /// Time frame for OHLC data 209 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 210 | pub enum Timeframe { 211 | OneMinute, 212 | FiveMinutes, 213 | FifteenMinutes, 214 | ThirtyMinutes, 215 | OneHour, 216 | FourHours, 217 | OneDay, 218 | OneWeek, 219 | } 220 | 221 | impl Timeframe { 222 | pub fn as_duration(&self) -> Duration { 223 | match self { 224 | Timeframe::OneMinute => Duration::minutes(1), 225 | Timeframe::FiveMinutes => Duration::minutes(5), 226 | Timeframe::FifteenMinutes => Duration::minutes(15), 227 | Timeframe::ThirtyMinutes => Duration::minutes(30), 228 | Timeframe::OneHour => Duration::hours(1), 229 | Timeframe::FourHours => Duration::hours(4), 230 | Timeframe::OneDay => Duration::days(1), 231 | Timeframe::OneWeek => Duration::weeks(1), 232 | } 233 | } 234 | 235 | pub fn as_str(&self) -> &'static str { 236 | match self { 237 | Timeframe::OneMinute => "1m", 238 | Timeframe::FiveMinutes => "5m", 239 | Timeframe::FifteenMinutes => "15m", 240 | Timeframe::ThirtyMinutes => "30m", 241 | Timeframe::OneHour => "1h", 242 | Timeframe::FourHours => "4h", 243 | Timeframe::OneDay => "1d", 244 | Timeframe::OneWeek => "1w", 245 | } 246 | } 247 | 248 | /// Get the start of the time window for a given timestamp 249 | pub fn align_timestamp(&self, timestamp: DateTime) -> DateTime { 250 | match self { 251 | Timeframe::OneMinute => timestamp 252 | .with_second(0) 253 | .unwrap() 254 | .with_nanosecond(0) 255 | .unwrap(), 256 | Timeframe::FiveMinutes => { 257 | let aligned_minute = (timestamp.minute() / 5) * 5; 258 | timestamp 259 | .with_minute(aligned_minute) 260 | .unwrap() 261 | .with_second(0) 262 | .unwrap() 263 | .with_nanosecond(0) 264 | .unwrap() 265 | } 266 | Timeframe::FifteenMinutes => { 267 | let aligned_minute = (timestamp.minute() / 15) * 15; 268 | timestamp 269 | .with_minute(aligned_minute) 270 | .unwrap() 271 | .with_second(0) 272 | .unwrap() 273 | .with_nanosecond(0) 274 | .unwrap() 275 | } 276 | Timeframe::ThirtyMinutes => { 277 | let aligned_minute = (timestamp.minute() / 30) * 30; 278 | timestamp 279 | .with_minute(aligned_minute) 280 | .unwrap() 281 | .with_second(0) 282 | .unwrap() 283 | .with_nanosecond(0) 284 | .unwrap() 285 | } 286 | Timeframe::OneHour => timestamp 287 | .with_minute(0) 288 | .unwrap() 289 | .with_second(0) 290 | .unwrap() 291 | .with_nanosecond(0) 292 | .unwrap(), 293 | Timeframe::FourHours => { 294 | let aligned_hour = (timestamp.hour() / 4) * 4; 295 | timestamp 296 | .with_hour(aligned_hour) 297 | .unwrap() 298 | .with_minute(0) 299 | .unwrap() 300 | .with_second(0) 301 | .unwrap() 302 | .with_nanosecond(0) 303 | .unwrap() 304 | } 305 | Timeframe::OneDay => timestamp 306 | .with_hour(0) 307 | .unwrap() 308 | .with_minute(0) 309 | .unwrap() 310 | .with_second(0) 311 | .unwrap() 312 | .with_nanosecond(0) 313 | .unwrap(), 314 | Timeframe::OneWeek => { 315 | let days_from_monday = timestamp.weekday().num_days_from_monday(); 316 | let week_start = timestamp - Duration::days(days_from_monday as i64); 317 | week_start 318 | .with_hour(0) 319 | .unwrap() 320 | .with_minute(0) 321 | .unwrap() 322 | .with_second(0) 323 | .unwrap() 324 | .with_nanosecond(0) 325 | .unwrap() 326 | } 327 | } 328 | } 329 | } 330 | 331 | /// OHLC data structure 332 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 333 | pub struct OHLCData { 334 | pub timestamp: DateTime, 335 | pub symbol: String, 336 | pub timeframe: Timeframe, 337 | pub open: Decimal, 338 | pub high: Decimal, 339 | pub low: Decimal, 340 | pub close: Decimal, 341 | pub volume: Decimal, 342 | pub trade_count: u64, 343 | } 344 | 345 | impl OHLCData { 346 | pub fn new( 347 | timestamp: DateTime, 348 | symbol: String, 349 | timeframe: Timeframe, 350 | open: Decimal, 351 | high: Decimal, 352 | low: Decimal, 353 | close: Decimal, 354 | volume: Decimal, 355 | trade_count: u64, 356 | ) -> Self { 357 | Self { 358 | timestamp, 359 | symbol, 360 | timeframe, 361 | open, 362 | high, 363 | low, 364 | close, 365 | volume, 366 | trade_count, 367 | } 368 | } 369 | 370 | /// Create OHLC from a collection of tick data 371 | pub fn from_ticks( 372 | ticks: &[TickData], 373 | timeframe: Timeframe, 374 | window_start: DateTime, 375 | ) -> Option { 376 | if ticks.is_empty() { 377 | return None; 378 | } 379 | 380 | let symbol = ticks[0].symbol.clone(); 381 | let mut open = ticks[0].price; 382 | let mut high = ticks[0].price; 383 | let mut low = ticks[0].price; 384 | let mut close = ticks[ticks.len() - 1].price; 385 | let mut volume = Decimal::ZERO; 386 | 387 | for tick in ticks { 388 | if tick.price > high { 389 | high = tick.price; 390 | } 391 | if tick.price < low { 392 | low = tick.price; 393 | } 394 | volume += tick.quantity; 395 | } 396 | 397 | Some(OHLCData::new( 398 | window_start, 399 | symbol, 400 | timeframe, 401 | open, 402 | high, 403 | low, 404 | close, 405 | volume, 406 | ticks.len() as u64, 407 | )) 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src-tauri/src/commands.rs: -------------------------------------------------------------------------------- 1 | use crate::state::AppState; 2 | use crate::types::*; 3 | use tauri::State; 4 | use trading_core::{ 5 | backtest::{ 6 | engine::{BacktestEngine, BacktestConfig}, 7 | strategy::create_strategy, 8 | }, 9 | data::types::TradeSide, 10 | }; 11 | use rust_decimal::Decimal; 12 | use trading_core::backtest::engine::BacktestResult; 13 | 14 | use std::str::FromStr; 15 | use tracing::{info, error}; 16 | 17 | #[tauri::command] 18 | pub async fn get_data_info( 19 | state: State<'_, AppState>, 20 | ) -> Result { 21 | info!("Getting backtest data info"); 22 | 23 | let data_info = state.repository 24 | .get_backtest_data_info() 25 | .await 26 | .map_err(|e| { 27 | error!("Failed to get data info: {}", e); 28 | e.to_string() 29 | })?; 30 | 31 | let response = DataInfoResponse { 32 | total_records: data_info.total_records, 33 | symbols_count: data_info.symbols_count, 34 | earliest_time: data_info.earliest_time.map(|t| t.to_rfc3339()), 35 | latest_time: data_info.latest_time.map(|t| t.to_rfc3339()), 36 | symbol_info: data_info.symbol_info.into_iter().map(|info| SymbolInfo { 37 | symbol: info.symbol, 38 | records_count: info.records_count, 39 | earliest_time: info.earliest_time.map(|t| t.to_rfc3339()), 40 | latest_time: info.latest_time.map(|t| t.to_rfc3339()), 41 | min_price: info.min_price.map(|p| p.to_string()), 42 | max_price: info.max_price.map(|p| p.to_string()), 43 | }).collect(), 44 | }; 45 | 46 | info!("Data info retrieved successfully: {} symbols, {} total records", 47 | response.symbols_count, response.total_records); 48 | Ok(response) 49 | } 50 | 51 | #[tauri::command] 52 | pub async fn get_available_strategies() -> Result, String> { 53 | info!("Getting available strategies"); 54 | 55 | let strategies = trading_core::backtest::strategy::list_strategies(); 56 | let response: Vec = strategies.into_iter().map(|s| StrategyInfo { 57 | id: s.id, 58 | name: s.name, 59 | description: s.description, 60 | }).collect(); 61 | 62 | info!("Retrieved {} strategies", response.len()); 63 | Ok(response) 64 | } 65 | 66 | #[tauri::command] 67 | pub async fn validate_backtest_config( 68 | state: State<'_, AppState>, 69 | symbol: String, 70 | data_count: i64, 71 | ) -> Result { 72 | info!("Validating backtest config for symbol: {}, data_count: {}", symbol, data_count); 73 | 74 | let data_info = state.repository 75 | .get_backtest_data_info() 76 | .await 77 | .map_err(|e| e.to_string())?; 78 | 79 | let is_valid = data_info.has_sufficient_data(&symbol, data_count as u64); 80 | info!("Validation result: {}", is_valid); 81 | 82 | Ok(is_valid) 83 | } 84 | 85 | #[tauri::command] 86 | pub async fn get_historical_data( 87 | state: State<'_, AppState>, 88 | request: HistoricalDataRequest, 89 | ) -> Result, String> { 90 | info!("Getting historical data for symbol: {}, limit: {:?}", 91 | request.symbol, request.limit); 92 | 93 | let limit = request.limit.unwrap_or(1000).min(10000); 94 | let data = state.repository 95 | .get_recent_ticks_for_backtest(&request.symbol, limit) 96 | .await 97 | .map_err(|e| { 98 | error!("Failed to get historical data: {}", e); 99 | e.to_string() 100 | })?; 101 | 102 | let response: Vec = data.into_iter().map(|tick| TickDataResponse { 103 | timestamp: tick.timestamp.to_rfc3339(), 104 | symbol: tick.symbol, 105 | price: tick.price.to_string(), 106 | quantity: tick.quantity.to_string(), 107 | side: match tick.side { 108 | TradeSide::Buy => "Buy".to_string(), 109 | TradeSide::Sell => "Sell".to_string(), 110 | }, 111 | }).collect(); 112 | 113 | info!("Retrieved {} historical data points", response.len()); 114 | Ok(response) 115 | } 116 | 117 | #[tauri::command] 118 | pub async fn run_backtest( 119 | state: State<'_, AppState>, 120 | request: BacktestRequest, 121 | ) -> Result { 122 | info!("Starting backtest: strategy={}, symbol={}, data_count={}", 123 | request.strategy_id, request.symbol, request.data_count); 124 | 125 | let initial_capital = Decimal::from_str(&request.initial_capital) 126 | .map_err(|_| "Invalid initial capital")?; 127 | let commission_rate = Decimal::from_str(&request.commission_rate) 128 | .map_err(|_| "Invalid commission rate")?; 129 | 130 | let mut config = BacktestConfig::new(initial_capital) 131 | .with_commission_rate(commission_rate); 132 | 133 | for (key, value) in request.strategy_params { 134 | config = config.with_param(&key, &value); 135 | } 136 | 137 | info!("Creating strategy: {}", request.strategy_id); 138 | let temp_strategy = create_strategy(&request.strategy_id) 139 | .map_err(|e| { 140 | error!("Failed to create strategy: {}", e); 141 | e 142 | })?; 143 | 144 | let mut data_source = "tick".to_string(); 145 | 146 | // Check if strategy supports OHLC 147 | if temp_strategy.supports_ohlc() { 148 | if let Some(timeframe) = temp_strategy.preferred_timeframe() { 149 | info!("Strategy supports OHLC, attempting {} timeframe", timeframe.as_str()); 150 | 151 | // Estimate candle count (roughly data_count / 50, minimum 100) 152 | let candle_count = (request.data_count / 50).max(100) as u32; 153 | 154 | match state.repository.generate_recent_ohlc_for_backtest( 155 | &request.symbol, 156 | timeframe, 157 | candle_count 158 | ).await { 159 | Ok(ohlc_data) if !ohlc_data.is_empty() => { 160 | info!("Generated {} OHLC candles, running OHLC backtest", ohlc_data.len()); 161 | data_source = format!("OHLC-{}", timeframe.as_str()); 162 | 163 | let strategy = create_strategy(&request.strategy_id)?; 164 | let mut engine = BacktestEngine::new(strategy, config) 165 | .map_err(|e| { 166 | error!("Failed to create backtest engine: {}", e); 167 | e 168 | })?; 169 | 170 | let result = engine.run_with_ohlc(ohlc_data); 171 | return Ok(create_backtest_response(result, data_source)); 172 | }, 173 | Ok(_) => { 174 | info!("No OHLC data available, falling back to tick data"); 175 | }, 176 | Err(e) => { 177 | info!("OHLC generation failed: {}, falling back to tick data", e); 178 | } 179 | } 180 | } 181 | } 182 | 183 | // Fallback to tick data 184 | info!("Loading tick data for backtest"); 185 | let data = state.repository 186 | .get_recent_ticks_for_backtest(&request.symbol, request.data_count) 187 | .await 188 | .map_err(|e| { 189 | error!("Failed to load historical data: {}", e); 190 | e.to_string() 191 | })?; 192 | 193 | if data.is_empty() { 194 | return Err("No historical data available for the specified symbol".to_string()); 195 | } 196 | 197 | info!("Loaded {} tick data points, running tick backtest", data.len()); 198 | 199 | let strategy = create_strategy(&request.strategy_id)?; 200 | let mut engine = BacktestEngine::new(strategy, config) 201 | .map_err(|e| { 202 | error!("Failed to create backtest engine: {}", e); 203 | e 204 | })?; 205 | 206 | let result = engine.run(data); 207 | Ok(create_backtest_response(result, data_source)) 208 | } 209 | 210 | // 3. Add helper function to commands.rs 211 | fn create_backtest_response(result: BacktestResult, data_source: String) -> BacktestResponse { 212 | info!("Backtest completed successfully"); 213 | 214 | BacktestResponse { 215 | strategy_name: result.strategy_name.clone(), 216 | initial_capital: result.initial_capital.to_string(), 217 | final_value: result.final_value.to_string(), 218 | total_pnl: result.total_pnl.to_string(), 219 | return_percentage: result.return_percentage.to_string(), 220 | total_trades: result.total_trades, 221 | winning_trades: result.winning_trades, 222 | losing_trades: result.losing_trades, 223 | max_drawdown: result.max_drawdown.to_string(), 224 | sharpe_ratio: result.sharpe_ratio.to_string(), 225 | volatility: result.volatility.to_string(), 226 | win_rate: result.win_rate.to_string(), 227 | profit_factor: result.profit_factor.to_string(), 228 | total_commission: result.total_commission.to_string(), 229 | data_source, // NEW FIELD 230 | trades: result.trades.into_iter().map(|trade| TradeInfo { 231 | timestamp: trade.timestamp.to_rfc3339(), 232 | symbol: trade.symbol, 233 | side: match trade.side { 234 | trading_core::data::types::TradeSide::Buy => "Buy".to_string(), 235 | trading_core::data::types::TradeSide::Sell => "Sell".to_string(), 236 | }, 237 | quantity: trade.quantity.to_string(), 238 | price: trade.price.to_string(), 239 | realized_pnl: trade.realized_pnl.map(|pnl| pnl.to_string()), 240 | commission: trade.commission.to_string(), 241 | }).collect(), 242 | equity_curve: result.equity_curve.into_iter().map(|value| value.to_string()).collect(), 243 | } 244 | } 245 | 246 | #[tauri::command] 247 | pub async fn get_strategy_capabilities() -> Result, String> { 248 | info!("Getting strategy capabilities"); 249 | 250 | let strategies = trading_core::backtest::strategy::list_strategies(); 251 | let mut capabilities = Vec::new(); 252 | 253 | for strategy_info in strategies { 254 | // Create temporary strategy instance to check capabilities 255 | match trading_core::backtest::strategy::create_strategy(&strategy_info.id) { 256 | Ok(strategy) => { 257 | capabilities.push(StrategyCapability { 258 | id: strategy_info.id, 259 | name: strategy_info.name, 260 | description: strategy_info.description, 261 | supports_ohlc: strategy.supports_ohlc(), 262 | preferred_timeframe: strategy.preferred_timeframe().map(|tf| tf.as_str().to_string()), 263 | }); 264 | } 265 | Err(e) => { 266 | info!("Failed to create strategy {}: {}", strategy_info.id, e); 267 | capabilities.push(StrategyCapability { 268 | id: strategy_info.id, 269 | name: strategy_info.name, 270 | description: strategy_info.description, 271 | supports_ohlc: false, 272 | preferred_timeframe: None, 273 | }); 274 | } 275 | } 276 | } 277 | 278 | info!("Retrieved capabilities for {} strategies", capabilities.len()); 279 | Ok(capabilities) 280 | } 281 | 282 | #[tauri::command] 283 | pub async fn get_ohlc_preview( 284 | state: State<'_, AppState>, 285 | request: OHLCRequest, 286 | ) -> Result, String> { 287 | info!("Getting OHLC preview: {} {} count={}", 288 | request.symbol, request.timeframe, request.count); 289 | 290 | let timeframe = match request.timeframe.as_str() { 291 | "1m" => trading_core::data::types::Timeframe::OneMinute, 292 | "5m" => trading_core::data::types::Timeframe::FiveMinutes, 293 | "15m" => trading_core::data::types::Timeframe::FifteenMinutes, 294 | "30m" => trading_core::data::types::Timeframe::ThirtyMinutes, 295 | "1h" => trading_core::data::types::Timeframe::OneHour, 296 | "4h" => trading_core::data::types::Timeframe::FourHours, 297 | "1d" => trading_core::data::types::Timeframe::OneDay, 298 | "1w" => trading_core::data::types::Timeframe::OneWeek, 299 | _ => return Err(format!("Invalid timeframe: {}", request.timeframe)), 300 | }; 301 | 302 | let ohlc_data = state.repository 303 | .generate_recent_ohlc_for_backtest(&request.symbol, timeframe, request.count) 304 | .await 305 | .map_err(|e| { 306 | error!("Failed to generate OHLC preview: {}", e); 307 | e.to_string() 308 | })?; 309 | 310 | if ohlc_data.is_empty() { 311 | return Err("No OHLC data available for the specified parameters".to_string()); 312 | } 313 | 314 | let response: Vec = ohlc_data.into_iter().map(|ohlc| OHLCPreview { 315 | timestamp: ohlc.timestamp.to_rfc3339(), 316 | symbol: ohlc.symbol, 317 | open: ohlc.open.to_string(), 318 | high: ohlc.high.to_string(), 319 | low: ohlc.low.to_string(), 320 | close: ohlc.close.to_string(), 321 | volume: ohlc.volume.to_string(), 322 | trade_count: ohlc.trade_count, 323 | }).collect(); 324 | 325 | info!("Generated {} OHLC preview records", response.len()); 326 | Ok(response) 327 | } -------------------------------------------------------------------------------- /trading-core/src/exchange/binance.rs: -------------------------------------------------------------------------------- 1 | // ================================================================= 2 | // exchange/binance.rs - Binance Exchange Implementation 3 | // ================================================================= 4 | 5 | use async_trait::async_trait; 6 | use futures_util::{SinkExt, StreamExt}; 7 | use serde_json::json; 8 | use std::time::Duration; 9 | use tokio::time::sleep; 10 | use tokio_tungstenite::{connect_async, tungstenite::Message}; 11 | use tracing::{debug, error, info, warn}; 12 | 13 | use super::{ 14 | errors::ExchangeError, 15 | traits::Exchange, 16 | types::{ 17 | BinanceStreamMessage, BinanceSubscribeMessage, BinanceTradeMessage, HistoricalTradeParams, 18 | }, 19 | utils::{build_binance_trade_streams, convert_binance_to_tick_data}, 20 | }; 21 | use crate::data::types::TickData; 22 | 23 | // Constants 24 | const BINANCE_WS_URL: &str = "wss://stream.binance.com:9443/stream"; 25 | const BINANCE_API_URL: &str = "https://api.binance.com"; 26 | const RECONNECT_DELAY: Duration = Duration::from_secs(5); 27 | const PING_INTERVAL: Duration = Duration::from_secs(30); 28 | 29 | /// Binance exchange implementation 30 | pub struct BinanceExchange { 31 | ws_url: String, 32 | api_url: String, 33 | client: reqwest::Client, 34 | } 35 | 36 | impl BinanceExchange { 37 | /// Create a new Binance exchange instance 38 | pub fn new() -> Self { 39 | Self { 40 | ws_url: BINANCE_WS_URL.to_string(), 41 | api_url: BINANCE_API_URL.to_string(), 42 | client: reqwest::Client::new(), 43 | } 44 | } 45 | 46 | /// Parse WebSocket message and extract trade data 47 | fn parse_trade_message(&self, text: &str) -> Result { 48 | // First try to parse as stream message (combined streams format) 49 | if let Ok(stream_msg) = serde_json::from_str::(text) { 50 | return convert_binance_to_tick_data(stream_msg.data); 51 | } 52 | 53 | // Fallback: try to parse as direct trade message 54 | if let Ok(trade_msg) = serde_json::from_str::(text) { 55 | return convert_binance_to_tick_data(trade_msg); 56 | } 57 | 58 | // Check if it's a subscription confirmation or other control message 59 | if let Ok(value) = serde_json::from_str::(text) { 60 | if value.get("result").is_some() || value.get("id").is_some() { 61 | // This is a subscription confirmation, not an error 62 | debug!("Received subscription confirmation: {}", text); 63 | return Err(ExchangeError::ParseError( 64 | "Control message, not trade data".to_string(), 65 | )); 66 | } 67 | } 68 | 69 | Err(ExchangeError::ParseError(format!( 70 | "Unable to parse message: {}", 71 | text 72 | ))) 73 | } 74 | 75 | /// Handle WebSocket connection with reconnection logic 76 | async fn handle_websocket_connection( 77 | &self, 78 | symbols: &[String], 79 | callback: Box, 80 | mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, 81 | ) -> Result<(), ExchangeError> { 82 | let streams = build_binance_trade_streams(symbols)?; 83 | info!( 84 | "Connecting to Binance WebSocket with {} streams", 85 | streams.len() 86 | ); 87 | 88 | let mut reconnect_attempts = 0; 89 | const MAX_RECONNECT_ATTEMPTS: u32 = 10; 90 | 91 | loop { 92 | // Check for shutdown signal before each connection attempt 93 | if shutdown_rx.try_recv().is_ok() { 94 | info!("Shutdown signal received, stopping WebSocket connection attempts"); 95 | return Ok(()); 96 | } 97 | 98 | match self 99 | .connect_and_subscribe(&streams, &callback, shutdown_rx.resubscribe()) 100 | .await 101 | { 102 | Ok(()) => { 103 | // Reset reconnect attempts on successful connection 104 | reconnect_attempts = 0; 105 | info!( 106 | "WebSocket connection ended normally - checking if shutdown was requested" 107 | ); 108 | 109 | // If connection ended normally, it's likely due to shutdown signal 110 | // Exit the reconnection loop 111 | return Ok(()); 112 | } 113 | Err(e) => { 114 | reconnect_attempts += 1; 115 | error!( 116 | "WebSocket connection failed (attempt {}): {}", 117 | reconnect_attempts, e 118 | ); 119 | 120 | if reconnect_attempts >= MAX_RECONNECT_ATTEMPTS { 121 | return Err(ExchangeError::NetworkError(format!( 122 | "Max reconnection attempts ({}) exceeded", 123 | MAX_RECONNECT_ATTEMPTS 124 | ))); 125 | } 126 | 127 | warn!("Attempting to reconnect in {:?}...", RECONNECT_DELAY); 128 | 129 | // Wait for reconnect delay or shutdown signal 130 | tokio::select! { 131 | _ = sleep(RECONNECT_DELAY) => { 132 | // Continue to retry 133 | continue; 134 | } 135 | _ = shutdown_rx.recv() => { 136 | info!("Shutdown signal received during reconnect delay"); 137 | return Ok(()); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | /// Connect to WebSocket and handle subscription 146 | async fn connect_and_subscribe( 147 | &self, 148 | streams: &[String], 149 | callback: &Box, 150 | mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, 151 | ) -> Result<(), ExchangeError> { 152 | // Establish WebSocket connection 153 | let (ws_stream, _) = connect_async(&self.ws_url) 154 | .await 155 | .map_err(|e| ExchangeError::WebSocketError(format!("Failed to connect: {}", e)))?; 156 | 157 | debug!("WebSocket connected to {}", self.ws_url); 158 | 159 | let (mut write, mut read) = ws_stream.split(); 160 | 161 | // Send subscription message 162 | let subscribe_msg = BinanceSubscribeMessage::new(streams.to_vec()); 163 | let subscribe_json = serde_json::to_string(&subscribe_msg).map_err(|e| { 164 | ExchangeError::ParseError(format!("Failed to serialize subscription: {}", e)) 165 | })?; 166 | 167 | write 168 | .send(Message::Text(subscribe_json)) 169 | .await 170 | .map_err(|e| { 171 | ExchangeError::WebSocketError(format!("Failed to send subscription: {}", e)) 172 | })?; 173 | 174 | info!("Subscription sent for {} streams", streams.len()); 175 | 176 | // Message processing loop 177 | loop { 178 | tokio::select! { 179 | msg = read.next() => { 180 | match msg { 181 | Some(Ok(Message::Text(text))) => { 182 | // 处理文本消息 183 | match self.parse_trade_message(&text) { 184 | Ok(tick_data) => callback(tick_data), 185 | Err(e) => warn!("Parse error: {}", e), 186 | } 187 | } 188 | Some(Ok(Message::Ping(ping))) => { 189 | write.send(Message::Pong(ping)).await?; 190 | } 191 | Some(Ok(Message::Close(_))) => { 192 | info!("WebSocket closed by server"); 193 | break; 194 | } 195 | Some(Err(e)) => { 196 | return Err(ExchangeError::WebSocketError(e.to_string())); 197 | } 198 | None => { 199 | info!("WebSocket stream ended"); 200 | break; 201 | } 202 | _ => continue, 203 | } 204 | } 205 | _ = shutdown_rx.recv() => { 206 | info!("Shutdown signal received, closing WebSocket gracefully"); 207 | // 发送 Close frame 给服务器 208 | if let Err(e) = write.send(Message::Close(None)).await { 209 | warn!("Failed to send close frame: {}", e); 210 | } 211 | break; 212 | } 213 | } 214 | } 215 | 216 | Ok(()) 217 | } 218 | 219 | /// Fetch historical trades using REST API 220 | async fn fetch_historical_trades_api( 221 | &self, 222 | params: &HistoricalTradeParams, 223 | ) -> Result, ExchangeError> { 224 | let mut url = format!("{}/api/v3/aggTrades", self.api_url); 225 | url.push_str(&format!("?symbol={}", params.symbol)); 226 | 227 | if let Some(start_time) = params.start_time { 228 | url.push_str(&format!("&startTime={}", start_time.timestamp_millis())); 229 | } 230 | 231 | if let Some(end_time) = params.end_time { 232 | url.push_str(&format!("&endTime={}", end_time.timestamp_millis())); 233 | } 234 | 235 | if let Some(limit) = params.limit { 236 | // Binance API has a maximum limit of 1000 237 | let limit = limit.min(1000); 238 | url.push_str(&format!("&limit={}", limit)); 239 | } 240 | 241 | debug!("Fetching historical trades from: {}", url); 242 | 243 | let response = self 244 | .client 245 | .get(&url) 246 | .timeout(Duration::from_secs(30)) 247 | .send() 248 | .await?; 249 | 250 | if !response.status().is_success() { 251 | let status = response.status(); 252 | let error_text = response 253 | .text() 254 | .await 255 | .unwrap_or_else(|_| "Unknown error".to_string()); 256 | return Err(ExchangeError::ApiError(format!( 257 | "HTTP {}: {}", 258 | status, error_text 259 | ))); 260 | } 261 | 262 | let trades_json = response.text().await?; 263 | let trades: Vec = serde_json::from_str(&trades_json)?; 264 | 265 | let mut tick_data_vec = Vec::with_capacity(trades.len()); 266 | 267 | for trade in trades { 268 | // Parse aggregated trade data from Binance API 269 | let trade_msg = BinanceTradeMessage { 270 | symbol: params.symbol.clone(), 271 | trade_id: trade["a"].as_u64().unwrap_or(0), // Aggregate trade ID 272 | price: trade["p"].as_str().unwrap_or("0").to_string(), 273 | quantity: trade["q"].as_str().unwrap_or("0").to_string(), 274 | trade_time: trade["T"].as_u64().unwrap_or(0), 275 | is_buyer_maker: trade["m"].as_bool().unwrap_or(false), 276 | }; 277 | 278 | match convert_binance_to_tick_data(trade_msg) { 279 | Ok(tick_data) => tick_data_vec.push(tick_data), 280 | Err(e) => warn!("Failed to convert historical trade: {}", e), 281 | } 282 | } 283 | 284 | info!( 285 | "Successfully fetched {} historical trades for {}", 286 | tick_data_vec.len(), 287 | params.symbol 288 | ); 289 | Ok(tick_data_vec) 290 | } 291 | } 292 | 293 | #[async_trait] 294 | impl Exchange for BinanceExchange { 295 | async fn subscribe_trades( 296 | &self, 297 | symbols: &[String], 298 | callback: Box, 299 | shutdown_rx: tokio::sync::broadcast::Receiver<()>, 300 | ) -> Result<(), ExchangeError> { 301 | if symbols.is_empty() { 302 | return Err(ExchangeError::InvalidSymbol( 303 | "No symbols provided".to_string(), 304 | )); 305 | } 306 | 307 | info!( 308 | "Starting Binance trade subscription for symbols: {:?}", 309 | symbols 310 | ); 311 | 312 | // This will run indefinitely with reconnection logic 313 | self.handle_websocket_connection(symbols, callback, shutdown_rx.resubscribe()) 314 | .await 315 | } 316 | 317 | async fn get_historical_trades( 318 | &self, 319 | params: HistoricalTradeParams, 320 | ) -> Result, ExchangeError> { 321 | if params.symbol.is_empty() { 322 | return Err(ExchangeError::InvalidSymbol( 323 | "Symbol cannot be empty".to_string(), 324 | )); 325 | } 326 | 327 | info!("Fetching historical trades for symbol: {}", params.symbol); 328 | 329 | self.fetch_historical_trades_api(¶ms).await 330 | } 331 | } 332 | 333 | impl Default for BinanceExchange { 334 | fn default() -> Self { 335 | Self::new() 336 | } 337 | } 338 | 339 | #[cfg(test)] 340 | mod tests { 341 | use super::*; 342 | use crate::data::types::TradeSide; 343 | use chrono::Utc; 344 | use rust_decimal::Decimal; 345 | use std::str::FromStr; 346 | 347 | #[test] 348 | fn test_parse_trade_message() { 349 | let exchange = BinanceExchange::new(); 350 | 351 | // Test combined stream message format 352 | let stream_msg = r#"{ 353 | "stream": "btcusdt@trade", 354 | "data": { 355 | "e": "trade", 356 | "E": 1672515782136, 357 | "s": "BTCUSDT", 358 | "t": 12345, 359 | "p": "50000.00", 360 | "q": "0.001", 361 | "b": 88, 362 | "a": 50, 363 | "T": 1672515782136, 364 | "m": false, 365 | "M": true 366 | } 367 | }"#; 368 | 369 | let tick_data = exchange.parse_trade_message(stream_msg).unwrap(); 370 | 371 | assert_eq!(tick_data.symbol, "BTCUSDT"); 372 | assert_eq!(tick_data.price, Decimal::from_str("50000.00").unwrap()); 373 | assert_eq!(tick_data.quantity, Decimal::from_str("0.001").unwrap()); 374 | assert_eq!(tick_data.side, TradeSide::Buy); // is_buyer_maker = false -> Buy 375 | assert_eq!(tick_data.trade_id, "12345"); 376 | assert!(!tick_data.is_buyer_maker); 377 | } 378 | 379 | #[test] 380 | fn test_parse_direct_trade_message() { 381 | let exchange = BinanceExchange::new(); 382 | 383 | // Test direct trade message format 384 | let trade_msg = r#"{ 385 | "e": "trade", 386 | "E": 1672515782136, 387 | "s": "ETHUSDT", 388 | "t": 67890, 389 | "p": "3000.50", 390 | "q": "0.1", 391 | "b": 88, 392 | "a": 50, 393 | "T": 1672515782136, 394 | "m": true, 395 | "M": true 396 | }"#; 397 | 398 | let tick_data = exchange.parse_trade_message(trade_msg).unwrap(); 399 | 400 | assert_eq!(tick_data.symbol, "ETHUSDT"); 401 | assert_eq!(tick_data.price, Decimal::from_str("3000.50").unwrap()); 402 | assert_eq!(tick_data.side, TradeSide::Sell); // is_buyer_maker = true -> Sell 403 | assert!(tick_data.is_buyer_maker); 404 | } 405 | 406 | #[test] 407 | fn test_parse_subscription_confirmation() { 408 | let exchange = BinanceExchange::new(); 409 | 410 | let confirmation_msg = r#"{ 411 | "result": null, 412 | "id": 1 413 | }"#; 414 | 415 | let result = exchange.parse_trade_message(confirmation_msg); 416 | assert!(result.is_err()); 417 | 418 | // Should be a parse error indicating it's a control message 419 | if let Err(ExchangeError::ParseError(msg)) = result { 420 | assert!(msg.contains("Control message")); 421 | } else { 422 | panic!("Expected ParseError with control message indication"); 423 | } 424 | } 425 | 426 | #[tokio::test] 427 | async fn test_historical_trade_params() { 428 | let params = HistoricalTradeParams::new("BTCUSDT".to_string()) 429 | .with_limit(100) 430 | .with_time_range(Utc::now() - chrono::Duration::hours(1), Utc::now()); 431 | 432 | assert_eq!(params.symbol, "BTCUSDT"); 433 | assert_eq!(params.limit, Some(100)); 434 | assert!(params.start_time.is_some()); 435 | assert!(params.end_time.is_some()); 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /trading-core/src/data/cache.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use redis::{Client as RedisClient, Commands, Connection}; 3 | use serde_json; 4 | use std::collections::{HashMap, VecDeque}; 5 | use std::sync::{Arc, RwLock}; 6 | use std::time::{Duration, Instant}; 7 | use tokio::sync::Mutex; 8 | use tracing::{debug, error, warn}; 9 | 10 | use super::types::{DataError, DataResult, TickData}; 11 | 12 | // ================================================================= 13 | // Cache Interface Definition 14 | // ================================================================= 15 | 16 | /// TickData cache interface 17 | #[async_trait] 18 | pub trait TickDataCache: Send + Sync { 19 | /// Add new tick data 20 | async fn push_tick(&self, tick: &TickData) -> DataResult<()>; 21 | 22 | /// Get recent tick data 23 | async fn get_recent_ticks(&self, symbol: &str, limit: usize) -> DataResult>; 24 | 25 | /// Get list of cached symbols 26 | async fn get_symbols(&self) -> DataResult>; 27 | 28 | /// Clear cache for specific symbol 29 | async fn clear_symbol(&self, symbol: &str) -> DataResult<()>; 30 | 31 | /// Clear all cache 32 | async fn clear_all(&self) -> DataResult<()>; 33 | } 34 | 35 | // ================================================================= 36 | // Memory Cache Implementation 37 | // ================================================================= 38 | 39 | /// Memory cache entry for ticks 40 | #[derive(Debug, Clone)] 41 | struct MemoryCacheEntry { 42 | ticks: VecDeque, 43 | last_access: Instant, 44 | last_update: Instant, 45 | } 46 | 47 | impl MemoryCacheEntry { 48 | fn new() -> Self { 49 | let now = Instant::now(); 50 | Self { 51 | ticks: VecDeque::new(), 52 | last_access: now, 53 | last_update: now, 54 | } 55 | } 56 | 57 | fn push_tick(&mut self, tick: TickData, max_size: usize) { 58 | self.ticks.push_back(tick); 59 | self.last_update = Instant::now(); 60 | 61 | // Maintain size limit 62 | while self.ticks.len() > max_size { 63 | self.ticks.pop_front(); 64 | } 65 | } 66 | 67 | fn get_recent(&mut self, limit: usize) -> Vec { 68 | self.last_access = Instant::now(); 69 | 70 | self.ticks 71 | .iter() 72 | .rev() // Latest first 73 | .take(limit) 74 | .cloned() 75 | .collect() 76 | } 77 | 78 | fn is_expired(&self, ttl: Duration) -> bool { 79 | self.last_access.elapsed() > ttl 80 | } 81 | } 82 | 83 | /// In-memory tick cache implementation 84 | pub struct InMemoryTickCache { 85 | data: Arc>>, 86 | max_ticks_per_symbol: usize, 87 | ttl: Duration, 88 | } 89 | 90 | impl InMemoryTickCache { 91 | pub fn new(max_ticks_per_symbol: usize, ttl_seconds: u64) -> Self { 92 | Self { 93 | data: Arc::new(RwLock::new(HashMap::new())), 94 | max_ticks_per_symbol, 95 | ttl: Duration::from_secs(ttl_seconds), 96 | } 97 | } 98 | 99 | /// Clean up expired cache entries 100 | pub fn cleanup_expired(&self) { 101 | if let Ok(mut data) = self.data.write() { 102 | let expired_symbols: Vec = data 103 | .iter() 104 | .filter(|(_, entry)| entry.is_expired(self.ttl)) 105 | .map(|(symbol, _)| symbol.clone()) 106 | .collect(); 107 | 108 | for symbol in expired_symbols { 109 | data.remove(&symbol); 110 | debug!("Cleaned up expired cache for symbol: {}", symbol); 111 | } 112 | } 113 | } 114 | } 115 | 116 | #[async_trait] 117 | impl TickDataCache for InMemoryTickCache { 118 | async fn push_tick(&self, tick: &TickData) -> DataResult<()> { 119 | match self.data.write() { 120 | Ok(mut data) => { 121 | let entry = data 122 | .entry(tick.symbol.clone()) 123 | .or_insert_with(MemoryCacheEntry::new); 124 | 125 | entry.push_tick(tick.clone(), self.max_ticks_per_symbol); 126 | debug!( 127 | "Added tick to memory cache: symbol={}, price={}", 128 | tick.symbol, tick.price 129 | ); 130 | Ok(()) 131 | } 132 | Err(e) => { 133 | error!("Failed to acquire write lock for memory cache: {}", e); 134 | Err(DataError::Cache(format!("Lock error: {}", e))) 135 | } 136 | } 137 | } 138 | 139 | async fn get_recent_ticks(&self, symbol: &str, limit: usize) -> DataResult> { 140 | match self.data.write() { 141 | Ok(mut data) => { 142 | if let Some(entry) = data.get_mut(symbol) { 143 | let ticks = entry.get_recent(limit); 144 | debug!( 145 | "Retrieved {} ticks from memory cache for symbol: {}", 146 | ticks.len(), 147 | symbol 148 | ); 149 | Ok(ticks) 150 | } else { 151 | debug!("No memory cache found for symbol: {}", symbol); 152 | Ok(Vec::new()) 153 | } 154 | } 155 | Err(e) => { 156 | error!("Failed to acquire write lock for memory cache: {}", e); 157 | Err(DataError::Cache(format!("Lock error: {}", e))) 158 | } 159 | } 160 | } 161 | 162 | async fn get_symbols(&self) -> DataResult> { 163 | match self.data.read() { 164 | Ok(data) => Ok(data.keys().cloned().collect()), 165 | Err(e) => Err(DataError::Cache(format!("Lock error: {}", e))), 166 | } 167 | } 168 | 169 | async fn clear_symbol(&self, symbol: &str) -> DataResult<()> { 170 | match self.data.write() { 171 | Ok(mut data) => { 172 | data.remove(symbol); 173 | debug!("Cleared memory cache for symbol: {}", symbol); 174 | Ok(()) 175 | } 176 | Err(e) => Err(DataError::Cache(format!("Lock error: {}", e))), 177 | } 178 | } 179 | 180 | async fn clear_all(&self) -> DataResult<()> { 181 | match self.data.write() { 182 | Ok(mut data) => { 183 | data.clear(); 184 | debug!("Cleared all memory cache"); 185 | Ok(()) 186 | } 187 | Err(e) => Err(DataError::Cache(format!("Lock error: {}", e))), 188 | } 189 | } 190 | } 191 | 192 | // ================================================================= 193 | // Redis Cache Implementation 194 | // ================================================================= 195 | 196 | /// Redis cache implementation 197 | pub struct RedisTickCache { 198 | client: RedisClient, 199 | connection: Arc>, 200 | max_ticks_per_symbol: usize, 201 | ttl_seconds: u64, 202 | } 203 | 204 | impl RedisTickCache { 205 | pub async fn new( 206 | redis_url: &str, 207 | max_ticks_per_symbol: usize, 208 | ttl_seconds: u64, 209 | ) -> DataResult { 210 | let client = RedisClient::open(redis_url) 211 | .map_err(|e| DataError::Cache(format!("Failed to create Redis client: {}", e)))?; 212 | 213 | let connection = client 214 | .get_connection() 215 | .map_err(|e| DataError::Cache(format!("Failed to connect to Redis: {}", e)))?; 216 | 217 | debug!("Connected to Redis at: {}", redis_url); 218 | 219 | Ok(Self { 220 | client, 221 | connection: Arc::new(Mutex::new(connection)), 222 | max_ticks_per_symbol, 223 | ttl_seconds, 224 | }) 225 | } 226 | 227 | fn get_cache_key(&self, symbol: &str) -> String { 228 | format!("tick:{}", symbol) 229 | } 230 | } 231 | 232 | #[async_trait] 233 | impl TickDataCache for RedisTickCache { 234 | async fn push_tick(&self, tick: &TickData) -> DataResult<()> { 235 | let key = self.get_cache_key(&tick.symbol); 236 | let tick_json = serde_json::to_string(tick) 237 | .map_err(|e| DataError::Cache(format!("Failed to serialize tick: {}", e)))?; 238 | 239 | let mut conn = self.connection.lock().await; 240 | 241 | // Use LPUSH to add to list head (latest first) 242 | let _: () = conn 243 | .lpush(&key, &tick_json) 244 | .map_err(|e| DataError::Cache(format!("Redis LPUSH failed: {}", e)))?; 245 | 246 | // Limit list length 247 | let _: () = conn 248 | .ltrim(&key, 0, self.max_ticks_per_symbol as isize - 1) 249 | .map_err(|e| DataError::Cache(format!("Redis LTRIM failed: {}", e)))?; 250 | 251 | // Set TTL 252 | let _: () = conn 253 | .expire(&key, self.ttl_seconds as usize) 254 | .map_err(|e| DataError::Cache(format!("Redis EXPIRE failed: {}", e)))?; 255 | 256 | debug!( 257 | "Added tick to Redis cache: symbol={}, price={}", 258 | tick.symbol, tick.price 259 | ); 260 | Ok(()) 261 | } 262 | 263 | async fn get_recent_ticks(&self, symbol: &str, limit: usize) -> DataResult> { 264 | let key = self.get_cache_key(symbol); 265 | let mut conn = self.connection.lock().await; 266 | 267 | // Use LRANGE to get latest N records 268 | let tick_jsons: Vec = conn 269 | .lrange(&key, 0, limit as isize - 1) 270 | .map_err(|e| DataError::Cache(format!("Redis LRANGE failed: {}", e)))?; 271 | 272 | let mut ticks = Vec::with_capacity(tick_jsons.len()); 273 | for tick_json in tick_jsons { 274 | match serde_json::from_str::(&*tick_json) { 275 | Ok(tick) => ticks.push(tick), 276 | Err(e) => { 277 | warn!("Failed to deserialize tick from Redis: {}", e); 278 | // Continue processing other data, don't interrupt on single error 279 | } 280 | } 281 | } 282 | 283 | debug!( 284 | "Retrieved {} ticks from Redis cache for symbol: {}", 285 | ticks.len(), 286 | symbol 287 | ); 288 | Ok(ticks) 289 | } 290 | 291 | async fn get_symbols(&self) -> DataResult> { 292 | let mut conn = self.connection.lock().await; 293 | 294 | let keys: Vec = conn 295 | .keys("tick:*") 296 | .map_err(|e| DataError::Cache(format!("Redis KEYS failed: {}", e)))?; 297 | 298 | let symbols: Vec = keys 299 | .into_iter() 300 | .filter_map(|key| { 301 | if key.starts_with("tick:") { 302 | Some(key[5..].to_string()) // Remove "tick:" prefix 303 | } else { 304 | None 305 | } 306 | }) 307 | .collect(); 308 | 309 | Ok(symbols) 310 | } 311 | 312 | async fn clear_symbol(&self, symbol: &str) -> DataResult<()> { 313 | let key = self.get_cache_key(symbol); 314 | let mut conn = self.connection.lock().await; 315 | 316 | let _: () = conn 317 | .del(&key) 318 | .map_err(|e| DataError::Cache(format!("Redis DEL failed: {}", e)))?; 319 | 320 | debug!("Cleared Redis cache for symbol: {}", symbol); 321 | Ok(()) 322 | } 323 | 324 | async fn clear_all(&self) -> DataResult<()> { 325 | let symbols = self.get_symbols().await?; 326 | 327 | for symbol in symbols { 328 | self.clear_symbol(&symbol).await?; 329 | } 330 | 331 | debug!("Cleared all Redis cache"); 332 | Ok(()) 333 | } 334 | } 335 | 336 | // ================================================================= 337 | // Tiered Cache Implementation 338 | // ================================================================= 339 | 340 | /// Tiered cache: L1 memory + L2 Redis 341 | pub struct TieredCache { 342 | memory_cache: InMemoryTickCache, 343 | redis_cache: RedisTickCache, 344 | } 345 | 346 | impl TieredCache { 347 | pub async fn new( 348 | memory_config: (usize, u64), // (max_ticks_per_symbol, ttl_seconds) 349 | redis_config: (&str, usize, u64), // (url, max_ticks_per_symbol, ttl_seconds) 350 | ) -> DataResult { 351 | let memory_cache = InMemoryTickCache::new(memory_config.0, memory_config.1); 352 | let redis_cache = 353 | RedisTickCache::new(redis_config.0, redis_config.1, redis_config.2).await?; 354 | 355 | debug!("Initialized tiered cache with memory and Redis layers"); 356 | 357 | Ok(Self { 358 | memory_cache, 359 | redis_cache, 360 | }) 361 | } 362 | 363 | /// Periodically clean expired items from memory cache 364 | pub fn cleanup_memory(&self) { 365 | self.memory_cache.cleanup_expired(); 366 | } 367 | } 368 | 369 | #[async_trait] 370 | impl TickDataCache for TieredCache { 371 | async fn push_tick(&self, tick: &TickData) -> DataResult<()> { 372 | // Write to both memory and Redis in parallel 373 | let memory_result = self.memory_cache.push_tick(tick); 374 | let redis_result = self.redis_cache.push_tick(tick); 375 | 376 | // Wait for both operations to complete 377 | let (memory_res, redis_res) = tokio::join!(memory_result, redis_result); 378 | 379 | // If memory cache fails, log error but don't interrupt 380 | if let Err(e) = memory_res { 381 | error!("Memory cache push failed: {}", e); 382 | } 383 | 384 | // Redis failure returns error 385 | redis_res?; 386 | 387 | Ok(()) 388 | } 389 | 390 | async fn get_recent_ticks(&self, symbol: &str, limit: usize) -> DataResult> { 391 | // 1. Try memory cache first 392 | let memory_ticks = self.memory_cache.get_recent_ticks(symbol, limit).await?; 393 | if memory_ticks.len() == limit { 394 | debug!("L1 cache hit for symbol: {}", symbol); 395 | return Ok(memory_ticks); 396 | } 397 | 398 | // 2. L1 miss, try Redis 399 | let redis_ticks = self.redis_cache.get_recent_ticks(symbol, limit).await?; 400 | if !redis_ticks.is_empty() { 401 | debug!("L2 cache hit for symbol: {}", symbol); 402 | 403 | // Backfill to memory cache 404 | for tick in &redis_ticks { 405 | if let Err(e) = self.memory_cache.push_tick(tick).await { 406 | warn!("Failed to backfill memory cache: {}", e); 407 | } 408 | } 409 | 410 | return Ok(redis_ticks.into_iter().take(limit).collect()); 411 | } 412 | 413 | // 3. Complete cache miss 414 | debug!("Cache miss for symbol: {}", symbol); 415 | Ok(Vec::new()) 416 | } 417 | 418 | async fn get_symbols(&self) -> DataResult> { 419 | // Merge symbols from memory and Redis 420 | let memory_symbols = self.memory_cache.get_symbols().await?; 421 | let redis_symbols = self.redis_cache.get_symbols().await?; 422 | 423 | let mut all_symbols = memory_symbols; 424 | for symbol in redis_symbols { 425 | if !all_symbols.contains(&symbol) { 426 | all_symbols.push(symbol); 427 | } 428 | } 429 | 430 | Ok(all_symbols) 431 | } 432 | 433 | async fn clear_symbol(&self, symbol: &str) -> DataResult<()> { 434 | // Clear both memory and Redis in parallel 435 | let memory_result = self.memory_cache.clear_symbol(symbol); 436 | let redis_result = self.redis_cache.clear_symbol(symbol); 437 | 438 | let (memory_res, redis_res) = tokio::join!(memory_result, redis_result); 439 | 440 | // Both must succeed 441 | memory_res?; 442 | redis_res?; 443 | 444 | Ok(()) 445 | } 446 | 447 | async fn clear_all(&self) -> DataResult<()> { 448 | // Clear both memory and Redis in parallel 449 | let memory_result = self.memory_cache.clear_all(); 450 | let redis_result = self.redis_cache.clear_all(); 451 | 452 | let (memory_res, redis_res) = tokio::join!(memory_result, redis_result); 453 | 454 | memory_res?; 455 | redis_res?; 456 | 457 | Ok(()) 458 | } 459 | } 460 | 461 | #[cfg(test)] 462 | mod tests { 463 | use super::*; 464 | use crate::data::types::TradeSide; 465 | use chrono::Utc; 466 | use rust_decimal::Decimal; 467 | 468 | fn create_test_tick(symbol: &str, price: &str, trade_id: &str) -> TickData { 469 | TickData::new( 470 | Utc::now(), 471 | symbol.to_string(), 472 | price.parse::().unwrap(), 473 | "1.0".parse::().unwrap(), 474 | TradeSide::Buy, 475 | trade_id.to_string(), 476 | false, 477 | ) 478 | } 479 | 480 | #[tokio::test] 481 | async fn test_memory_cache() { 482 | let cache = InMemoryTickCache::new(100, 300); 483 | 484 | let tick = create_test_tick("BTCUSDT", "50000.0", "test1"); 485 | 486 | // Test adding 487 | cache.push_tick(&tick).await.unwrap(); 488 | 489 | // Test getting 490 | let ticks = cache.get_recent_ticks("BTCUSDT", 1).await.unwrap(); 491 | assert_eq!(ticks.len(), 1); 492 | assert_eq!(ticks[0].symbol, "BTCUSDT"); 493 | assert_eq!(ticks[0].price, "50000.0".parse::().unwrap()); 494 | } 495 | 496 | #[tokio::test] 497 | async fn test_memory_cache_size_limit() { 498 | let cache = InMemoryTickCache::new(2, 300); // Max 2 items 499 | 500 | // Add 3 items 501 | for i in 1..=3 { 502 | let price = format!("{}.0", 50000 + i); 503 | let tick = create_test_tick("BTCUSDT", &price, &format!("test{}", i)); 504 | cache.push_tick(&tick).await.unwrap(); 505 | } 506 | 507 | // Should only keep the latest 2 508 | let ticks = cache.get_recent_ticks("BTCUSDT", 10).await.unwrap(); 509 | assert_eq!(ticks.len(), 2); 510 | 511 | // Latest should be first 512 | assert_eq!(ticks[0].price, "50003.0".parse::().unwrap()); // Latest 513 | assert_eq!(ticks[1].price, "50002.0".parse::().unwrap()); // Second latest 514 | } 515 | } 516 | --------------------------------------------------------------------------------