├── 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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------