├── 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 | [](https://www.rust-lang.org/)
6 | [](LICENSE)
7 | [](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 | 
240 |
241 | ### Results Dashboard
242 | 
243 |
244 | ### Trade Analysis
245 | 
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 |
--------------------------------------------------------------------------------