├── src ├── bin │ ├── test.rs │ ├── load.rs │ ├── kwant.rs │ └── enginetest.rs ├── frontend │ ├── interface │ │ ├── README.md │ │ ├── src │ │ │ ├── vite-env.d.ts │ │ │ ├── components │ │ │ │ ├── AddMarketButton.tsx │ │ │ │ ├── footer.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── MarketCard.tsx │ │ │ │ ├── AddMarket.tsx │ │ │ │ └── Markets.tsx │ │ │ ├── main.tsx │ │ │ ├── index.css │ │ │ ├── App.tsx │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── vite.config.ts │ │ ├── .gitignore │ │ ├── index.html │ │ ├── public │ │ │ └── vite.svg │ │ ├── eslint.config.js │ │ ├── tsconfig.node.json │ │ ├── tsconfig.app.json │ │ └── package.json │ ├── mod.rs │ └── ws_structs.rs ├── consts.rs ├── signal │ ├── mod.rs │ ├── types.rs │ └── signal.rs ├── lib.rs ├── backtest.rs ├── assets.rs ├── wallet.rs ├── margin.rs ├── helper.rs ├── trade_setup.rs ├── strategy.rs ├── executor.rs ├── market.rs └── bot.rs ├── .gitignore ├── run.sh ├── config.toml ├── Cargo.toml └── README.md /src/bin/test.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/interface/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | pub const MAX_HISTORY: usize = 10000; 2 | -------------------------------------------------------------------------------- /src/frontend/mod.rs: -------------------------------------------------------------------------------- 1 | mod ws_structs; 2 | 3 | pub use ws_structs::*; 4 | -------------------------------------------------------------------------------- /src/frontend/interface/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /target 3 | Cargo.lock 4 | testnet 5 | src/frontend/interface/.vite 6 | -------------------------------------------------------------------------------- /src/frontend/interface/src/components/AddMarketButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function AddMarketButton(){ 4 | const [ 5 | } 6 | -------------------------------------------------------------------------------- /src/frontend/interface/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | cargo run --release --bin load #update tradable assets list 2 | 3 | 4 | cargo run --release --bin kwant & #PREFIX WITH "RUST_LOG=info" for logging 5 | 6 | cd ./src/frontend/interface 7 | 8 | npm install 9 | npm run dev 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/interface/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | #Three trading styles: Scalp - Swing - build_position 2 | 3 | style = "Scalp" 4 | 5 | #Risk: Low - Normal - High 6 | risk = "High" 7 | 8 | #Stance: Bull - Neutral - Bear 9 | stance = "Neutral" 10 | 11 | #Follow trend: true - false 12 | followTrend= false 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/interface/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | tailwindcss() 10 | ], 11 | }) 12 | -------------------------------------------------------------------------------- /src/signal/mod.rs: -------------------------------------------------------------------------------- 1 | mod signal; 2 | mod types; 3 | 4 | pub use signal::{ 5 | SignalEngine, 6 | EngineCommand, 7 | }; 8 | 9 | pub use types::{ 10 | Tracker, 11 | Handler, 12 | IndexId, 13 | IndicatorKind, 14 | ExecParam, 15 | ExecParams, 16 | TimeFrameData, 17 | EditType, 18 | Entry, 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /src/frontend/interface/src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer: React.FC = () => { 4 | return ( 5 |
6 | © {new Date().getFullYear()} Under Control — All rights reserved. 7 |
8 | ); 9 | }; 10 | 11 | export default Footer; 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/interface/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/frontend/interface/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Kwant Bot 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/frontend/interface/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @keyframes gradientBG { 4 | 0% { background-position: 0% 50%; } 5 | 50% { background-position: 100% 50%; } 6 | 100% { background-position: 0% 50%; } 7 | } 8 | 9 | .bg-special { 10 | background: 11 | /* your custom palette here */ 12 | linear-gradient( 13 | 90deg, 14 | black, 15 | gray, 16 | gray, 17 | black 18 | ); 19 | background-size: 300% 300%; 20 | animation: gradientBG 120s ease infinite; 21 | } 22 | -------------------------------------------------------------------------------- /src/bin/load.rs: -------------------------------------------------------------------------------- 1 | use hyperliquid_rust_sdk::{BaseUrl, InfoClient, AssetMeta}; 2 | use std::fs::File; 3 | use std::io::Write; 4 | 5 | 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | 10 | let mut info = InfoClient::new(None, Some(BaseUrl::Mainnet)).await.unwrap(); 11 | 12 | let universe = info.meta().await.unwrap().universe; 13 | let assets: Vec<&str> = universe.iter().map(|a| a.name.as_str()).collect(); 14 | 15 | let mut f = File::create("src/assets.rs").unwrap(); 16 | 17 | f.write_all(format!("pub static MARKETS: &[&str] = &{:?};", assets).as_bytes()); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/frontend/interface/public/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/frontend/interface/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /src/frontend/interface/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/interface/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import MarketCard from './components/MarketCard' 3 | import MarketsPage from './components/Markets' 4 | import Header from './components/Header' 5 | import Footer from './components/footer' 6 | import type {IndicatorKind, MarketInfo} from './types' 7 | import viteLogo from '/vite.svg'; 8 | 9 | 10 | const handleTogglePause = (asset: string) => { 11 | console.log(`Toggled pause for ${asset}`); 12 | }; 13 | 14 | const handleRemove = (asset: string) => { 15 | console.log(`Removed market ${asset}`); 16 | }; 17 | 18 | const App: React.FC = () => ( 19 |
20 |
21 | 22 |
23 |
24 | ); 25 | 26 | export default App; 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/frontend/interface/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod market; 2 | mod executor; 3 | mod consts; 4 | mod assets; 5 | mod wallet; 6 | mod backtest; 7 | 8 | 9 | pub mod frontend; 10 | pub mod helper; 11 | pub mod signal; 12 | pub mod strategy; 13 | pub mod trade_setup; 14 | pub mod bot; 15 | pub mod margin; 16 | 17 | pub use frontend::*; 18 | pub use bot::{Bot, BotEvent, BotToMarket}; 19 | pub use wallet::Wallet; 20 | pub use signal::{SignalEngine, IndexId, IndicatorKind, EditType, Entry}; 21 | pub use market::{Market, MarketCommand, MarketUpdate, AssetPrice}; 22 | pub use consts::{MAX_HISTORY}; 23 | pub use assets::MARKETS; 24 | pub use executor::Executor; 25 | // pub use backtest::BackTester; 26 | pub use trade_setup::{TradeParams, TimeFrame, TradeCommand, TradeInfo, MarketTradeInfo, TradeFillInfo, LiquidationFillInfo}; 27 | pub use margin::{AssetMargin, MarginAllocation}; 28 | 29 | //expost HL sdk types 30 | pub use hyperliquid_rust_sdk::{BaseUrl, Error}; 31 | pub use ethers::signers::LocalWallet; 32 | pub use kwant::indicators::Value; 33 | -------------------------------------------------------------------------------- /src/frontend/interface/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interface", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/vite": "^4.1.11", 14 | "framer-motion": "^12.23.12", 15 | "lucide-react": "^0.539.0", 16 | "react": "^19.1.0", 17 | "react-dom": "^19.1.0", 18 | "react-icons": "^5.5.0", 19 | "tailwindcss": "^4.1.11" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.30.1", 23 | "@types/react": "^19.1.8", 24 | "@types/react-dom": "^19.1.6", 25 | "@vitejs/plugin-react": "^4.6.0", 26 | "eslint": "^9.30.1", 27 | "eslint-plugin-react-hooks": "^5.2.0", 28 | "eslint-plugin-react-refresh": "^0.4.20", 29 | "globals": "^16.3.0", 30 | "typescript": "~5.8.3", 31 | "typescript-eslint": "^8.35.1", 32 | "vite": "^7.0.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/backtest.rs: -------------------------------------------------------------------------------- 1 | use crate::{SignalEngine, MARKETS, IndexId}; 2 | use crate::helper::{load_candles}; 3 | use kwant::indicators::{Price}; 4 | use crate::trade_setup::{TradeParams, TimeFrame}; 5 | use crate::strategy::{Strategy, CustomStrategy, Style, Stance}; 6 | use tokio::time::{sleep, Duration}; 7 | use hyperliquid_rust_sdk::{InfoClient, BaseUrl}; 8 | 9 | pub struct BackTester{ 10 | pub asset: String, 11 | pub signal_engine: SignalEngine, 12 | pub params: TradeParams, 13 | pub candle_data: Vec, 14 | } 15 | 16 | 17 | 18 | 19 | impl BackTester{ 20 | 21 | pub fn new(asset: &str,params: TradeParams, config: Option>, margin: f64) -> Self{ 22 | if !MARKETS.contains(&asset){ 23 | panic!("ASSET ISN'T TRADABLE, MARKET CAN'T BE INITILIAZED"); 24 | } 25 | 26 | BackTester{ 27 | asset: asset.to_string(), 28 | signal_engine: SignalEngine::new_backtest(params.clone(), config, margin), 29 | params, 30 | candle_data: Vec::new(), 31 | } 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperliquid_rust_bot" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | hyperliquid_rust_sdk = { git = "https://github.com/0xNoSystem/hyperliquid-rust-sdk.git", branch = "master" } 8 | kwant = { git = "https://github.com/0xNoSystem/Indicators_rs.git", branch = "main" } 9 | chrono = "0.4.26" 10 | env_logger = "0.10.0" 11 | ethers = {version = "2.0.14", features = ["eip712", "abigen"]} 12 | futures-util = "0.3.28" 13 | hex = "0.4.3" 14 | http = "0.2.9" 15 | lazy_static = "1.3" 16 | log = "0.4.19" 17 | rand = "0.8.5" 18 | reqwest = "0.11.18" 19 | serde = { version = "1.0.175", features = ["derive"] } 20 | serde_json = "1.0.103" 21 | rmp-serde = "1.0.0" 22 | thiserror = "1.0.44" 23 | tokio = { version = "1.29.1", features = ["full"] } 24 | tokio-tungstenite = {version = "0.20.0", features = ["native-tls"]} 25 | uuid = {version = "1.6.1", features = ["v4"]} 26 | dotenv = "0.15.0" 27 | flume = "0.11.1" 28 | toml = "0.8.20" 29 | arraydeque = "0.5.1" 30 | rustc-hash = "2.1.1" 31 | futures = "0.3.31" 32 | hyperliquid = "0.2.4" 33 | anyhow = "1.0.98" 34 | actix = "0.13" 35 | actix-web = "4.11.0" 36 | actix-web-actors = "4" 37 | actix-cors = "0.6" 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets.rs: -------------------------------------------------------------------------------- 1 | pub static MARKETS: &[&str] = &["BTC", "ETH", "ATOM", "MATIC", "DYDX", "SOL", "AVAX", "BNB", "APE", "OP", "LTC", "ARB", "DOGE", "INJ", "SUI", "kPEPE", "CRV", "LDO", "LINK", "STX", "RNDR", "CFX", "FTM", "GMX", "SNX", "XRP", "BCH", "APT", "AAVE", "COMP", "MKR", "WLD", "FXS", "HPOS", "RLB", "UNIBOT", "YGG", "TRX", "kSHIB", "UNI", "SEI", "RUNE", "OX", "FRIEND", "SHIA", "CYBER", "ZRO", "BLZ", "DOT", "BANANA", "TRB", "FTT", "LOOM", "OGN", "RDNT", "ARK", "BNT", "CANTO", "REQ", "BIGTIME", "KAS", "ORBS", "BLUR", "TIA", "BSV", "ADA", "TON", "MINA", "POLYX", "GAS", "PENDLE", "STG", "FET", "STRAX", "NEAR", "MEME", "ORDI", "BADGER", "NEO", "ZEN", "FIL", "PYTH", "SUSHI", "ILV", "IMX", "kBONK", "GMT", "SUPER", "USTC", "NFTI", "JUP", "kLUNC", "RSR", "GALA", "JTO", "NTRN", "ACE", "MAV", "WIF", "CAKE", "PEOPLE", "ENS", "ETC", "XAI", "MANTA", "UMA", "ONDO", "ALT", "ZETA", "DYM", "MAVIA", "W", "PANDORA", "STRK", "PIXEL", "AI", "TAO", "AR", "MYRO", "kFLOKI", "BOME", "ETHFI", "ENA", "MNT", "TNSR", "SAGA", "MERL", "HBAR", "POPCAT", "OMNI", "EIGEN", "REZ", "NOT", "TURBO", "BRETT", "IO", "ZK", "BLAST", "LISTA", "MEW", "RENDER", "kDOGS", "POL", "CATI", "CELO", "HMSTR", "SCR", "NEIROETH", "kNEIRO", "GOAT", "MOODENG", "GRASS", "PURR", "PNUT", "XLM", "CHILLGUY", "SAND", "IOTA", "ALGO", "HYPE", "ME", "MOVE", "VIRTUAL", "PENGU", "USUAL", "FARTCOIN", "AI16Z", "AIXBT", "ZEREBRO", "BIO", "GRIFFAIN", "SPX", "S", "MORPHO", "TRUMP", "MELANIA", "ANIME", "VINE", "VVV", "JELLY", "BERA", "TST", "LAYER", "IP", "OM", "KAITO", "NIL", "PAXG", "PROMPT", "BABY", "WCT", "HYPER", "ZORA", "INIT", "DOOD", "LAUNCHCOIN", "NXPC", "SOPH", "RESOLV", "SYRUP", "PUMP", "PROVE", "YZY", "XPL", "WLFI", "LINEA"]; -------------------------------------------------------------------------------- /src/frontend/ws_structs.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use crate::{MarketTradeInfo,MarginAllocation, IndexId, TradeParams, Value, AssetPrice, AssetMargin}; 3 | use std::collections::HashMap; 4 | 5 | 6 | #[derive(Clone, Debug, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct AddMarketInfo { 9 | pub asset: String, 10 | pub margin_alloc: MarginAllocation, 11 | pub trade_params: TradeParams, 12 | pub config: Option>, 13 | } 14 | 15 | #[derive(Clone, Debug, Serialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct MarketInfo{ 18 | pub asset: String, 19 | pub lev: u32, 20 | pub price: f64, 21 | pub params: TradeParams, 22 | pub margin: f64, 23 | pub pnl: f64, 24 | pub is_paused: bool, 25 | pub indicators: Vec, 26 | } 27 | 28 | 29 | #[derive(Clone, Debug, Deserialize, Serialize)] 30 | #[serde(rename_all = "camelCase")] 31 | pub struct IndicatorData{ 32 | pub id: IndexId, 33 | pub value: Option, 34 | } 35 | 36 | #[derive(Clone, Debug, Serialize)] 37 | #[serde(rename_all = "camelCase")] 38 | pub enum EditMarketInfo{ 39 | Lev(f64), 40 | Strategy, 41 | Indicator(Vec), 42 | } 43 | 44 | 45 | 46 | #[derive(Clone, Debug, Serialize)] 47 | #[serde(rename_all = "camelCase")] 48 | pub enum UpdateFrontend{ 49 | ConfirmMarket(MarketInfo), 50 | UpdatePrice(AssetPrice), 51 | NewTradeInfo(MarketTradeInfo), 52 | UpdateTotalMargin(f64), 53 | UpdateMarketMargin(AssetMargin), 54 | UpdateIndicatorValues{asset: String, data: Vec}, 55 | MarketInfoEdit((String, EditMarketInfo)), 56 | UserError(String), 57 | LoadSession(Vec), 58 | } 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/frontend/interface/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Github, ExternalLink } from 'lucide-react'; 3 | 4 | const Header: React.FC = () => ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |

KWANT

13 |

Trading Bot Console

14 |
15 |
16 | 17 | 35 |
36 |
37 | ); 38 | 39 | export default Header; 40 | -------------------------------------------------------------------------------- /src/wallet.rs: -------------------------------------------------------------------------------- 1 | use ethers::signers::LocalWallet; 2 | use crate::helper::{address}; 3 | use hyperliquid_rust_sdk::{Error,InfoClient, UserFillsResponse, BaseUrl, AssetPosition}; 4 | 5 | pub struct Wallet{ 6 | info_client: InfoClient, 7 | pub wallet: LocalWallet, 8 | pub pubkey: String, 9 | pub url: BaseUrl, 10 | } 11 | 12 | 13 | impl Wallet{ 14 | 15 | pub async fn new(url: BaseUrl,pubkey: String, wallet: LocalWallet) -> Result{ 16 | 17 | let mut info_client = InfoClient::new(None, Some(url)).await?; 18 | Ok(Wallet{ 19 | info_client, 20 | wallet, 21 | pubkey, 22 | url, 23 | }) 24 | } 25 | 26 | pub async fn get_user_fees(&self) -> Result<(f64, f64), Error>{ 27 | let user = address(&self.pubkey); 28 | let user_fees = self.info_client.user_fees(user).await?; 29 | let add_fee: f64 = user_fees.user_add_rate.parse().unwrap(); 30 | let cross_fee: f64 = user_fees.user_cross_rate.parse().unwrap(); 31 | 32 | Ok((add_fee, cross_fee)) 33 | } 34 | 35 | pub async fn user_fills(&self) -> Result, Error>{ 36 | 37 | let user = address(&self.pubkey); 38 | 39 | return self.info_client.user_fills(user).await; 40 | 41 | 42 | } 43 | 44 | pub async fn get_user_margin(&self) -> Result { 45 | let user = address(&self.pubkey); 46 | 47 | let info = self.info_client.user_state(user) 48 | .await?; 49 | 50 | let res = info.margin_summary.account_value 51 | .parse::() 52 | .map_err(|e| Error::GenericParse(format!("FATAL: failed to parse account balance to f64, {}",e)))?; 53 | 54 | let upnl: f64 = info.asset_positions.into_iter().filter_map(|p|{ 55 | let u = p.position.unrealized_pnl.parse::().ok()?; 56 | let f = p.position.cum_funding.since_open.parse::().ok()?; 57 | Some(u - f) 58 | }).sum(); 59 | 60 | Ok(res - upnl) 61 | } 62 | 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/margin.rs: -------------------------------------------------------------------------------- 1 | use std::hash::BuildHasherDefault; 2 | use rustc_hash::FxHasher; 3 | use hyperliquid_rust_sdk::{Error}; 4 | use std::sync::Arc; 5 | 6 | use crate::Wallet; 7 | use std::collections::HashMap; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | 11 | #[derive(Clone, Debug, Copy, Deserialize, Serialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub enum MarginAllocation{ 14 | Alloc(f64), //percentage of available margin 15 | Amount(f64), 16 | } 17 | 18 | pub type MarginMap = HashMap>; 19 | 20 | pub struct MarginBook{ 21 | user: Arc, 22 | map: MarginMap, 23 | pub total_on_chain: f64, 24 | } 25 | 26 | 27 | impl MarginBook{ 28 | 29 | pub fn new(user: Arc) -> Self{ 30 | 31 | Self{ 32 | user, 33 | map: HashMap::default(), 34 | total_on_chain: f64::from_bits(1), 35 | } 36 | } 37 | pub async fn sync(&mut self) -> Result<(), Error>{ 38 | self.total_on_chain = self.user.get_user_margin().await?; 39 | Ok(()) 40 | } 41 | 42 | 43 | pub async fn update_asset(&mut self, update: AssetMargin) -> Result{ 44 | let (asset, requested_margin) = update; 45 | self.sync().await?; 46 | let free = self.free(); 47 | 48 | if requested_margin > free{ 49 | return Err(Error::InsufficientFreeMargin(free)); 50 | } 51 | self.map.insert(asset, requested_margin); 52 | 53 | Ok(requested_margin) 54 | } 55 | 56 | pub async fn allocate(&mut self, asset: String, alloc: MarginAllocation) -> Result{ 57 | self.sync().await?; 58 | let free = self.free(); 59 | 60 | 61 | match alloc{ 62 | MarginAllocation::Alloc(ptc)=>{ 63 | if ptc <= 0.0{ 64 | return Err(Error::InvalidMarginAmount); 65 | } 66 | let requested_margin = self.total_on_chain * ptc; 67 | if requested_margin > free{ 68 | log::warn!("Error::InsufficientFreeMargin({})", free); 69 | return Err(Error::InsufficientFreeMargin(free)); 70 | } 71 | self.map.insert(asset, requested_margin); 72 | return Ok(requested_margin); 73 | }, 74 | 75 | MarginAllocation::Amount(amount)=>{ 76 | if amount <= 0.0{ 77 | return Err(Error::InvalidMarginAmount); 78 | } 79 | if amount > free{ 80 | log::warn!("Error::InsufficientFreeMargin({})", free); 81 | return Err(Error::InsufficientFreeMargin(free)); 82 | } 83 | self.map.insert(asset, amount); 84 | return Ok(amount); 85 | }, 86 | } 87 | } 88 | 89 | pub fn remove(&mut self, asset: &String) { 90 | self.map.remove(asset); 91 | } 92 | 93 | pub fn used(&self) -> f64{ 94 | self.map.values().copied().sum() 95 | } 96 | 97 | pub fn free(&self) -> f64{ 98 | self.total_on_chain - self.used() 99 | } 100 | 101 | pub fn reset(&mut self) { 102 | self.map.clear(); 103 | } 104 | 105 | } 106 | 107 | 108 | pub type AssetMargin = (String, f64); 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperliquid Trading (perp trading) Bot - Rust 2 | 3 | Hyperliquid trading(Perp trading) Bot with Rust is an experimental trading system built with 4 | [`hyperliquid_rust_sdk`](https://github.com/0xTan1319/hyperliquid-rust-sdk). It manages multiple 5 | markets on the Hyperliquid exchange and places trades based on signals from 6 | user-selected indicators. 7 | 8 | The repository currently ships a React/TS UI and actix server, follow the getting started steps. 9 | 10 | ## Contact 11 | 12 | If you have any question or need more update for this, contact here: [Telegram](https://t.me/shiny0103) | [Twitter](https://x.com/0xTan1319) 13 | 14 | ## Features 15 | 16 | - Connect to Hyperliquid mainnet, testnet or localhost. 17 | - Manage several markets concurrently with configurable margin allocation. 18 | - Customisable strategy (risk, style, stance). 19 | - Indicator engine where each indicator is bound to a timeframe. 20 | - Asynchronous design using `tokio` and `flume` channels. 21 | 22 | ## Getting started 23 | 24 | After cloning the repo 25 | 26 | 1. Install a recent Rust toolchain. 27 | 2. Create a `.env` file in the root directory`: 28 | 29 | ```env 30 | PRIVATE_KEY= -> https://app.hyperliquid.xyz/API 31 | AGENT_KEY= 32 | WALLET= 33 | ``` 34 | 35 | 3. Run the app: 36 | 37 | ```bash 38 | ./run.sh 39 | ``` 40 | 41 | ## Strategy 42 | 43 | The bot uses `CustomStrategy` (see `src/strategy.rs`). It combines indicators 44 | such as RSI, StochRSI, EMA crosses, ADX and ATR. Risk level (`Low`, `Normal`, 45 | `High`), trading style (`Scalp` or `Swing`) and market stance (`Bull`, `Bear` or 46 | `Neutral`) can be set. Signals are generated when multiple indicator conditions 47 | agree—for example an oversold RSI with a bullish StochRSI crossover may trigger a 48 | long trade. 49 | 50 | ## Indicators 51 | 52 | Indicators are activated with `(IndicatorKind, TimeFrame)` pairs. Available kinds 53 | include: 54 | 55 | - `Rsi(u32)` 56 | - `SmaOnRsi { periods, smoothing_length }` 57 | - `StochRsi { periods, k_smoothing, d_smoothing }` 58 | - `Adx { periods, di_length }` 59 | - `Atr(u32)` 60 | - `Ema(u32)` 61 | - `EmaCross { short, long }` 62 | - `Sma(u32)` 63 | 64 | Each pair is wrapped in an `Entry` together with an `EditType` (`Add`, `Remove` or 65 | `Toggle`). The snippet below (from `enginetest.rs`) shows how a market can be 66 | created with a custom indicator configuration: 67 | 68 | ```rust 69 | let config = vec![ 70 | (IndicatorKind::Rsi(12), TimeFrame::Min1), 71 | (IndicatorKind::EmaCross { short: 21, long: 200 }, TimeFrame::Day1), 72 | ]; 73 | 74 | let market = AddMarketInfo { 75 | asset: "BTC".to_string(), 76 | margin_alloc: MarginAllocation::Alloc(0.1), 77 | trade_params, 78 | config: Some(config), 79 | }; 80 | ``` 81 | 82 | ## Project structure 83 | 84 | - `src/bot.rs` – orchestrates markets and keeps margin in sync. 85 | - `src/market.rs` – handles a single market: data feed, signal engine and order 86 | execution. 87 | - `src/signal/` – indicator trackers and strategy logic. 88 | - `src/executor.rs` – sends orders via the Hyperliquid API. 89 | - 'src/strategy.ra' - Implements strategies followed by the signal engine. 90 | - `src/trade_setup.rs` – trading parameters and trade metadata. 91 | - `config.toml` – example strategy configuration. 92 | 93 | Supported trading pairs can be found in `src/assets.rs` (`MARKETS`). 94 | 95 | ## Disclaimer 96 | 97 | This code is experimental and not audited. Use at your own risk when trading on 98 | live markets. 99 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use hyperliquid_rust_sdk::{Error,AssetMeta, InfoClient, Message, Subscription}; 3 | use tokio::sync::mpsc::{UnboundedReceiver}; 4 | use std::time::{SystemTime, UNIX_EPOCH}; 5 | use kwant::indicators::{Price}; 6 | use ethers::types::H160; 7 | use crate::TimeFrame; 8 | use log::warn; 9 | 10 | pub async fn subscribe_candles( 11 | info_client: &mut InfoClient, 12 | coin: &str, 13 | tf: &str, 14 | ) -> Result<(u32,UnboundedReceiver), Error> { 15 | 16 | let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); 17 | 18 | 19 | let subscription_id = info_client 20 | .subscribe( 21 | Subscription::Candle { 22 | coin: coin.to_string(), 23 | interval: tf.to_string(), 24 | }, 25 | sender, 26 | ) 27 | .await?; 28 | info!("Subscribed to new candle data: {:?}", subscription_id); 29 | 30 | Ok((subscription_id, receiver)) 31 | } 32 | 33 | 34 | 35 | 36 | fn get_time_now_and_candles_ago(candle_count: u64, tf: TimeFrame) -> (u64, u64) { 37 | let end = get_time_now(); 38 | 39 | let interval = candle_count 40 | .checked_mul(tf.to_secs()) 41 | .and_then(|s| s.checked_mul(1_000)) 42 | .expect("interval overflowed"); 43 | 44 | let start = end.saturating_sub(interval); 45 | 46 | (start, end) 47 | } 48 | 49 | 50 | 51 | async fn candles_snapshot(info_client: &InfoClient,coin: &str,time_frame: TimeFrame, start: u64, end: u64) -> Result, Error>{ 52 | 53 | let vec = info_client 54 | .candles_snapshot(coin.to_string(), time_frame.to_string(), start, end) 55 | .await?; 56 | 57 | let mut res: Vec = Vec::with_capacity(vec.len()); 58 | for candle in vec { 59 | let h = candle.high.parse::() 60 | .map_err(|e| Error::GenericParse(format!("Failed to parse high: {}", e)))?; 61 | let l = candle.low.parse::() 62 | .map_err(|e| Error::GenericParse(format!("Failed to parse low: {}", e)))?; 63 | let o = candle.open.parse::() 64 | .map_err(|e| Error::GenericParse(format!("Failed to parse open: {}", e)))?; 65 | let c = candle.close.parse::() 66 | .map_err(|e| Error::GenericParse(format!("Failed to parse close: {}", e)))?; 67 | 68 | res.push(Price { 69 | high: h, 70 | low: l, 71 | open: o, 72 | close: c, 73 | }); 74 | } 75 | Ok(res) 76 | } 77 | 78 | 79 | pub async fn load_candles(info_client: &InfoClient,coin: &str,tf: TimeFrame, candle_count: u64) -> Result, Error> { 80 | 81 | 82 | let (start, end) = get_time_now_and_candles_ago(candle_count + 1, tf); 83 | 84 | let price_data = candles_snapshot(info_client, coin, tf, start, end).await?; 85 | 86 | Ok(price_data) 87 | } 88 | 89 | 90 | 91 | 92 | pub fn address(address: &String) -> H160 { 93 | address.parse().unwrap() 94 | 95 | } 96 | 97 | 98 | 99 | pub async fn get_max_lev(info_client: &InfoClient, token: &str) -> u32{ 100 | let assets = info_client.meta().await.unwrap().universe; 101 | 102 | if let Some(asset) = assets.iter().find(|a| a.name == token) { 103 | asset.max_leverage 104 | }else{ 105 | warn!("ERROR: Failed to retrieve max_leverage for {}", token); 106 | 1 107 | } 108 | } 109 | 110 | 111 | pub async fn get_asset(info_client: &InfoClient, token: &str) -> Result{ 112 | let assets = info_client.meta().await?.universe; 113 | 114 | if let Some(asset) = assets.into_iter().find(|a| a.name == token) { 115 | Ok(asset) 116 | }else{ 117 | return Err(Error::AssetNotFound); 118 | } 119 | } 120 | 121 | #[inline] 122 | pub fn get_time_now() -> u64{ 123 | SystemTime::now() 124 | .duration_since(UNIX_EPOCH) 125 | .unwrap() 126 | .as_millis() as u64 127 | } 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/frontend/interface/src/components/MarketCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { Pause, Play, Trash2 } from 'lucide-react'; 4 | import type { MarketInfo } from '../types'; 5 | import { indicatorLabels, indicatorColors, decompose, get_value, fromTimeFrame } from '../types'; 6 | 7 | interface MarketCardProps { 8 | market: MarketInfo; 9 | onTogglePause: (asset: string) => void; 10 | onRemove: (asset: string) => void; 11 | } 12 | 13 | const formatPrice = (n: number) => (n < 1 ? n.toFixed(4) : n.toFixed(2)); 14 | 15 | const PnlBar: React.FC<{ pnl: number }> = ({ pnl }) => { 16 | const w = Math.min(100, Math.abs(pnl)); 17 | const pos = pnl >= 0; 18 | return ( 19 |
20 |
21 |
22 |
23 |
{pos ? '+' : ''}{pnl.toFixed(2)}
24 |
25 | ); 26 | }; 27 | 28 | const MarketCard: React.FC = ({ market, onTogglePause, onRemove }) => { 29 | const { asset, price, lev, margin, params, pnl, is_paused, indicators } = market; 30 | const { strategy } = params; 31 | const { risk, style, stance } = strategy.custom; 32 | 33 | return ( 34 | 35 | {/* Head */} 36 |
37 |
38 |
Asset
39 |
40 |

{asset}

41 | {is_paused ? 'Paused' : 'Live'} 42 |
43 |
${formatPrice(price)} • {lev}×
44 |
45 |
46 | 49 | 52 |
53 |
54 | 55 | {/* Metrics */} 56 |
57 |
58 |
Price
59 |
${formatPrice(price)}
60 |
61 |
62 |
Leverage
63 |
{lev}×
64 |
65 |
66 |
Margin
67 |
${margin.toFixed(2)}
68 |
69 |
70 | 71 | {/* PnL */} 72 |
73 |
PnL
74 | 75 |
76 | 77 | {/* Indicators */} 78 |
79 | {indicators.map((data, i) => { 80 | const { kind, timeframe, value } = decompose(data); 81 | const kindKey = Object.keys(kind)[0] as keyof typeof indicatorColors; 82 | return ( 83 | 84 | {indicatorLabels[kindKey] || (kindKey as string)} — {fromTimeFrame(timeframe)} 85 | 86 | ); 87 | })} 88 |
89 | 90 | {/* Strategy */} 91 |
92 |
93 |
Strategy
94 |
{style} / {stance}
95 |
96 |
97 |
Risk
98 |
{risk}
99 |
100 |
101 |
Trend Following
102 |
{strategy.custom.followTrend ? 'Yes' : 'No'}
103 |
104 |
105 |
106 | ); 107 | }; 108 | 109 | export default MarketCard; 110 | -------------------------------------------------------------------------------- /src/bin/kwant.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use tokio::{sync::{mpsc::{unbounded_channel, UnboundedSender}, broadcast::{self, Sender as BroadcastSender}}, time::Duration}; 3 | use actix::{Actor, StreamHandler, Handler, Message}; 4 | use actix_cors::Cors; 5 | use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder, Error as ActixError}; 6 | use actix::ActorContext; 7 | use actix::AsyncContext; 8 | use actix_web_actors::ws; 9 | use dotenv::dotenv; 10 | use env_logger; 11 | use log::{error, info}; 12 | use serde_json; 13 | use hyperliquid_rust_bot::{ 14 | Bot, BotEvent, UpdateFrontend, LocalWallet, Wallet, BaseUrl, 15 | }; 16 | use hyperliquid_rust_bot::strategy::{Strategy, CustomStrategy, Risk, Style, Stance}; 17 | 18 | #[actix_web::main] 19 | async fn main() -> Result<(), Box> { 20 | dotenv().ok(); 21 | env_logger::init(); 22 | 23 | let url = BaseUrl::Mainnet; 24 | let wallet: LocalWallet = env::var("PRIVATE_KEY")?.parse()?; 25 | let pubkey = env::var("WALLET")?; 26 | let wallet = Wallet::new(url, pubkey, wallet).await?; 27 | 28 | let (mut bot, cmd_sender) = Bot::new(wallet).await?; 29 | let (update_tx, mut update_rx) = unbounded_channel::(); 30 | tokio::spawn(async move { bot.start(update_tx).await }); 31 | 32 | let (bcast_tx, _) = broadcast::channel::(128); 33 | let bcast_cl = bcast_tx.clone(); 34 | 35 | let mut _dummy_rx = bcast_tx.subscribe(); 36 | tokio::spawn(async move { 37 | loop { 38 | let _ = _dummy_rx.recv().await; 39 | } 40 | }); 41 | 42 | tokio::spawn(async move { 43 | while let Some(update) = update_rx.recv().await { 44 | if let Err(err) = bcast_tx.send(update) { 45 | error!("broadcast send error: {}", err); 46 | } 47 | } 48 | }); 49 | 50 | let cmd_data = web::Data::new(cmd_sender.clone()); 51 | let bcast_data = web::Data::new(bcast_cl.clone()); 52 | 53 | HttpServer::new(move || { 54 | App::new() 55 | .app_data(cmd_data.clone()) 56 | .app_data(bcast_data.clone()) 57 | .wrap( 58 | Cors::default() 59 | .allow_any_origin() 60 | .allow_any_method() 61 | .allow_any_header() 62 | .supports_credentials(), 63 | ) 64 | .route("/command", web::post().to(execute)) 65 | .route("/ws", web::get().to(ws_route)) 66 | }) 67 | .bind(("127.0.0.1", 8090))? 68 | .run() 69 | .await?; 70 | 71 | Ok(()) 72 | } 73 | 74 | async fn execute( 75 | raw: web::Bytes, 76 | sender: web::Data>, 77 | ) -> impl Responder { 78 | // Log the raw request body 79 | let body_str = String::from_utf8_lossy(&raw); 80 | println!("Incoming raw body: {}", body_str); 81 | 82 | // Try to deserialize 83 | match serde_json::from_slice::(&raw) { 84 | Ok(event) => { 85 | if let Err(err) = sender.send(event) { 86 | error!("failed to send command: {}", err); 87 | return HttpResponse::InternalServerError().finish(); 88 | } 89 | HttpResponse::Ok().finish() 90 | } 91 | Err(err) => { 92 | error!("Failed to deserialize BotEvent: {}", err); 93 | HttpResponse::BadRequest().body(format!("Invalid BotEvent: {}", err)) 94 | } 95 | } 96 | } 97 | 98 | async fn ws_route( 99 | req: HttpRequest, 100 | stream: web::Payload, 101 | bcast: web::Data>, 102 | ) -> Result { 103 | let rx = bcast.subscribe(); 104 | let ws = MyWebSocket { rx }; 105 | ws::start(ws, &req, stream) 106 | } 107 | 108 | #[derive(Message)] 109 | #[rtype(result = "()")] 110 | struct ServerMessage(String); 111 | 112 | struct MyWebSocket { 113 | rx: broadcast::Receiver, 114 | } 115 | 116 | impl Actor for MyWebSocket { 117 | type Context = ws::WebsocketContext; 118 | 119 | fn started(&mut self, ctx: &mut Self::Context) { 120 | ctx.run_interval(Duration::from_secs(30), |_, ctx| ctx.ping(b"")); 121 | 122 | let mut rx = self.rx.resubscribe(); 123 | let addr = ctx.address(); 124 | tokio::spawn(async move { 125 | loop { 126 | match rx.recv().await { 127 | Ok(update) => { 128 | if let Ok(text) = serde_json::to_string(&update) { 129 | println!("\n{}\n", text); 130 | addr.do_send(ServerMessage(text)); 131 | } 132 | } 133 | Err(broadcast::error::RecvError::Lagged(cnt)) => { 134 | error!("missed {} messages", cnt); 135 | continue; 136 | } 137 | Err(broadcast::error::RecvError::Closed) => break, 138 | } 139 | } 140 | addr.do_send(ServerMessage("__SERVER_CLOSED__".into())); 141 | }); 142 | } 143 | } 144 | 145 | impl Handler for MyWebSocket { 146 | type Result = (); 147 | 148 | fn handle(&mut self, msg: ServerMessage, ctx: &mut Self::Context) { 149 | if msg.0 == "__SERVER_CLOSED__" { 150 | ctx.close(None); 151 | ctx.stop(); 152 | } else { 153 | ctx.text(msg.0); 154 | } 155 | } 156 | } 157 | 158 | impl StreamHandler> for MyWebSocket { 159 | fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { 160 | if let Ok(ws::Message::Ping(p)) = msg { 161 | ctx.pong(&p); 162 | } else if let Ok(ws::Message::Close(reason)) = msg { 163 | ctx.close(reason); 164 | ctx.stop(); 165 | } 166 | } 167 | } 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/frontend/interface/src/types.ts: -------------------------------------------------------------------------------- 1 | export type IndicatorKind = 2 | | { rsi: number } 3 | | { smaOnRsi: { periods: number; smoothingLength: number } } 4 | | { stochRsi: { periods: number; kSmoothing?: number | null; dSmoothing?: number | null } } 5 | | { adx: { periods: number; diLength: number } } 6 | | { atr: number } 7 | | { ema: number } 8 | | { emaCross: { short: number; long: number } } 9 | | { sma: number }; 10 | 11 | export interface MarketInfo{ 12 | asset: string, 13 | lev: number, 14 | price: number, 15 | margin: number, 16 | params: TradeParams, 17 | pnl: number, 18 | is_paused: boolean, 19 | indicators: indicatorData[], 20 | trades: TradeInfo[], 21 | } 22 | 23 | export interface indicatorData { 24 | id: IndexId, 25 | value?: Value 26 | }; 27 | 28 | export type Value = 29 | | { rsiValue: number } 30 | | { stochRsiValue: { k: number; d: number } } 31 | | { emaValue: number } 32 | | { emaCrossValue: { short: number; long: number; trend: boolean } } 33 | | { smaValue: number } 34 | | { smaRsiValue: number } 35 | | { adxValue: number } 36 | | { atrValue: number } 37 | 38 | export function get_value(v?: Value): string { 39 | if (!v) return "No value"; 40 | if ("rsiValue" in v) return `RSI: ${v.rsiValue.toFixed(2)}`; 41 | if ("stochRsiValue" in v) return `StochRSI: K=${v.stochRsiValue.k.toFixed(2)}, D=${v.stochRsiValue.d.toFixed(2)}`; 42 | if ("emaValue" in v) return `EMA: ${v.emaValue.toFixed(2)}`; 43 | if ("emaCrossValue" in v) return `EMA Cross: short=${v.emaCrossValue.short.toFixed(2)}, long=${v.emaCrossValue.long.toFixed(2)}, trend=${v.emaCrossValue.trend ? "↑" : "↓"}`; 44 | if ("smaValue" in v) return `SMA: ${v.smaValue.toFixed(2)}`; 45 | if ("smaRsiValue" in v) return `SMA on RSI: ${v.smaRsiValue.toFixed(2)}`; 46 | if ("adxValue" in v) return `ADX: ${v.adxValue.toFixed(2)}`; 47 | if ("atrValue" in v) return `ATR: ${v.atrValue.toFixed(2)}`; 48 | return "Unknown"; 49 | } 50 | 51 | export type Decomposed = { 52 | kind: IndicatorKind 53 | timeframe: TimeFrame 54 | value?: Value 55 | } 56 | 57 | 58 | 59 | export function decompose(ind: indicatorData): Decomposed { 60 | const [kind, timeframe] = ind.id 61 | return { kind, timeframe, value: ind.value } 62 | } 63 | 64 | export type IndexId = [IndicatorKind, TimeFrame]; 65 | 66 | 67 | export type TimeFrame = 68 | | "min1" 69 | | "min3" 70 | | "min5" 71 | | "min15" 72 | | "min30" 73 | | "hour1" 74 | | "hour2" 75 | | "hour4" 76 | | "hour12" 77 | | "day1" 78 | | "day3" 79 | | "week" 80 | | "month"; 81 | 82 | export const TIMEFRAME_CAMELCASE: Record = { 83 | "1m": "min1", 84 | "3m": "min3", 85 | "5m": "min5", 86 | "15m": "min15", 87 | "30m": "min30", 88 | "1h": "hour1", 89 | "2h": "hour2", 90 | "4h": "hour4", 91 | "12h": "hour12", 92 | "1d": "day1", 93 | "3d": "day3", 94 | "w": "week", 95 | "m": "month", 96 | }; 97 | 98 | const TIMEFRAME_SHORT: Record = Object.entries(TIMEFRAME_CAMELCASE) 99 | .reduce((acc, [short, tf]) => { 100 | acc[tf] = short; 101 | return acc; 102 | }, {} as Record); 103 | 104 | export function fromTimeFrame(tf: TimeFrame): string { 105 | return TIMEFRAME_SHORT[tf]; 106 | } 107 | 108 | export function into(tf: string): TimeFrame { 109 | return TIMEFRAME_CAMELCASE[tf]; 110 | } 111 | 112 | 113 | 114 | 115 | export type Risk = "Low" | "Normal" | "High"; 116 | export type Style = "Scalp" | "Swing"; 117 | export type Stance = "Bull" | "Bear" | "Neutral"; 118 | 119 | export interface CustomStrategy { 120 | risk: Risk; 121 | style: Style; 122 | stance: Stance; 123 | followTrend: boolean; 124 | } 125 | 126 | export type Strategy = { custom: CustomStrategy }; 127 | 128 | export interface TradeParams { 129 | timeFrame: TimeFrame; 130 | lev: number; 131 | strategy: Strategy; 132 | tradeTime: number; 133 | } 134 | 135 | export type MarginAllocation = 136 | | {alloc: number } 137 | | {amount: number }; 138 | 139 | 140 | 141 | 142 | export interface AddMarketInfo { 143 | asset: string; 144 | marginAlloc: MarginAllocation; 145 | tradeParams: TradeParams; 146 | config?: IndexId[]; 147 | }; 148 | 149 | export type Message = 150 | | { confirmMarket: MarketInfo } 151 | | { updatePrice: assetPrice } 152 | | { newTradeInfo: MarketTradeInfo } 153 | | { updateTotalMargin: number} 154 | | { updateMarketMargin: assetMargin } 155 | | { updateIndicatorValues: {asset: string, data: indicatorData[] }} 156 | | { marketInfoEdit: [string, editMarketInfo]} 157 | | { userError: string } 158 | | { loadSession: MarketInfo[]}; 159 | 160 | 161 | export type assetPrice = [string, number]; 162 | export type assetMargin = [string, number]; 163 | 164 | 165 | export type editMarketInfo = 166 | | {lev: number} 167 | | {strategy: Strategy} 168 | | {margin: number}; 169 | 170 | 171 | export interface TradeInfo{ 172 | open: number, 173 | close: number, 174 | pnl: number, 175 | fee: number, 176 | is_long: number, 177 | duration?: number, 178 | oid: [number, number] 179 | }; 180 | 181 | 182 | export interface MarketTradeInfo{ 183 | asset: string, 184 | info: TradeInfo, 185 | } 186 | 187 | 188 | export const indicatorLabels: Record = { 189 | rsi: 'RSI', 190 | smaOnRsi: 'SMA on RSI', 191 | stochRsi: 'Stoch RSI', 192 | adx: 'ADX', 193 | atr: 'ATR', 194 | ema: 'EMA', 195 | emaCross: 'EMA Cross', 196 | sma: 'SMA', 197 | }; 198 | 199 | export const indicatorColors: Record = { 200 | rsi: 'bg-green-800 text-green-200', 201 | smaOnRsi: 'bg-indigo-800 text-indigo-200', 202 | stochRsi: 'bg-purple-800 text-purple-200', 203 | adx: 'bg-yellow-800 text-yellow-200', 204 | atr: 'bg-red-800 text-red-200', 205 | ema: 'bg-blue-800 text-blue-200', 206 | emaCross: 'bg-pink-800 text-pink-200', 207 | sma: 'bg-gray-800 text-gray-200', 208 | }; 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /src/trade_setup.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use log::info; 4 | use hyperliquid_rust_sdk::{ExchangeClient, ExchangeResponseStatus, Error, TradeInfo as HLTradeInfo}; 5 | //use kwant::indicators::Price; 6 | 7 | use crate::strategy::{Strategy, CustomStrategy}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | 11 | 12 | #[derive(Clone, Debug, Deserialize, Serialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct TradeParams { 15 | pub strategy: Strategy, 16 | pub lev: u32, 17 | pub trade_time: u64, 18 | pub time_frame: TimeFrame, 19 | } 20 | 21 | 22 | 23 | impl TradeParams{ 24 | 25 | pub async fn update_lev(&mut self, lev: u32, client: &ExchangeClient, asset: &str, first_time: bool) -> Result{ 26 | if !first_time && self.lev == lev{ 27 | return Err(Error::Custom(format!("Leverage is unchanged"))); 28 | } 29 | 30 | let response = client 31 | .update_leverage(lev, asset, false, None) 32 | .await?; 33 | 34 | info!("Update leverage response: {response:?}"); 35 | match response{ 36 | ExchangeResponseStatus::Ok(_) => { 37 | self.lev = lev; 38 | return Ok(lev); 39 | }, 40 | ExchangeResponseStatus::Err(e)=>{ 41 | return Err(Error::Custom(e)); 42 | }, 43 | } 44 | } 45 | 46 | } 47 | 48 | 49 | 50 | impl Default for TradeParams { 51 | fn default() -> Self { 52 | Self { 53 | strategy: Strategy::Custom(CustomStrategy::default()), 54 | lev: 20, 55 | trade_time: 300, 56 | time_frame: TimeFrame::Min5, 57 | } 58 | } 59 | } 60 | 61 | impl fmt::Display for TradeParams { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | write!( 64 | f, 65 | "leverage: {}\nStrategy: {:?}\nTrade time: {} s\ntime_frame: {}", 66 | self.lev, 67 | self.strategy, 68 | self.trade_time, 69 | self.time_frame.as_str(), 70 | ) 71 | } 72 | } 73 | 74 | 75 | #[derive(Clone, Debug, Copy, Serialize, Deserialize)] 76 | #[serde(rename_all = "camelCase")] 77 | pub enum TradeCommand{ 78 | ExecuteTrade {size: f64, is_long: bool, duration: u64}, 79 | OpenTrade {size: f64, is_long: bool}, 80 | CloseTrade{size: f64}, 81 | BuildPosition {size: f64, is_long: bool, interval: u64}, 82 | CancelTrade, 83 | Liquidation(LiquidationFillInfo), 84 | Toggle, 85 | Resume, 86 | Pause, 87 | } 88 | 89 | 90 | #[derive(Clone, Debug, Copy, Deserialize, Serialize)] 91 | #[serde(rename_all = "camelCase")] 92 | pub struct TradeInfo{ 93 | pub open: f64, 94 | pub close: f64, 95 | pub pnl: f64, 96 | pub fee: f64, 97 | pub is_long: bool, 98 | pub duration: Option, 99 | pub oid: (u64, u64), 100 | } 101 | 102 | 103 | 104 | #[derive(Clone, Debug, Serialize)] 105 | #[serde(rename_all = "camelCase")] 106 | pub struct MarketTradeInfo{ 107 | pub asset: String, 108 | pub info: TradeInfo, 109 | } 110 | 111 | 112 | 113 | 114 | #[derive(Clone, Debug, Serialize, Deserialize)] 115 | #[serde(rename_all = "camelCase")] 116 | pub struct TradeFillInfo{ 117 | pub price: f64, 118 | pub fill_type: String, 119 | pub sz: f64, 120 | pub oid: u64, 121 | pub is_long: bool, } 122 | 123 | impl From for TradeFillInfo{ 124 | 125 | fn from(liq: LiquidationFillInfo) -> Self{ 126 | let LiquidationFillInfo {price, sz, oid, is_long} = liq; 127 | 128 | TradeFillInfo{ 129 | price, 130 | fill_type: "Liquidation".to_string(), 131 | sz, 132 | oid, 133 | is_long, 134 | } 135 | } 136 | } 137 | 138 | 139 | #[derive(Clone, Debug, Copy, Serialize, Deserialize)] 140 | #[serde(rename_all = "camelCase")] 141 | pub struct LiquidationFillInfo{ 142 | pub price: f64, 143 | pub sz: f64, 144 | pub oid: u64, 145 | pub is_long: bool, //was the user going long ? 146 | } 147 | 148 | 149 | 150 | impl From> for LiquidationFillInfo{ 151 | 152 | fn from(trades: Vec) -> Self{ 153 | let n = trades.len(); 154 | let is_long = match trades[0].side.as_str(){ 155 | "A" => true, 156 | "B" => false, 157 | _ => panic!("THIS IS INSANE"), 158 | }; 159 | 160 | let mut sz: f64 = f64::from_bits(1); 161 | let mut total: f64 = f64::from_bits(1); 162 | 163 | trades.iter().for_each(|t| { 164 | let size = t.sz.parse::().unwrap(); 165 | total += size * t.px.parse::().unwrap(); 166 | sz += size; 167 | }); 168 | 169 | let avg_px = total / sz; 170 | 171 | Self{ 172 | price: avg_px, 173 | sz, 174 | oid: 000000, 175 | is_long, 176 | } 177 | } 178 | } 179 | 180 | 181 | 182 | 183 | 184 | 185 | //TIME FRAME 186 | #[derive(Debug, Clone, Copy, PartialEq, Eq,Deserialize,Serialize, Hash)] 187 | #[serde(rename_all = "camelCase")] 188 | pub enum TimeFrame { 189 | Min1, 190 | Min3, 191 | Min5, 192 | Min15, 193 | Min30, 194 | Hour1, 195 | Hour2, 196 | Hour4, 197 | Hour12, 198 | Day1, 199 | Day3, 200 | Week, 201 | Month, 202 | } 203 | 204 | 205 | 206 | 207 | impl TimeFrame{ 208 | 209 | pub fn to_secs(&self) -> u64{ 210 | match *self { 211 | TimeFrame::Min1 => 1 * 60, 212 | TimeFrame::Min3 => 3 * 60, 213 | TimeFrame::Min5 => 5 * 60, 214 | TimeFrame::Min15 => 15 * 60, 215 | TimeFrame::Min30 => 30 * 60, 216 | TimeFrame::Hour1 => 1 * 60 * 60, 217 | TimeFrame::Hour2 => 2 * 60 * 60, 218 | TimeFrame::Hour4 => 4 * 60 * 60, 219 | TimeFrame::Hour12 => 12 * 60 * 60, 220 | TimeFrame::Day1 => 24 * 60 * 60, 221 | TimeFrame::Day3 => 3 * 24 * 60 * 60, 222 | TimeFrame::Week => 7 * 24 * 60 * 60, 223 | TimeFrame::Month => 30 * 24 * 60 * 60, // approximate month as 30 days 224 | } 225 | } 226 | 227 | pub fn to_millis(&self) -> u64{ 228 | self.to_secs() * 1000 229 | } 230 | 231 | 232 | } 233 | 234 | impl TimeFrame { 235 | pub fn as_str(&self) -> &'static str { 236 | match self { 237 | TimeFrame::Min1 => "1m", 238 | TimeFrame::Min3 => "3m", 239 | TimeFrame::Min5 => "5m", 240 | TimeFrame::Min15 => "15m", 241 | TimeFrame::Min30 => "30m", 242 | TimeFrame::Hour1 => "1h", 243 | TimeFrame::Hour2 => "2h", 244 | TimeFrame::Hour4 => "4h", 245 | TimeFrame::Hour12 => "12h", 246 | TimeFrame::Day1 => "1d", 247 | TimeFrame::Day3 => "3d", 248 | TimeFrame::Week => "w", 249 | TimeFrame::Month => "m", 250 | } 251 | } 252 | pub fn to_string(&self) -> String{ 253 | 254 | self.as_str().to_string() 255 | 256 | } 257 | 258 | } 259 | 260 | impl std::fmt::Display for TimeFrame { 261 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 262 | f.write_str(self.as_str()) 263 | } 264 | } 265 | 266 | 267 | 268 | impl std::str::FromStr for TimeFrame { 269 | 270 | type Err = String; 271 | fn from_str(s: &str) -> Result { 272 | 273 | match s { 274 | "1m" => Ok(TimeFrame::Min1), 275 | "3m" => Ok(TimeFrame::Min3), 276 | "5m" => Ok(TimeFrame::Min5), 277 | "15m" => Ok(TimeFrame::Min15), 278 | "30m" => Ok(TimeFrame::Min30), 279 | "1h" => Ok(TimeFrame::Hour1), 280 | "2h" => Ok(TimeFrame::Hour2), 281 | "4h" => Ok(TimeFrame::Hour4), 282 | "12h" => Ok(TimeFrame::Hour12), 283 | "1d" => Ok(TimeFrame::Day1), 284 | "3d" => Ok(TimeFrame::Day3), 285 | "w" => Ok(TimeFrame::Week), 286 | "m" => Ok(TimeFrame::Month), 287 | _ => Err(format!("Invalid TimeFrame string: '{}'", s)), 288 | } 289 | } 290 | } 291 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /src/signal/types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::collections::HashMap; 3 | use std::sync::Arc; 4 | 5 | use std::hash::BuildHasherDefault; 6 | use rustc_hash::FxHasher; 7 | 8 | use arraydeque::{ArrayDeque, behavior::Wrapping}; 9 | use kwant::indicators::{Rsi, Atr, StochasticRsi, Price, Indicator, Ema, EmaCross, Sma, SmaRsi, Adx, Value}; 10 | 11 | use crate::trade_setup::TimeFrame; 12 | use crate::helper::get_time_now; 13 | use crate::MAX_HISTORY; 14 | use crate::IndicatorData; 15 | 16 | use serde::{Deserialize, Serialize}; 17 | 18 | #[derive(Debug, Copy, Clone)] 19 | pub struct ExecParams{ 20 | pub margin: f64, 21 | pub lev: u32, 22 | pub tf: TimeFrame, 23 | } 24 | 25 | impl ExecParams{ 26 | pub fn new(margin: f64, lev:u32, tf: TimeFrame)-> Self{ 27 | Self{ 28 | margin, 29 | lev, 30 | tf, 31 | } 32 | } 33 | } 34 | 35 | pub enum ExecParam{ 36 | Margin(f64), 37 | Lev(u32), 38 | Tf(TimeFrame), 39 | } 40 | 41 | 42 | #[derive(Debug, Clone,Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] 43 | #[serde(rename_all = "camelCase")] 44 | pub enum IndicatorKind{ 45 | Rsi(u32), 46 | SmaOnRsi{periods: u32, smoothing_length: u32}, 47 | StochRsi{periods: u32, k_smoothing: Option, d_smoothing: Option}, 48 | Adx{periods: u32, di_length: u32}, 49 | Atr(u32), 50 | Ema(u32), 51 | EmaCross{short:u32, long:u32}, 52 | Sma(u32), 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct Handler{ 57 | pub indicator: Box, 58 | pub is_active: bool, 59 | } 60 | 61 | impl Handler{ 62 | 63 | pub fn new(indicator: IndicatorKind) -> Handler{ 64 | Handler{ 65 | indicator: match_kind(indicator), 66 | is_active: true, 67 | } 68 | } 69 | 70 | fn toggle(&mut self) -> bool{ 71 | self.is_active = !self.is_active; 72 | self.is_active 73 | } 74 | 75 | pub fn update(&mut self,price: Price, after_close: bool){ 76 | if !after_close{ 77 | self.indicator.update_before_close(price); 78 | }else{ 79 | self.indicator.update_after_close(price); 80 | } 81 | } 82 | pub fn get_value(&self) -> Option{ 83 | self.indicator.get_last() 84 | } 85 | 86 | pub fn load<'a,I: IntoIterator>(&mut self, price_data: I){ 87 | let data_vec: Vec = price_data.into_iter().copied().collect(); 88 | self.indicator.load(&data_vec); 89 | } 90 | 91 | pub fn reset(&mut self){ 92 | self.indicator.reset(); 93 | } 94 | 95 | } 96 | 97 | 98 | unsafe impl Send for Handler {} 99 | 100 | 101 | pub type IndexId = (IndicatorKind, TimeFrame); 102 | 103 | fn match_kind(kind: IndicatorKind) -> Box { 104 | match kind { 105 | IndicatorKind::Rsi (periods) => { 106 | Box::new(Rsi::new(periods, periods, None,None, None)) 107 | } 108 | IndicatorKind::SmaOnRsi{periods, smoothing_length} => { 109 | Box::new(SmaRsi::new(periods, smoothing_length)) 110 | } 111 | IndicatorKind::StochRsi{periods, k_smoothing, d_smoothing}=> { 112 | Box::new(StochasticRsi::new(periods, k_smoothing, d_smoothing)) 113 | } 114 | IndicatorKind::Adx { periods, di_length } => { 115 | Box::new(Adx::new(periods, di_length)) 116 | } 117 | IndicatorKind::Atr(periods) => { 118 | Box::new(Atr::new(periods)) 119 | } 120 | IndicatorKind::Ema(periods) => { 121 | Box::new(Ema::new(periods)) 122 | } 123 | IndicatorKind::EmaCross { short, long } => { 124 | Box::new(EmaCross::new(short, long)) 125 | } 126 | IndicatorKind::Sma(periods) => { 127 | Box::new(Sma::new(periods)) 128 | } 129 | } 130 | } 131 | 132 | 133 | type History = Box>; 134 | 135 | #[derive(Debug)] 136 | pub struct Tracker{ 137 | pub price_data: History, 138 | pub indicators: HashMap>, 139 | tf: TimeFrame, 140 | next_close: u64, 141 | } 142 | 143 | 144 | 145 | impl Tracker{ 146 | pub fn new(tf: TimeFrame) -> Self{ 147 | Tracker{ 148 | price_data: Box::new(ArrayDeque::new()), 149 | indicators: HashMap::default(), 150 | tf, 151 | next_close: Self::calc_next_close(tf), 152 | } 153 | } 154 | 155 | 156 | pub fn digest(&mut self, price: Price){ 157 | let time = get_time_now(); 158 | 159 | if time >= self.next_close{ 160 | self.next_close = Self::calc_next_close(self.tf); 161 | self.price_data.push_back(price); 162 | self.update_indicators(price, true); 163 | }else{ 164 | self.update_indicators(price, false); 165 | } 166 | 167 | } 168 | 169 | fn update_indicators(&mut self,price: Price, after_close: bool){ 170 | 171 | for (_kind, handler) in &mut self.indicators{ 172 | handler.update(price, after_close); 173 | } 174 | } 175 | 176 | fn calc_next_close(tf: TimeFrame)-> u64 { 177 | let now = get_time_now(); 178 | 179 | let tf_ms = tf.to_millis(); 180 | ((now / tf_ms) + 1) * tf_ms 181 | } 182 | 183 | 184 | pub async fn load>(&mut self, price_data: I){ 185 | let buffer: Vec = price_data.into_iter().collect(); 186 | let safe_buff: Arc<[Price]> = buffer.clone().into(); 187 | 188 | let mut handles: Vec> = Vec::new(); 189 | let mut temp_handlers = std::mem::take(&mut self.indicators); 190 | 191 | for (kind, mut handler) in temp_handlers{ 192 | let buff = safe_buff.clone(); 193 | 194 | let handle = tokio::spawn(async move{ 195 | handler.load(&*buff); 196 | (kind, handler) 197 | }); 198 | 199 | handles.push(handle); 200 | } 201 | 202 | let new_indicators: HashMap> = futures::future::join_all(handles) 203 | .await 204 | .into_iter() 205 | .map(Result::unwrap) // unwrap JoinHandle 206 | .collect(); 207 | 208 | self.indicators = new_indicators; 209 | self.price_data.extend(buffer); 210 | } 211 | 212 | 213 | pub fn add_indicator(&mut self, kind: IndicatorKind, load: bool){ 214 | let mut handler = Handler::new(kind); 215 | if load{ 216 | handler.load(&*self.price_data); 217 | } 218 | self.indicators.insert(kind, handler); 219 | } 220 | 221 | pub fn remove_indicator(&mut self, kind: IndicatorKind){ 222 | self.indicators.remove(&kind); 223 | } 224 | 225 | pub fn toggle_indicator(&mut self, kind: IndicatorKind){ 226 | if let Some(handler) = self.indicators.get_mut(&kind){ 227 | let _ = handler.toggle(); 228 | } 229 | } 230 | 231 | pub fn get_active_values(&self) -> Vec{ 232 | let mut values = Vec::new(); 233 | for (_kind, handler) in &self.indicators{ 234 | if let Some(val) = handler.get_value(){ 235 | values.push(val); 236 | } 237 | } 238 | values 239 | } 240 | 241 | 242 | pub fn get_indicators_data(&self) -> Vec{ 243 | let mut values = Vec::new(); 244 | for (kind, handler) in &self.indicators{ 245 | if let Some(val) = handler.get_value(){ 246 | values.push( 247 | IndicatorData{ 248 | id: (*kind, self.tf), 249 | value: Some(val), 250 | } 251 | ); 252 | } 253 | } 254 | values 255 | } 256 | 257 | pub fn reset(&mut self){ 258 | self.price_data.clear(); 259 | for (_kind, handler) in &mut self.indicators{ 260 | handler.reset(); 261 | } 262 | } 263 | 264 | } 265 | 266 | 267 | 268 | pub type TimeFrameData = HashMap>; 269 | 270 | #[derive(Copy, Clone, Debug,PartialEq, Deserialize)] 271 | #[serde(rename_all = "camelCase")] 272 | pub struct Entry{ 273 | pub id: IndexId, 274 | pub edit: EditType 275 | } 276 | 277 | #[derive(Copy, Clone, Debug, Deserialize, PartialEq)] 278 | #[serde(rename_all = "camelCase")] 279 | pub enum EditType{ 280 | Toggle, 281 | Add, 282 | Remove, 283 | } 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /src/bin/enginetest.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | #![allow(unused_mut)] 3 | #![allow(unused_variables)] 4 | #![allow(dead_code)] 5 | 6 | use std::{ 7 | env, fs, 8 | str::FromStr, 9 | }; 10 | 11 | // 12 | use dotenv::dotenv; 13 | use log::info; 14 | use std::sync::Arc; 15 | use hyperliquid_rust_sdk::Error; 16 | use hyperliquid_rust_bot::{ 17 | Bot, 18 | BotEvent, 19 | MarginAllocation, 20 | AssetMargin, 21 | BotToMarket, 22 | MarketCommand, 23 | IndexId, Entry, EditType, IndicatorKind, 24 | MARKETS, 25 | TradeParams,TimeFrame, AddMarketInfo, UpdateFrontend, 26 | 27 | LocalWallet, Wallet, BaseUrl, 28 | }; 29 | use hyperliquid_rust_bot::strategy::{Strategy, CustomStrategy, Risk, Style, Stance}; 30 | 31 | use tokio::{ 32 | sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, 33 | time::{sleep, Duration}, 34 | }; 35 | 36 | 37 | const COIN: &str = "BTC"; 38 | const URL: BaseUrl = BaseUrl::Mainnet; 39 | 40 | #[tokio::main] 41 | async fn main() -> Result<(), Error>{ 42 | use IndicatorKind::*; 43 | env_logger::init(); 44 | match URL{ 45 | BaseUrl::Mainnet => dotenv().ok(), 46 | BaseUrl::Testnet => dotenv::from_filename("testnet").ok(), 47 | BaseUrl::Localhost => dotenv::from_filename(".env.test").ok(), 48 | }; 49 | let wallet = load_wallet(BaseUrl::Mainnet).await?; 50 | let strat = Strategy::Custom(load_strategy("./config.toml")); 51 | 52 | let trade_params = TradeParams{ 53 | strategy: strat, 54 | lev: 20, 55 | trade_time: 300, 56 | time_frame: TimeFrame::from_str("5m").unwrap_or(TimeFrame::Min1), 57 | 58 | }; 59 | 60 | let config = Vec::from([ 61 | ( 62 | IndicatorKind::Rsi(12), 63 | TimeFrame::Min1, 64 | ), 65 | ( 66 | IndicatorKind::SmaOnRsi{periods: 14, smoothing_length: 9}, 67 | TimeFrame::Hour1, 68 | ), 69 | ( 70 | IndicatorKind::StochRsi{periods: 16,k_smoothing: Some(4), d_smoothing: Some(4)}, 71 | TimeFrame::Hour4, 72 | ), 73 | 74 | ( 75 | IndicatorKind::EmaCross{short: 21, long: 200}, 76 | TimeFrame::Day1, 77 | ), 78 | ( 79 | IndicatorKind::Adx { 80 | periods: 14, 81 | di_length: 14, 82 | }, 83 | TimeFrame::Min5, 84 | ), 85 | ( 86 | IndicatorKind::Atr(14), 87 | TimeFrame::Min15, 88 | ), 89 | ( 90 | IndicatorKind::Sma(50), 91 | TimeFrame::Hour1, 92 | ), 93 | ]); 94 | 95 | let (app_tx, mut app_rv) = unbounded_channel::(); 96 | 97 | let (mut bot, sender) = Bot::new(wallet).await?; 98 | 99 | tokio::spawn(async move { 100 | bot.start(app_tx).await; 101 | }); 102 | 103 | 104 | 105 | tokio::spawn(async move { 106 | let market_add = AddMarketInfo{ 107 | asset: COIN.to_string(), 108 | margin_alloc: MarginAllocation::Alloc(0.1), 109 | trade_params: trade_params.clone(), 110 | config: Some(config), 111 | }; 112 | let market_add2 = AddMarketInfo{ 113 | asset: "SOL".to_string(), 114 | margin_alloc: MarginAllocation::Alloc(0.1), 115 | trade_params: TradeParams::default(), 116 | config: None, 117 | }; 118 | let market_add3 = AddMarketInfo{ 119 | asset: "xrp ".to_string(), 120 | margin_alloc: MarginAllocation::Amount(50.0), 121 | trade_params: trade_params, 122 | config: None, 123 | }; 124 | let cmd = BotToMarket{ 125 | asset:"BTC".to_string(), 126 | cmd: MarketCommand::UpdateLeverage(40), 127 | }; 128 | 129 | let cmd2 = BotToMarket{ 130 | asset: "SOL".to_string(), 131 | cmd: MarketCommand::EditIndicators(Vec::from([Entry{id: (Ema(33), TimeFrame::Hour4),edit: EditType::Add}])), 132 | }; 133 | 134 | let cmd3 = BotToMarket{ 135 | asset: "XRP".to_string(), 136 | cmd: MarketCommand::EditIndicators(Vec::from([Entry{id: (Ema(33), TimeFrame::Hour4),edit: EditType::Toggle}, 137 | Entry{id: (Rsi(12), TimeFrame::Min1),edit: EditType::Add}, 138 | ]) 139 | ), 140 | }; 141 | 142 | let _ = sleep(Duration::from_secs(5)).await; 143 | sender.send(BotEvent::AddMarket(market_add.clone())); 144 | let _ = sleep(Duration::from_secs(5)).await; 145 | sender.send(BotEvent::AddMarket(market_add2)); 146 | sender.send(BotEvent::AddMarket(market_add3)); 147 | //let _ = sleep(Duration::from_secs(20)).await; 148 | //sender.send(BotEvent::RemoveMarket("BTC".to_string())); 149 | let _ = sleep(Duration::from_secs(5)).await; 150 | sender.send(BotEvent::MarketComm(cmd)); 151 | sender.send(BotEvent::MarketComm(cmd2)); 152 | let _ = sleep(Duration::from_secs(5)).await; 153 | sender.send(BotEvent::MarketComm(cmd3)); 154 | let _ = sleep(Duration::from_secs(10)).await; 155 | 156 | sender.send(BotEvent::PauseAll); 157 | let _ = sleep(Duration::from_secs(10)).await; 158 | sender.send(BotEvent::ResumeAll); 159 | let _ = sleep(Duration::from_secs(10)).await; 160 | sender.send(BotEvent::CloseAll); 161 | let _ = sleep(Duration::from_secs(10)).await; 162 | sender.send(BotEvent::AddMarket(market_add)); 163 | }); 164 | 165 | 166 | while let Some(update) = app_rv.recv().await{ 167 | info!("FRONT END RECEIVED {:?}", update); 168 | } 169 | 170 | /* tokio::spawn(async move{ 171 | 172 | /*let _ = sleep(Duration::from_secs(10)).await; 173 | sender.send(MarketCommand::UpdateLeverage(50)).await; 174 | let _ = sleep(Duration::from_secs(10)).await; 175 | sender.send(MarketCommand::UpdateLeverage(40)).await;*/ 176 | 177 | //let _ = sleep(Duration::from_secs(120)).await; 178 | //sender.send(MarketCommand::Pause).await; 179 | let _ = sleep(Duration::from_secs(20)).await; 180 | //sender.send(MarketCommand::UpdateTimeFrame(TimeFrame::from_str("4h").unwrap())).await; 181 | let _ = sender.send(MarketCommand::EditIndicators(Vec::from([Entry{id: (Ema(33), TimeFrame::Hour1),edit: EditType::Add}, 182 | Entry{id: (SmaOnRsi{periods: 12, smoothing_length: 9}, TimeFrame::Min1),edit: EditType::Add} 183 | ]))).await; 184 | 185 | let _ = sleep(Duration::from_secs(20)).await; 186 | let _ = sender.send(MarketCommand::EditIndicators(Vec::from([Entry{id: (Ema(33), TimeFrame::Hour4),edit: EditType::Add}]))).await; 187 | let _ = sleep(Duration::from_secs(10)).await; 188 | let _ = sender.send(MarketCommand::EditIndicators(Vec::from([Entry{id: (Ema(33), TimeFrame::Hour1),edit: EditType::Toggle}]))).await; 189 | 190 | let _ = sleep(Duration::from_secs(20)).await; 191 | let _ = sender.send(MarketCommand::EditIndicators(Vec::from([Entry{id: (Sma(10), TimeFrame::Min5),edit: EditType::Add}]))).await; 192 | let _ = sleep(Duration::from_secs(20)).await; 193 | sender.send(MarketCommand::EditIndicators(Vec::from([Entry{id: (Ema(10), TimeFrame::Hour4),edit: EditType::Remove}, 194 | Entry{id: (Sma(10), TimeFrame::Min5),edit: EditType::Remove}, 195 | Entry{id: (Atr(14), TimeFrame::Min15),edit: EditType::Remove}, 196 | Entry{id: (Rsi(12), TimeFrame::Min1),edit: EditType::Toggle} 197 | ]))).await; 198 | let _ = sleep(Duration::from_secs(30)).await; 199 | let _ = sender.send(MarketCommand::EditIndicators(Vec::from([]))).await; 200 | let _ =sender.send(MarketCommand::EditIndicators(Vec::from([Entry{id: (Rsi(12), TimeFrame::Min1),edit: EditType::Toggle}]))).await; 201 | let _ = sleep(Duration::from_secs(100000)).await; 202 | sender.send(MarketCommand::Close).await; 203 | //let _ = sleep(Duration::from_secs(30)).await; 204 | //let _ = sender.send(MarketCommand::Close).await; 205 | }); 206 | */ 207 | 208 | Ok(()) 209 | } 210 | 211 | 212 | 213 | 214 | fn load_strategy(path: &str) -> CustomStrategy { 215 | let content = fs::read_to_string(path).expect("failed to read file"); 216 | toml::from_str(&content).expect("failed to parse toml") 217 | } 218 | 219 | 220 | async fn load_wallet(url: BaseUrl) -> Result{ 221 | let wallet = std::env::var("PRIVATE_KEY").expect("Error fetching PRIVATE_KEY") 222 | .parse(); 223 | 224 | if let Err(ref e) = wallet{ 225 | return Err(Error::Custom(format!("Failed to load wallet: {}", e))); 226 | } 227 | let pubkey: String = std::env::var("WALLET").expect("Error fetching WALLET address"); 228 | Ok(Wallet::new(url , pubkey, wallet.unwrap()).await?) 229 | } 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /src/signal/signal.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::hash::BuildHasherDefault; 3 | use rustc_hash::FxHasher; 4 | 5 | use log::info; 6 | 7 | use kwant::indicators::{Price, Indicator, Value}; 8 | 9 | use crate::trade_setup::{TimeFrame,TradeParams, TradeCommand}; 10 | use crate::strategy::Strategy; 11 | use crate::{IndicatorData, MarketCommand}; 12 | 13 | use tokio::sync::mpsc::{UnboundedReceiver, Sender as tokioSender, unbounded_channel}; 14 | use flume::{Sender, bounded}; 15 | 16 | use super::types::{ 17 | Tracker, 18 | IndexId, 19 | ExecParam, 20 | ExecParams, 21 | TimeFrameData, 22 | EditType, 23 | Entry, 24 | }; 25 | 26 | 27 | pub struct SignalEngine{ 28 | engine_rv: UnboundedReceiver, 29 | trade_tx: Sender, 30 | data_tx: Option>, 31 | trackers: HashMap, BuildHasherDefault>, 32 | strategy: Strategy, 33 | exec_params: ExecParams, 34 | } 35 | 36 | 37 | 38 | impl SignalEngine{ 39 | 40 | pub async fn new( 41 | config: Option>, 42 | trade_params: TradeParams, 43 | engine_rv: UnboundedReceiver, 44 | data_tx: Option>, 45 | trade_tx: Sender, 46 | margin: f64, 47 | ) -> Self{ 48 | let mut trackers:HashMap, BuildHasherDefault> = HashMap::default(); 49 | trackers.insert(trade_params.time_frame, Box::new(Tracker::new(trade_params.time_frame))); 50 | 51 | if let Some(list) = config{ 52 | if !list.is_empty(){ 53 | for id in list{ 54 | if let Some(tracker) = &mut trackers.get_mut(&id.1){ 55 | tracker.add_indicator(id.0, false); 56 | }else{ 57 | let mut new_tracker = Tracker::new(id.1); 58 | new_tracker.add_indicator(id.0, false); 59 | trackers.insert(id.1, Box::new(new_tracker)); 60 | } 61 | } 62 | }}; 63 | 64 | SignalEngine{ 65 | engine_rv, 66 | trade_tx, 67 | data_tx, 68 | trackers, 69 | strategy: trade_params.strategy, 70 | exec_params: ExecParams::new(margin, trade_params.lev, trade_params.time_frame), 71 | } 72 | } 73 | 74 | pub fn reset(&mut self){ 75 | for (_tf, tracker) in &mut self.trackers{ 76 | tracker.reset(); 77 | } 78 | } 79 | 80 | 81 | pub fn add_indicator(&mut self, id: IndexId){ 82 | if let Some(tracker) = &mut self.trackers.get_mut(&id.1){ 83 | tracker.add_indicator(id.0, true); 84 | }else{ 85 | let mut new_tracker = Tracker::new(id.1); 86 | new_tracker.add_indicator(id.0, false); 87 | self.trackers.insert(id.1, Box::new(new_tracker)); 88 | } 89 | } 90 | 91 | pub fn remove_indicator(&mut self, id: IndexId){ 92 | if let Some(tracker) = &mut self.trackers.get_mut(&id.1){ 93 | tracker.remove_indicator(id.0); 94 | } 95 | } 96 | 97 | pub fn toggle_indicator(&mut self, id: IndexId){ 98 | if let Some(tracker) = &mut self.trackers.get_mut(&id.1){ 99 | tracker.toggle_indicator(id.0); 100 | } 101 | } 102 | 103 | pub fn get_active_indicators(&self) -> Vec{ 104 | let mut active = Vec::new(); 105 | for (tf, tracker) in &self.trackers{ 106 | for (kind, handler) in &tracker.indicators{ 107 | if handler.is_active{ 108 | active.push((*kind, *tf)); 109 | } 110 | } 111 | } 112 | active 113 | } 114 | 115 | pub fn get_active_values(&self) -> Vec{ 116 | let mut values = Vec::new(); 117 | for (_tf, tracker) in &self.trackers{ 118 | values.extend(tracker.get_active_values()); 119 | } 120 | values 121 | } 122 | 123 | pub fn get_indicators_data(&self) -> Vec{ 124 | let mut values = Vec::new(); 125 | for (_tf, tracker) in &self.trackers{ 126 | values.extend(tracker.get_indicators_data()); 127 | } 128 | values 129 | } 130 | 131 | pub fn display_values(&self){ 132 | for (tf, tracker) in &self.trackers{ 133 | for (kind, handler) in &tracker.indicators{ 134 | if handler.is_active{ 135 | info!("\nKind: {:?} TF: {}\nValue: {:?}\n", kind, tf.as_str(), handler.get_value()); 136 | } 137 | } 138 | } 139 | } 140 | 141 | pub fn change_strategy(&mut self, strategy: Strategy){ 142 | self.strategy = strategy; 143 | info!("Strategy changed to: {:?}", self.strategy); 144 | } 145 | 146 | pub fn get_strategy(&self) -> &Strategy{ 147 | &self.strategy 148 | } 149 | 150 | pub async fn load>(&mut self,tf: TimeFrame, price_data: I) { 151 | if let Some(tracker) = self.trackers.get_mut(&tf){ 152 | tracker.load(price_data).await 153 | } 154 | } 155 | 156 | 157 | fn get_signal(&self, price: f64, values: Vec) -> Option{ 158 | 159 | match self.strategy{ 160 | Strategy::Custom(brr) => brr.generate_signal(values, price, self.exec_params) 161 | } 162 | } 163 | 164 | } 165 | 166 | impl SignalEngine{ 167 | 168 | pub async fn start(&mut self){ 169 | 170 | let mut tick: u64 = 0; 171 | 172 | while let Some(cmd) = self.engine_rv.recv().await{ 173 | 174 | match cmd { 175 | 176 | EngineCommand::UpdatePrice(price) => { 177 | for (_tf, tracker) in &mut self.trackers{ 178 | tracker.digest(price); 179 | } 180 | 181 | //self.display_indicators(price.close); 182 | let ind = self.get_indicators_data(); 183 | let values: Vec = ind.iter().filter_map(|t| t.value).collect(); 184 | 185 | if tick % 5 == 0{ 186 | if let Some(sender) = &self.data_tx{ 187 | sender.send(MarketCommand::UpdateIndicatorData(ind)).await; 188 | } 189 | } 190 | 191 | if let Some(trade) = self.get_signal(price.close, values){ 192 | let _ = self.trade_tx.try_send(trade); 193 | } 194 | 195 | tick += 1; 196 | }, 197 | 198 | EngineCommand::UpdateStrategy(new_strat) =>{ 199 | self.change_strategy(new_strat); 200 | }, 201 | 202 | 203 | EngineCommand::EditIndicators{indicators, price_data} =>{ 204 | info!("Received Indicator Edit Vec of length : {}", indicators.len()); 205 | 206 | 207 | for entry in indicators{ 208 | match entry.edit{ 209 | EditType::Add => { self.add_indicator(entry.id);}, 210 | EditType::Remove => {self.remove_indicator(entry.id);}, 211 | EditType::Toggle => {self.toggle_indicator(entry.id)}, 212 | } 213 | } 214 | if let Some(data) = price_data{ 215 | for (tf, prices) in data{ 216 | self.load(tf, prices); 217 | } 218 | } 219 | 220 | } 221 | 222 | EngineCommand::UpdateExecParams(param)=>{ 223 | use ExecParam::*; 224 | match param{ 225 | Margin(m)=>{ 226 | self.exec_params.margin = m; 227 | }, 228 | Lev(l) => { 229 | self.exec_params.lev = l; 230 | }, 231 | Tf(t) => { 232 | self.exec_params.tf = t; 233 | }, 234 | } 235 | }, 236 | 237 | EngineCommand::Stop =>{ 238 | return; 239 | }, 240 | } 241 | } 242 | } 243 | 244 | pub fn display_indicators(&mut self, price: f64){ 245 | info!("\nPrice => {}\n", price); 246 | //let vec = self.get_active_indicators(); 247 | self.display_values(); 248 | //Update 249 | } 250 | 251 | 252 | 253 | pub fn new_backtest(trade_params: TradeParams, config: Option>, margin: f64) -> Self{ 254 | let mut trackers:HashMap, BuildHasherDefault> = HashMap::default(); 255 | trackers.insert(trade_params.time_frame, Box::new(Tracker::new(trade_params.time_frame))); 256 | 257 | if let Some(list) = config{ 258 | if !list.is_empty(){ 259 | for id in list{ 260 | if let Some(tracker) = &mut trackers.get_mut(&id.1){ 261 | tracker.add_indicator(id.0, false); 262 | }else{ 263 | let mut new_tracker = Tracker::new(id.1); 264 | new_tracker.add_indicator(id.0, false); 265 | trackers.insert(id.1, Box::new(new_tracker)); 266 | } 267 | } 268 | }} 269 | 270 | 271 | //channels won't be used in backtesting, these are placeholders 272 | let (_tx, dummy_rv) = unbounded_channel::(); 273 | let (dummy_tx, _rx) = bounded::(0); 274 | 275 | SignalEngine{ 276 | engine_rv: dummy_rv, 277 | trade_tx: dummy_tx, 278 | data_tx: None, 279 | trackers, 280 | strategy: trade_params.strategy, 281 | exec_params: ExecParams{margin, lev: trade_params.lev, tf: trade_params.time_frame}, 282 | } 283 | } 284 | } 285 | 286 | 287 | 288 | 289 | pub enum EngineCommand{ 290 | 291 | UpdatePrice(Price), 292 | UpdateStrategy(Strategy), 293 | EditIndicators{indicators: Vec,price_data: Option}, 294 | UpdateExecParams(ExecParam), 295 | Stop, 296 | } 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | -------------------------------------------------------------------------------- /src/frontend/interface/src/components/AddMarket.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react'; 2 | import { into, TIMEFRAME_CAMELCASE, indicatorLabels, indicatorColors } from '../types'; 3 | import type { 4 | TimeFrame, 5 | Risk, 6 | Style, 7 | Stance, 8 | CustomStrategy, 9 | TradeParams, 10 | AddMarketInfo, 11 | IndexId, 12 | IndicatorKind, 13 | AddMarketProps, 14 | } from '../types'; 15 | 16 | const riskOptions: Risk[] = ['Low', 'Normal', 'High']; 17 | const styleOptions: Style[] = ['Scalp', 'Swing']; 18 | const stanceOptions: Stance[] = ['Bull', 'Bear', 'Neutral']; 19 | const indicatorKinds: IndicatorKind[] = ['rsi', 'smaOnRsi', 'stochRsi', 'adx', 'atr', 'ema', 'emaCross', 'sma']; 20 | 21 | export const AddMarket: React.FC = ({ onClose, totalMargin }) => { 22 | const [asset, setAsset] = useState(''); 23 | const [marginType, setMarginType] = useState<'alloc' | 'amount'>('alloc'); 24 | const [marginValue, setMarginValue] = useState(0.1); 25 | const [tfSymbol, setTfSymbol] = useState('1m'); 26 | const [lev, setLev] = useState(1); 27 | const [tradeTime, setTradeTime] = useState(0); 28 | const [risk, setRisk] = useState('Low'); 29 | const [style, setStyle] = useState 255 |
256 | ); 257 | } 258 | -------------------------------------------------------------------------------- /src/executor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | 4 | use ethers::signers::LocalWallet; 5 | use flume::Receiver; 6 | use log::info; 7 | use tokio::{ 8 | sync::{mpsc::Sender, Mutex}, 9 | time::{sleep, Duration}, 10 | }; 11 | 12 | use hyperliquid_rust_sdk::{ 13 | Error,BaseUrl, ExchangeClient, ExchangeDataStatus, ExchangeResponseStatus, MarketOrderParams, 14 | }; 15 | 16 | use crate::trade_setup::{TradeCommand, TradeFillInfo, TradeInfo, LiquidationFillInfo}; 17 | use crate::market::MarketCommand; 18 | 19 | 20 | 21 | 22 | pub struct Executor { 23 | trade_rv: Receiver, 24 | market_tx: Sender, 25 | asset: String, 26 | exchange_client: Arc, 27 | is_paused: bool, 28 | fees: (f64, f64), 29 | open_position: Arc>>, 30 | } 31 | 32 | 33 | 34 | impl Executor { 35 | 36 | pub async fn new( 37 | wallet: LocalWallet, 38 | asset: String, 39 | fees: (f64, f64), 40 | trade_rv: Receiver, 41 | market_tx: Sender, 42 | ) -> Result{ 43 | 44 | let exchange_client = Arc::new(ExchangeClient::new(None, wallet, Some(BaseUrl::Mainnet), None, None).await?); 45 | Ok(Executor{ 46 | trade_rv, 47 | market_tx, 48 | asset, 49 | exchange_client, 50 | is_paused: false, 51 | fees, 52 | open_position: Arc::new(Mutex::new(None)), 53 | }) 54 | } 55 | 56 | async fn try_trade(client: Arc, params: MarketOrderParams<'_>) -> Result{ 57 | 58 | let response = client 59 | .market_open(params) 60 | .await 61 | .map_err(|e| format!("Transport failure, {}",e))?; 62 | 63 | info!("Market order placed: {response:?}"); 64 | 65 | let response = match response { 66 | ExchangeResponseStatus::Ok(exchange_response) => exchange_response, 67 | ExchangeResponseStatus::Err(e) => { 68 | return Err(format!("Exchange Error: Couldn't execute trade => {}",e)); 69 | } 70 | }; 71 | 72 | let status = response 73 | .data 74 | .filter(|d| !d.statuses.is_empty()) 75 | .and_then(|d| d.statuses.get(0).cloned()) 76 | .ok_or_else(|| "Exchange Error: Couldn't fetch trade status".to_string())?; 77 | 78 | Ok(status) 79 | 80 | } 81 | pub async fn open_order(&self,size: f64, is_long: bool) -> Result{ 82 | 83 | let market_open_params = MarketOrderParams { 84 | asset: self.asset.as_str(), 85 | is_buy: is_long, 86 | sz: size as f64, 87 | px: None, 88 | slippage: Some(0.01), // 1% slippage 89 | cloid: None, 90 | wallet: None, 91 | }; 92 | 93 | 94 | let status = Self::try_trade(self.exchange_client.clone(), market_open_params).await?; 95 | 96 | match status{ 97 | 98 | ExchangeDataStatus::Filled(ref order) => { 99 | 100 | println!("Open order filled: {order:?}"); 101 | let sz: f64 = order.total_sz.parse::().unwrap(); 102 | let price: f64 = order.avg_px.parse::().unwrap(); 103 | let fill_info = TradeFillInfo{fill_type: "Open".to_string(),sz, price, oid: order.oid, is_long}; 104 | 105 | Ok(fill_info) 106 | }, 107 | 108 | _ => Err("Open order not filled".to_string()), 109 | } 110 | 111 | 112 | } 113 | pub async fn close_order(&self, size: f64, is_long: bool) -> Result { 114 | 115 | let market_close_params = MarketOrderParams{ 116 | asset: self.asset.as_str(), 117 | is_buy: !is_long, 118 | sz: size as f64, 119 | px: None, 120 | slippage: Some(0.01), // 1% slippage 121 | cloid: None, 122 | wallet: None, 123 | }; 124 | 125 | 126 | 127 | let status = Self::try_trade(self.exchange_client.clone(),market_close_params).await?; 128 | match status{ 129 | 130 | ExchangeDataStatus::Filled(ref order) => { 131 | 132 | println!("Close order filled: {order:?}"); 133 | let sz: f64 = order.total_sz.parse::().unwrap(); 134 | let price: f64 = order.avg_px.parse::().unwrap(); 135 | let fill_info = TradeFillInfo{fill_type: "Close".to_string(),sz, price, oid: order.oid, is_long}; 136 | return Ok(fill_info); 137 | }, 138 | 139 | _ => Err("Close order not filled".to_string()), 140 | } 141 | } 142 | 143 | 144 | pub async fn close_order_static(client: Arc,asset: String, size: f64, is_long: bool) -> Result{ 145 | 146 | 147 | let market_close_params = MarketOrderParams { 148 | asset: asset.as_str(), 149 | is_buy: !is_long, 150 | sz: size as f64, 151 | px: None, 152 | slippage: Some(0.01), // 1% slippage 153 | cloid: None, 154 | wallet: None, 155 | }; 156 | 157 | let status = Self::try_trade(client,market_close_params).await?; 158 | match status{ 159 | 160 | ExchangeDataStatus::Filled(ref order) => { 161 | 162 | println!("Close order filled: {order:?}"); 163 | let sz: f64 = order.total_sz.parse::().unwrap(); 164 | let price: f64 = order.avg_px.parse::().unwrap(); 165 | let fill_info = TradeFillInfo{fill_type: "Close".to_string(),sz, price, oid: order.oid, is_long}; 166 | return Ok(fill_info); 167 | }, 168 | 169 | _ => Err("Close order not filled".to_string()), 170 | } 171 | 172 | 173 | } 174 | 175 | 176 | fn get_trade_info(open: TradeFillInfo, close: TradeFillInfo, fees: &(f64, f64)) -> TradeInfo{ 177 | let is_long = open.is_long; 178 | let (fee, pnl) = Self::calculate_pnl(fees,is_long, &open, &close); 179 | 180 | TradeInfo{ 181 | open: open.price, 182 | close: close.price, 183 | pnl, 184 | fee, 185 | is_long, 186 | duration: None, 187 | oid: (open.oid, close.oid), 188 | } 189 | } 190 | 191 | 192 | 193 | 194 | 195 | fn calculate_pnl(fees: &(f64, f64) ,is_long: bool, trade_fill_open: &TradeFillInfo, trade_fill_close: &TradeFillInfo) -> (f64, f64){ 196 | let fee_open = trade_fill_open.sz * trade_fill_open.price * fees.1; 197 | let fee_close = trade_fill_close.sz * trade_fill_close.price * fees.1; 198 | 199 | let pnl = if is_long{ 200 | trade_fill_close.sz * (trade_fill_close.price - trade_fill_open.price) - fee_open - fee_close 201 | }else{ 202 | trade_fill_close.sz * (trade_fill_open.price - trade_fill_close.price) - fee_open - fee_close 203 | }; 204 | 205 | (fee_open + fee_close, pnl) 206 | } 207 | 208 | 209 | pub async fn cancel_trade(&mut self) -> Option{ 210 | 211 | if let Some(pos) = self.open_position.lock().await.take(){ 212 | let trade_fill = self.close_order(pos.sz, pos.is_long).await; 213 | if let Ok(close) = trade_fill{ 214 | let trade_info = Self::get_trade_info(pos, close, &self.fees); 215 | return Some(trade_info); 216 | } 217 | } 218 | 219 | None 220 | } 221 | 222 | async fn is_active(&self) -> bool{ 223 | let guard = self.open_position.lock().await; 224 | guard.is_some() 225 | } 226 | 227 | fn toggle_pause(&mut self){ 228 | self.is_paused = !self.is_paused 229 | } 230 | 231 | 232 | pub async fn start(mut self){ 233 | println!("EXECUTOR STARTED"); 234 | 235 | let info_sender = self.market_tx.clone(); 236 | while let Ok(cmd) = self.trade_rv.recv_async().await{ 237 | 238 | match cmd{ 239 | TradeCommand::ExecuteTrade {size, is_long, duration} => { 240 | 241 | if self.is_active().await || self.is_paused{continue}; 242 | let trade_info = self.open_order(size, is_long).await; 243 | if let Ok(trade_fill) = trade_info{ 244 | { 245 | let mut pos = self.open_position.lock().await; 246 | *pos = Some(trade_fill.clone()); 247 | } 248 | 249 | let client = self.exchange_client.clone(); 250 | let asset = self.asset.clone(); 251 | let fees = self.fees; 252 | let sender = info_sender.clone(); 253 | let pos_handle = self.open_position.clone(); 254 | tokio::spawn(async move{ 255 | let _ = sleep(Duration::from_secs(duration)).await; 256 | let maybe_open = { 257 | let mut pos = pos_handle.lock().await; 258 | pos.take() 259 | }; 260 | 261 | if let Some(open) = maybe_open{ 262 | 263 | let close_fill = Self::close_order_static(client, asset, open.sz, is_long).await; 264 | if let Ok(fill) = close_fill{ 265 | let trade_info = Self::get_trade_info( 266 | open, 267 | fill, 268 | &fees); 269 | 270 | 271 | let _ = sender.send(MarketCommand::ReceiveTrade(trade_info)).await; 272 | info!("Trade Closed: {:?}", trade_info); 273 | } 274 | 275 | } 276 | }); 277 | }; 278 | 279 | }, 280 | 281 | TradeCommand::OpenTrade{size, is_long}=> { 282 | info!("Open trade command received"); 283 | 284 | if !self.is_active().await && !self.is_paused{ 285 | let trade_fill = self.open_order(size, is_long).await; 286 | 287 | if let Ok(trade) = trade_fill{ 288 | info!("Trade Opened: {:?}", trade.clone()); 289 | *self.open_position.lock().await = Some(trade); 290 | }; 291 | }else if self.is_active().await{ 292 | info!("OpenTrade skipped: a trade is already active"); 293 | } 294 | 295 | 296 | }, 297 | 298 | TradeCommand::CloseTrade{size} => { 299 | if self.is_paused{continue}; 300 | let maybe_open = { 301 | let mut pos = self.open_position.lock().await; 302 | pos.take() 303 | }; 304 | 305 | if let Some(open_pos) = maybe_open{ 306 | let size = size.min(open_pos.sz); 307 | let trade_fill = self.close_order(size,open_pos.is_long).await; 308 | 309 | if let Ok(fill) = trade_fill{ 310 | let trade_info = Self::get_trade_info( 311 | open_pos, 312 | fill, 313 | &self.fees); 314 | let _ = info_sender.send(MarketCommand::ReceiveTrade(trade_info)).await; 315 | info!("Trade Closed: {:?}", trade_info); 316 | }; 317 | }; 318 | }, 319 | 320 | TradeCommand::CancelTrade => { 321 | 322 | if let Some(trade_info) = self.cancel_trade().await{ 323 | let _ = info_sender.send(MarketCommand::ReceiveTrade(trade_info)).await; 324 | }; 325 | 326 | return; 327 | 328 | }, 329 | 330 | TradeCommand::Liquidation(liq_fill) => { 331 | let maybe_open = { 332 | let mut pos = self.open_position.lock().await; 333 | pos.take() 334 | }; 335 | 336 | if let Some(open_pos) = maybe_open{ 337 | let liq_fill: TradeFillInfo = liq_fill.into(); 338 | println!("MAKE SURE SIZES ARE THE SAME: \nLocal {open_pos:?}\nLiquidation: {liq_fill:?}"); 339 | let trade_info = Self::get_trade_info( 340 | open_pos, 341 | liq_fill, 342 | &self.fees); 343 | 344 | let _ = info_sender.send(MarketCommand::ReceiveTrade(trade_info)).await; 345 | info!("LIQUIDATION INFO: {:?}", trade_info); 346 | } 347 | }, 348 | 349 | TradeCommand::Toggle=> { 350 | 351 | if let Some(trade_info) = self.cancel_trade().await{ 352 | let _ = info_sender.send(MarketCommand::ReceiveTrade(trade_info)).await; 353 | }; 354 | self.toggle_pause(); 355 | info!("Executor is now {}", if self.is_paused { "paused" } else { "resumed" }); 356 | }, 357 | 358 | TradeCommand::Pause => { 359 | if let Some(trade_info) = self.cancel_trade().await{ 360 | let _ = info_sender.send(MarketCommand::ReceiveTrade(trade_info)).await; 361 | }; 362 | self.is_paused = true; 363 | }, 364 | TradeCommand::Resume => { 365 | self.is_paused = false; 366 | }, 367 | 368 | TradeCommand::BuildPosition{size, is_long, interval} => {info!("Contacting Bob the builder")}, 369 | 370 | } 371 | 372 | }} 373 | 374 | 375 | } 376 | 377 | -------------------------------------------------------------------------------- /src/market.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | use std::collections::{HashMap, HashSet}; 3 | use std::sync::Arc; 4 | use log::info; 5 | use serde::Deserialize; 6 | 7 | 8 | use ethers::signers::LocalWallet; 9 | use hyperliquid_rust_sdk::{AssetMeta,Error, BaseUrl, ExchangeClient, InfoClient, Message}; 10 | 11 | use kwant::indicators::Price; 12 | 13 | use crate::{MAX_HISTORY, MarketInfo, MarketTradeInfo}; 14 | 15 | use crate::executor::Executor; 16 | use crate::signal::{SignalEngine, ExecParam, EngineCommand, TimeFrameData, Entry, EditType, IndexId}; 17 | use crate::trade_setup::{TimeFrame, TradeParams, TradeCommand, TradeInfo, LiquidationFillInfo}; 18 | use crate::strategy::Strategy; 19 | use crate::helper::load_candles; 20 | use crate::{IndicatorData,AssetMargin, UpdateFrontend}; 21 | 22 | use tokio::{ 23 | sync::mpsc::{channel, Sender, Receiver, UnboundedSender, UnboundedReceiver, unbounded_channel}, 24 | }; 25 | 26 | use flume::{bounded, Sender as FlumeSender}; 27 | 28 | 29 | 30 | 31 | 32 | 33 | pub struct Market { 34 | info_client: InfoClient, 35 | exchange_client: ExchangeClient, 36 | pub trade_history: Vec, 37 | pub pnl: f64, 38 | pub trade_params: TradeParams, 39 | pub asset: AssetMeta, 40 | signal_engine: SignalEngine, 41 | executor: Executor, 42 | receivers: MarketReceivers, 43 | senders: MarketSenders, 44 | pub active_tfs: HashSet, 45 | pub margin: f64, 46 | } 47 | 48 | 49 | 50 | impl Market{ 51 | 52 | pub async fn new( 53 | wallet: LocalWallet, 54 | url: BaseUrl, 55 | bot_tx: UnboundedSender, 56 | price_rv: UnboundedReceiver, 57 | asset: AssetMeta, 58 | margin: f64, 59 | fees: (f64, f64), 60 | trade_params: TradeParams, 61 | config: Option> 62 | ) -> Result<(Self, Sender), Error>{ 63 | 64 | let mut info_client = InfoClient::new(None, Some(url)).await?; 65 | let exchange_client = ExchangeClient::new(None, wallet.clone(), Some(url), None, None).await?; 66 | 67 | //Look up needed tfs for loading 68 | let mut active_tfs: HashSet = HashSet::new(); 69 | active_tfs.insert(trade_params.time_frame); 70 | if let Some(ref cfg) = config{ 71 | for ind_id in cfg{ 72 | active_tfs.insert(ind_id.1); 73 | } 74 | } 75 | 76 | info!("\n MARGIN: {}", margin); 77 | //setup channels 78 | let (market_tx, mut market_rv) = channel::(7); 79 | let (exec_tx, mut exec_rv) = bounded::(0); 80 | let (engine_tx, mut engine_rv) = unbounded_channel::(); 81 | 82 | let senders = MarketSenders{ 83 | bot_tx, 84 | engine_tx, 85 | exec_tx: exec_tx.clone(), 86 | }; 87 | 88 | let receivers = MarketReceivers{ 89 | price_rv, 90 | market_rv, 91 | }; 92 | 93 | Ok((Market{ 94 | info_client, 95 | exchange_client, 96 | margin, 97 | trade_history: Vec::with_capacity(MAX_HISTORY), 98 | pnl: 0_f64, 99 | trade_params : trade_params.clone(), 100 | asset: asset.clone(), 101 | signal_engine: SignalEngine::new(config, trade_params,engine_rv, Some(market_tx.clone()),exec_tx, margin).await, 102 | executor: Executor::new(wallet, asset.name, fees,exec_rv ,market_tx.clone()).await?, 103 | receivers, 104 | senders, 105 | active_tfs, 106 | }, market_tx, 107 | )) 108 | } 109 | 110 | async fn init(&mut self) -> Result<(), Error>{ 111 | 112 | //check if lev > max_lev 113 | let lev = self.trade_params.lev.min(self.asset.max_leverage); 114 | let upd = self.trade_params.update_lev(lev ,&self.exchange_client, self.asset.name.as_str(), true).await; 115 | if let Ok(lev) = upd{ 116 | let engine_tx = self.senders.engine_tx.clone(); 117 | let _ = engine_tx.send(EngineCommand::UpdateExecParams(ExecParam::Lev(lev))); 118 | }; 119 | 120 | self.load_engine(2000).await?; 121 | println!("\nMarket initialized for {} {:?}\n", self.asset.name, self.trade_params); 122 | Ok(()) 123 | } 124 | 125 | 126 | 127 | pub fn change_strategy(&mut self, strategy: Strategy){ 128 | 129 | self.trade_params.strategy = strategy; 130 | 131 | } 132 | 133 | async fn load_engine(&mut self, candle_count: u64) -> Result<(), Error>{ 134 | 135 | info!("---------Loading Engine: this may take some time----------------"); 136 | for tf in &self.active_tfs{ 137 | let price_data = load_candles(&self.info_client, 138 | self.asset.name.as_str(), 139 | *tf, 140 | candle_count).await?; 141 | self.signal_engine.load(*tf, price_data).await; 142 | } 143 | 144 | Ok(()) 145 | 146 | } 147 | 148 | pub fn get_trade_history(&self) -> &Vec{ 149 | 150 | &self.trade_history 151 | } 152 | 153 | } 154 | 155 | 156 | impl Market{ 157 | 158 | pub async fn start(mut self) -> Result<(), Error>{ 159 | self.init().await?; 160 | 161 | let info = MarketInfo{ 162 | asset: self.asset.name.clone(), 163 | lev: self.trade_params.lev, 164 | price: 0.0, 165 | params: self.trade_params.clone(), 166 | margin: self.margin, 167 | pnl: 0.0, 168 | is_paused: false, 169 | indicators: self.signal_engine.get_indicators_data(), 170 | }; 171 | let _ = self.senders.bot_tx.send(MarketUpdate::InitMarket(info)); 172 | 173 | let mut signal_engine = self.signal_engine; 174 | let executor = self.executor; 175 | 176 | //Start engine 177 | let engine_handle = tokio::spawn(async move { 178 | signal_engine.start().await; 179 | }); 180 | //Start exucutor 181 | let executor_handle = tokio::spawn(async move { 182 | executor.start().await; 183 | }); 184 | //Candle Stream 185 | let engine_price_tx = self.senders.engine_tx.clone(); 186 | let bot_price_update = self.senders.bot_tx.clone(); 187 | 188 | let asset_name: Arc = Arc::from(self.asset.name.clone()); 189 | let candle_stream_handle = tokio::spawn(async move { 190 | let mut curr = f64::from_bits(1); 191 | while let Some(Message::Candle(candle)) = self.receivers.price_rv.recv().await{ 192 | let close = candle.data.close.parse::().ok().unwrap(); 193 | let high = candle.data.high.parse::().ok().unwrap(); 194 | let low = candle.data.low.parse::().ok().unwrap(); 195 | let open = candle.data.open.parse::().ok().unwrap(); 196 | let price = Price{open,high, low, close}; 197 | 198 | let _ = engine_price_tx.send(EngineCommand::UpdatePrice(price)); 199 | if close != curr { 200 | let _ = bot_price_update.send(MarketUpdate::PriceUpdate((asset_name.clone().to_string(), close))); 201 | curr = close; 202 | }; 203 | } 204 | }); 205 | 206 | //listen to changes and trade results 207 | let engine_update_tx = self.senders.engine_tx.clone(); 208 | let bot_update_tx = self.senders.bot_tx; 209 | let asset = self.asset.clone(); 210 | 211 | 212 | 213 | while let Some(cmd) = self.receivers.market_rv.recv().await{ 214 | match cmd { 215 | MarketCommand::UpdateLeverage(lev)=>{ 216 | let lev = lev.min(asset.max_leverage); 217 | let upd = self.trade_params.update_lev(lev ,&self.exchange_client, asset.name.as_str(), false).await; 218 | if let Ok(lev) = upd{ 219 | let _ = engine_update_tx.send(EngineCommand::UpdateExecParams(ExecParam::Lev(lev))); 220 | }; 221 | }, 222 | 223 | MarketCommand::UpdateStrategy(strat)=>{ 224 | let _ = engine_update_tx.send(EngineCommand::UpdateStrategy(strat)); 225 | }, 226 | 227 | MarketCommand::EditIndicators(entry_vec)=>{ 228 | let mut map: TimeFrameData = HashMap::new(); 229 | for &entry in &entry_vec{ 230 | if entry.edit == EditType::Add && !self.active_tfs.contains(&entry.id.1){ 231 | let tf_data = load_candles(&self.info_client, 232 | asset.name.as_str(), 233 | entry.id.1, 234 | 3000).await?; 235 | map.insert(entry.id.1, tf_data); 236 | self.active_tfs.insert(entry.id.1); 237 | } 238 | }; 239 | 240 | let price_data = if map.is_empty() {None} else {Some(map)}; 241 | let _ = engine_update_tx.send(EngineCommand::EditIndicators{indicators: entry_vec, 242 | price_data, 243 | }); 244 | }, 245 | 246 | MarketCommand::ReceiveTrade(trade_info) =>{ 247 | self.pnl += trade_info.pnl; 248 | self.margin += trade_info.pnl; 249 | self.trade_history.push(trade_info); 250 | let _ = engine_update_tx.send(EngineCommand::UpdateExecParams(ExecParam::Margin(self.margin))); 251 | let _ = bot_update_tx.send(MarketUpdate::TradeUpdate( 252 | MarketTradeInfo{ 253 | asset: asset.name.clone(), 254 | info: trade_info, 255 | } 256 | )); 257 | let _ = bot_update_tx.send(MarketUpdate::MarginUpdate((asset.name.clone(), self.margin))); 258 | }, 259 | 260 | MarketCommand::ReceiveLiquidation(liq_fill) => { 261 | self.senders.exec_tx.send_async(TradeCommand::Liquidation(liq_fill)).await; 262 | }, 263 | 264 | MarketCommand::UpdateTimeFrame(tf)=>{ 265 | self.trade_params.time_frame = tf; 266 | let _ = engine_update_tx.send(EngineCommand::UpdateExecParams(ExecParam::Tf(tf))); 267 | }, 268 | 269 | MarketCommand::UpdateMargin(marge) => { 270 | self.margin = marge; 271 | let _ = engine_update_tx.send(EngineCommand::UpdateExecParams(ExecParam::Margin(self.margin))); 272 | let _ = bot_update_tx.send(MarketUpdate::MarginUpdate((asset.name.to_string(), self.margin))); 273 | }, 274 | 275 | MarketCommand::UpdateIndicatorData(data) =>{ 276 | let _ = bot_update_tx.send( 277 | MarketUpdate::RelayToFrontend( 278 | UpdateFrontend::UpdateIndicatorValues{ 279 | asset: asset.name.to_string(), 280 | data, 281 | }) 282 | ); 283 | }, 284 | 285 | MarketCommand::Toggle =>{ 286 | let _ = self.senders.exec_tx.send_async(TradeCommand::Toggle).await; 287 | }, 288 | 289 | MarketCommand::Pause =>{ 290 | let _ = self.senders.exec_tx.send_async(TradeCommand::Pause).await; 291 | }, 292 | 293 | MarketCommand::Resume => { 294 | let _ = self.senders.exec_tx.send_async(TradeCommand::Resume).await; 295 | }, 296 | 297 | 298 | MarketCommand::Close=>{ 299 | info!("\nClosing {} Market...\n", asset.name); 300 | let _ = engine_update_tx.send(EngineCommand::Stop); 301 | //shutdown Executor 302 | info!("\nShutting down executor\n"); 303 | match self.senders.exec_tx.send(TradeCommand::CancelTrade) { 304 | Ok(_) =>{ 305 | if let Some(cmd) = self.receivers.market_rv.recv().await { 306 | match cmd { 307 | MarketCommand::ReceiveTrade(trade_info) => { 308 | info!("\nReceived final trade before shutdown: {:?}\n", trade_info); 309 | self.pnl += trade_info.pnl; 310 | self.margin += trade_info.pnl; 311 | self.trade_history.push(trade_info); 312 | let _ = bot_update_tx.send(MarketUpdate::TradeUpdate( 313 | MarketTradeInfo{ 314 | asset: asset.name.to_string(), 315 | info: trade_info} 316 | )); 317 | break; 318 | }, 319 | 320 | _ => break, 321 | 322 | }} 323 | }, 324 | 325 | _ => { 326 | log::warn!("Cancel message not sent"); 327 | }, 328 | 329 | } 330 | break; 331 | }, 332 | }; 333 | 334 | }; 335 | 336 | let _ = engine_handle.await; 337 | let _ = executor_handle.await; 338 | let _ = candle_stream_handle.await; 339 | info!("No. of trade : {}\nPNL: {}",&self.trade_history.len(),&self.pnl); 340 | Ok(()) 341 | } 342 | } 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | #[derive(Debug, Clone, Deserialize)] 351 | #[serde(rename_all = "camelCase")] 352 | pub enum MarketCommand{ 353 | UpdateLeverage(u32), 354 | UpdateStrategy(Strategy), 355 | EditIndicators(Vec), 356 | UpdateTimeFrame(TimeFrame), 357 | ReceiveTrade(TradeInfo), 358 | ReceiveLiquidation(LiquidationFillInfo), 359 | UpdateMargin(f64), 360 | UpdateIndicatorData(Vec), 361 | Toggle, 362 | Resume, 363 | Pause, 364 | Close, 365 | } 366 | 367 | 368 | struct MarketSenders{ 369 | bot_tx: UnboundedSender, 370 | engine_tx: UnboundedSender, 371 | exec_tx: FlumeSender, 372 | } 373 | 374 | 375 | struct MarketReceivers { 376 | pub price_rv: UnboundedReceiver, 377 | pub market_rv: Receiver, 378 | } 379 | 380 | 381 | 382 | #[derive(Debug, Clone)] 383 | pub enum MarketUpdate{ 384 | InitMarket(MarketInfo), 385 | PriceUpdate(AssetPrice), 386 | TradeUpdate(MarketTradeInfo), 387 | MarginUpdate(AssetMargin), 388 | RelayToFrontend(UpdateFrontend), 389 | } 390 | 391 | 392 | pub type AssetPrice = (String, f64); 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /src/bot.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use tokio::time::{sleep, interval, Duration}; 3 | use std::collections::HashMap; 4 | use hyperliquid_rust_sdk::{Error, InfoClient, Message,Subscription, TradeInfo as HLTradeInfo}; 5 | use crate::{Market, 6 | MarketCommand, 7 | MarketUpdate,AssetPrice, 8 | MARKETS, 9 | TradeParams,TradeInfo, 10 | Wallet, IndexId, LiquidationFillInfo, 11 | UpdateFrontend, AddMarketInfo, MarketInfo, 12 | }; 13 | 14 | use crate::helper::{get_asset, subscribe_candles}; 15 | use tokio::{ 16 | sync::mpsc::{Sender, UnboundedSender, UnboundedReceiver, unbounded_channel}, 17 | }; 18 | 19 | use crate::margin::{MarginAllocation, MarginBook, AssetMargin}; 20 | use crate::helper::address; 21 | use std::sync::Arc; 22 | use tokio::sync::Mutex; 23 | 24 | use std::hash::BuildHasherDefault; 25 | use rustc_hash::FxHasher; 26 | use serde::{Deserialize, Serialize}; 27 | 28 | 29 | pub struct Bot{ 30 | info_client: InfoClient, 31 | wallet: Arc, 32 | markets: HashMap, BuildHasherDefault>, 33 | candle_subs: HashMap, 34 | session: Arc>>>, 35 | fees: (f64, f64), 36 | bot_tx: UnboundedSender, 37 | bot_rv: UnboundedReceiver, 38 | update_rv: Option>, 39 | update_tx: UnboundedSender, 40 | app_tx: Option>, 41 | } 42 | 43 | 44 | 45 | impl Bot{ 46 | pub async fn new(wallet: Wallet) -> Result<(Self, UnboundedSender), Error>{ 47 | 48 | let mut info_client = InfoClient::with_reconnect(None, Some(wallet.url)).await?; 49 | let fees = wallet.get_user_fees().await?; 50 | 51 | let (bot_tx, mut bot_rv) = unbounded_channel::(); 52 | let (update_tx, mut update_rv) = unbounded_channel::(); 53 | 54 | Ok((Self{ 55 | info_client, 56 | wallet: wallet.into(), 57 | markets: HashMap::default(), 58 | candle_subs: HashMap::new(), 59 | session: Arc::new(Mutex::new(HashMap::default())), 60 | fees, 61 | bot_tx: bot_tx.clone(), 62 | bot_rv, 63 | update_rv: Some(update_rv), 64 | update_tx, 65 | app_tx: None, 66 | }, bot_tx)) 67 | } 68 | 69 | 70 | pub async fn add_market(&mut self, info: AddMarketInfo, margin_book: &Arc>) -> Result<(), Error>{ 71 | 72 | let AddMarketInfo { 73 | asset, 74 | margin_alloc, 75 | trade_params, 76 | config, 77 | } = info; 78 | let asset = asset.trim().to_uppercase(); 79 | let asset_str = asset.as_str(); 80 | 81 | if self.markets.contains_key(&asset){ 82 | return Ok(()); 83 | } 84 | 85 | if !MARKETS.contains(&asset_str){ 86 | 87 | return Err(Error::AssetNotFound); 88 | } 89 | 90 | let mut book = margin_book.lock().await; 91 | let margin = book.allocate(asset.clone(), margin_alloc).await?; 92 | 93 | let meta = get_asset(&self.info_client, asset_str).await?; 94 | let (sub_id, mut receiver) = subscribe_candles(&mut self.info_client, 95 | asset_str, 96 | trade_params.time_frame.as_str()) 97 | .await?; 98 | 99 | 100 | let (market, market_tx) = Market::new( 101 | self.wallet.wallet.clone(), 102 | self.wallet.url, 103 | self.update_tx.clone(), 104 | receiver, 105 | meta, 106 | margin, 107 | self.fees, 108 | trade_params, 109 | config, 110 | ).await?; 111 | 112 | 113 | self.markets.insert(asset.clone(), market_tx); 114 | self.candle_subs.insert(asset.clone(), sub_id); 115 | let cancel_margin = margin_book.clone(); 116 | let app_tx = self.app_tx.clone(); 117 | 118 | tokio::spawn(async move { 119 | if let Err(e) = market.start().await { 120 | if let Some(tx) = app_tx{ 121 | tx.send(UpdateFrontend::UserError(format!("Market {} exited with error: {:?}", &asset, e))); 122 | } 123 | let mut book = cancel_margin.lock().await; 124 | book.remove(&asset); 125 | } 126 | }); 127 | 128 | 129 | 130 | Ok(()) 131 | 132 | } 133 | 134 | 135 | pub async fn remove_market(&mut self, asset: &String, margin_book: &Arc>) -> Result<(), Error>{ 136 | let asset = asset.trim().to_uppercase(); 137 | 138 | if let Some(sub_id) = self.candle_subs.remove(&asset){ 139 | let _ = self.info_client.unsubscribe(sub_id).await?; 140 | info!("Removed {} market successfully", asset); 141 | }else{ 142 | info!("Couldn't remove {} market, it doesn't exist", asset); 143 | return Ok(()); 144 | } 145 | 146 | if let Some(tx) = self.markets.remove(&asset){ 147 | let tx = tx.clone(); 148 | let cmd = MarketCommand::Close; 149 | let close = tokio::spawn(async move { 150 | if let Err(e) = tx.send(cmd).await{ 151 | log::warn!("Failed to send Close command: {:?}", e); 152 | return false; 153 | } 154 | true 155 | }).await.unwrap(); 156 | 157 | if close{ 158 | let mut sess_guard = self.session.lock().await; 159 | let _ = sess_guard.remove(&asset); 160 | let mut book = margin_book.lock().await; 161 | book.remove(&asset); 162 | } 163 | }else{ 164 | info!("Failed: Close {} market, it doesn't exist", asset); 165 | } 166 | 167 | 168 | Ok(()) 169 | } 170 | 171 | pub async fn pause_or_resume_market(&self, asset: &String){ 172 | let asset = asset.trim().to_uppercase(); 173 | 174 | if let Some(tx) = self.markets.get(&asset){ 175 | let tx = tx.clone(); 176 | let cmd = MarketCommand::Toggle; 177 | tokio::spawn(async move{ 178 | if let Err(e) = tx.send(cmd).await{ 179 | log::warn!("Failed to send Toggle command: {:?}", e); 180 | } 181 | }); 182 | 183 | }else{ 184 | info!("Failed: Pause {} market, it doesn't exist", asset); 185 | return; 186 | } 187 | 188 | let mut sess_guard = self.session.lock().await; 189 | if let Some(info) = sess_guard.get_mut(&asset){ 190 | info.is_paused = !info.is_paused; 191 | } 192 | } 193 | 194 | pub async fn pause_all(&self){ 195 | 196 | info!("PAUSING ALL MARKETS"); 197 | for (_asset, tx) in &self.markets{ 198 | let _ = tx.send(MarketCommand::Pause).await; 199 | } 200 | 201 | let mut session = self.session.lock().await; 202 | for (_asset, info) in session.iter_mut(){ 203 | info.is_paused = true; 204 | } 205 | 206 | } 207 | pub async fn resume_all(&self){ 208 | info!("RESUMING ALL MARKETS"); 209 | for (_asset, tx) in &self.markets{ 210 | let _ = tx.send(MarketCommand::Resume).await; 211 | } 212 | } 213 | pub async fn close_all(&mut self){ 214 | info!("CLOSING ALL MARKETS"); 215 | for (_asset, id) in self.candle_subs.drain(){ 216 | self.info_client.unsubscribe(id).await; 217 | } 218 | self.candle_subs.clear(); 219 | for (_asset, tx) in self.markets.drain(){ 220 | let _ = tx.send(MarketCommand::Close).await; 221 | } 222 | 223 | let mut session = self.session.lock().await; 224 | session.clear(); 225 | } 226 | 227 | 228 | pub async fn send_cmd(&self, asset: &String, cmd: MarketCommand){ 229 | let asset = asset.trim().to_uppercase(); 230 | 231 | if let Some(tx) = self.markets.get(&asset){ 232 | let tx = tx.clone(); 233 | tokio::spawn(async move{ 234 | if let Err(e) = tx.send(cmd).await{ 235 | log::warn!("Failed to send Market command: {:?}", e); 236 | } 237 | }); 238 | } 239 | } 240 | 241 | pub fn get_markets(&self) -> Vec<&String>{ 242 | let mut assets = Vec::new(); 243 | for (asset, _tx) in &self.markets{ 244 | assets.push(asset); 245 | } 246 | 247 | assets 248 | } 249 | 250 | pub async fn get_session(&self) -> Vec{ 251 | 252 | let mut guard = self.session.lock().await; 253 | let session: Vec = guard.values().cloned().collect(); 254 | 255 | session 256 | 257 | } 258 | 259 | 260 | pub async fn start(mut self, app_tx: UnboundedSender) -> Result<(), Error>{ 261 | use BotEvent::*; 262 | use MarketUpdate::*; 263 | use UpdateFrontend::*; 264 | 265 | self.app_tx = Some(app_tx.clone()); 266 | 267 | 268 | //safe 269 | let mut update_rv = self.update_rv.take().unwrap(); 270 | 271 | 272 | let user = self.wallet.clone(); 273 | let mut margin_book= MarginBook::new(user); 274 | let margin_arc = Arc::new(Mutex::new(margin_book)); 275 | let margin_sync = margin_arc.clone(); 276 | let margin_user_edit = margin_arc.clone(); 277 | let margin_market_edit = margin_arc.clone(); 278 | 279 | let app_tx_margin = app_tx.clone(); 280 | let err_tx = app_tx.clone(); 281 | 282 | //keep marginbook in sync with DEX 283 | tokio::spawn(async move{ 284 | let mut ticker = interval(Duration::from_secs(2)); 285 | loop{ 286 | ticker.tick().await; 287 | let result = { 288 | let mut book = margin_sync.lock().await; 289 | book.sync().await 290 | }; 291 | 292 | match result { 293 | Ok(_) => { 294 | let total = { 295 | let book = margin_sync.lock().await; 296 | book.total_on_chain - book.used() 297 | }; 298 | let _ = app_tx_margin.send(UpdateTotalMargin(total)); 299 | } 300 | Err(e) => { 301 | log::warn!("Failed to fetch User Margin"); 302 | let _ = app_tx_margin.send(UserError(e.to_string())); 303 | continue; 304 | } 305 | } 306 | let _ = sleep(Duration::from_millis(500)).await; 307 | } 308 | }); 309 | 310 | 311 | //Market -> Bot 312 | let session_adder = self.session.clone(); 313 | tokio::spawn(async move{ 314 | while let Some(market_update) = update_rv.recv().await{ 315 | 316 | match market_update{ 317 | InitMarket(info) => { 318 | let mut session_guard = session_adder.lock().await; 319 | session_guard.insert(info.asset.clone(), info.clone()); 320 | let _ = app_tx.send(ConfirmMarket(info)); 321 | }, 322 | PriceUpdate(asset_price) => {let _ = app_tx.send(UpdatePrice(asset_price));}, 323 | TradeUpdate(trade_info) => { 324 | let _ = app_tx.send(NewTradeInfo(trade_info)); 325 | 326 | }, 327 | MarginUpdate(asset_margin) => { 328 | let result = { 329 | let mut book = margin_market_edit.lock().await; 330 | book.update_asset(asset_margin.clone()).await 331 | }; 332 | 333 | match result { 334 | Ok(_) => { 335 | let _ = app_tx.send(UpdateMarketMargin(asset_margin)); 336 | } 337 | Err(e) => { 338 | let _ = app_tx.send(UserError(e.to_string())); 339 | } 340 | } 341 | }, 342 | RelayToFrontend(cmd) => {let _ = app_tx.send(cmd); 343 | }, 344 | } 345 | } 346 | }); 347 | 348 | //listen and send Liquidation events 349 | let (liq_tx, mut liq_rv) = unbounded_channel(); 350 | let _id = self.info_client 351 | .subscribe(Subscription::UserFills{user: address(&self.wallet.pubkey) }, liq_tx) 352 | .await?; 353 | 354 | loop{ 355 | tokio::select!( 356 | biased; 357 | 358 | Some(Message::UserFills(update)) = liq_rv.recv() => { 359 | 360 | if update.data.is_snapshot.is_some(){ 361 | continue; 362 | } 363 | let mut liq_map: HashMap> = HashMap::new(); 364 | 365 | for trade in update.data.fills.into_iter(){ 366 | if trade.liquidation.is_some(){ 367 | liq_map 368 | .entry(trade.coin.clone()) 369 | .or_insert_with(Vec::new) 370 | .push(trade); 371 | } 372 | } 373 | println!("\nTRADES |||||||||| {:?}\n\n", liq_map); 374 | 375 | for (coin, fills) in liq_map.into_iter(){ 376 | let to_send = LiquidationFillInfo::from(fills); 377 | let cmd = MarketCommand::ReceiveLiquidation(to_send); 378 | self.send_cmd(&coin, cmd).await; 379 | } 380 | }, 381 | 382 | 383 | Some(event) = self.bot_rv.recv() => { 384 | 385 | match event{ 386 | AddMarket(add_market_info) => { 387 | if let Err(e) = self.add_market(add_market_info, &margin_user_edit).await{ 388 | let _ = err_tx.send(UserError(e.to_string())); 389 | } 390 | }, 391 | ToggleMarket(asset) => {self.pause_or_resume_market(&asset).await;}, 392 | RemoveMarket(asset) => {let _ = self.remove_market(&asset, &margin_user_edit).await;}, 393 | MarketComm(command) => {self.send_cmd(&command.asset, command.cmd).await;}, 394 | ManualUpdateMargin(asset_margin) => { 395 | let result = { 396 | let mut book = margin_user_edit.lock().await; 397 | book.update_asset(asset_margin.clone()).await 398 | }; 399 | if let Ok(new_margin) = result{ 400 | let cmd = MarketCommand::UpdateMargin(new_margin); 401 | self.send_cmd(&asset_margin.0.to_string(), cmd).await; 402 | } 403 | }, 404 | ResumeAll =>{self.resume_all().await}, 405 | PauseAll => {self.pause_all().await;}, 406 | CloseAll => { 407 | self.close_all().await; 408 | let mut book = margin_user_edit.lock().await; 409 | book.reset(); 410 | }, 411 | 412 | GetSession =>{ 413 | let session = self.get_session().await; 414 | let _ = err_tx.send(LoadSession(session)); 415 | }, 416 | } 417 | }, 418 | 419 | 420 | )} 421 | 422 | } 423 | 424 | } 425 | 426 | 427 | 428 | 429 | #[derive(Clone, Debug, Deserialize)] 430 | #[serde(rename_all = "camelCase")] 431 | pub enum BotEvent{ 432 | AddMarket(AddMarketInfo), 433 | ToggleMarket(String), 434 | RemoveMarket(String), 435 | MarketComm(BotToMarket), 436 | ManualUpdateMargin(AssetMargin), 437 | ResumeAll, 438 | PauseAll, 439 | CloseAll, 440 | GetSession, 441 | } 442 | 443 | 444 | 445 | #[derive(Clone, Debug, Deserialize)] 446 | #[serde(rename_all = "camelCase")] 447 | pub struct BotToMarket{ 448 | pub asset: String, 449 | pub cmd: MarketCommand, 450 | } 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | --------------------------------------------------------------------------------