├── .deepsource.toml ├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql.yml │ ├── docker-image.yml │ └── python-publish.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── doc ├── Create_strategy.png ├── Model of logarithmic grid.ods ├── Modified martingale.svg ├── graf1.png ├── tlg_notify.png └── tmux.png ├── martin_binance ├── __init__.py ├── backtest │ ├── OoTSP.py │ ├── VCoSEL.py │ ├── __init__.py │ ├── exchange_simulator.py │ └── optimizer.py ├── client.py ├── db_utils.py ├── executor.py ├── lib.py ├── params.py ├── service │ ├── .tmux.conf │ ├── funds_export.service │ ├── funds_rate_exporter.py │ ├── grafana.json │ ├── relaunch.py │ ├── relaunch.service │ └── tlg-proxy.service ├── strategy_base.py ├── telegram_proxy │ ├── tlg_client.py │ ├── tlg_proxy.proto │ └── tlg_proxy.py ├── templates │ ├── cli_0_BTCUSDT.py │ ├── cli_1_BTCUSDT.py │ ├── cli_2_TESTBTCTESTUSDT.py │ ├── cli_3_BTCUSDT.py │ ├── funds_rate.db │ ├── ms_cfg.toml │ └── trial_params.json └── tlg │ └── __init__.py ├── pyproject.toml ├── requirements.txt └── uml ├── architecture-0.png └── architecture.puml /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | exclude_patterns = [ 4 | ] 5 | 6 | [[analyzers]] 7 | name = "python" 8 | enabled = true 9 | 10 | [analyzers.meta] 11 | runtime_version = "3.x.x" 12 | max_line_length = 120 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | martin_binance/margin/ 3 | martin_binance/service/ 4 | # **/*.template 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://github.com/DogsTailFarmer/martin-binance#donate"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Environment (please complete the following information):** 11 | - Version [e.g. 1.2.6] 12 | - OS: [e.g. Ubuntu 22.04] 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Basic trade parameters: 19 | - exchange: 20 | - pair: 21 | - other: 22 | 23 | Steps to reproduce the behavior: 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Screenshots** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "public" ] 6 | pull_request: 7 | branches: [ "public" ] 8 | schedule: 9 | - cron: "40 8 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: PrepareReg Names 16 | run: | 17 | echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 18 | echo IMAGE_TAG=$(echo ${{ github.ref }} | tr '[:upper:]' '[:lower:]' | awk '{split($0,a,"/"); print a[3]}') >> $GITHUB_ENV 19 | 20 | - name: Login to GHCR 21 | uses: docker/login-action@v2 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.repository_owner }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Build and push 28 | run: | 29 | docker build . --tag ghcr.io/$IMAGE_REPOSITORY:$IMAGE_TAG 30 | docker push ghcr.io/$IMAGE_REPOSITORY:$IMAGE_TAG 31 | docker build . --tag ghcr.io/$IMAGE_REPOSITORY:latest 32 | docker push ghcr.io/$IMAGE_REPOSITORY:latest 33 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | workflow_dispatch: 8 | release: 9 | types: [published] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | deploy: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: '3.x' 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install build 29 | - name: Build package 30 | run: python -m build 31 | - name: Publish package 32 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 33 | with: 34 | user: __token__ 35 | password: ${{ secrets.PYPI_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.32 - 2025-05-30 2 | 🔧 fix(executor): adjust conditions for currency deposits with GRID_ONLY logic 3 | 4 | ## 3.0.31 - 2025-05-27 5 | 🔧 fix(executor): correct handling of `delta` in conditional for `initial_first` and `initial_second` updates 6 | 7 | ## 3.0.30 - 2025-05-25 8 | ✨ feat(executor): refactor `grid_update` method and simplify TP order handling logic for clarity and efficiency. 9 | 🔧 fix(executor): ensure grid orders placement when no open grid orders exist. 10 | 11 | ## 3.0.29 - 2025-05-25 12 | ✨ refactor(lib.py): simplify `convert_from_minute` using intervals for improved readability and maintainability 13 | 🔧 fix(executor.py): change type check to `isinstance` for `self.start_collect` in `event_exec_command` 14 | 🔧 fix(executor.py): streamline condition checks in `on_new_ticker` for better clarity and performance 15 | 16 | ## 3.0.28 - 2025-05-12 17 | 🔧 chore(dependencies): update `exchanges-wrapper` to version 2.1.37 in `pyproject.toml` and `requirements.txt` 18 | 🔧 fix(strategy): simplify `create_order_handler` method in `strategy_base.py` 19 | 20 | ## 3.0.27 - 2025-04-25 21 | 🔧 chore(dependencies): bump `exchanges-wrapper` version to 2.1.36 in `pyproject.toml` and `requirements.txt` 22 | 🔧 refactor(lib): update function return types to `dict` and `list` in `martin_binance/lib.py` 23 | 🔧 refactor(strategy_base): update function return types to `list` in `martin_binance/strategy_base.py` 24 | 25 | ## 3.0.26 - 2025-04-24 26 | 🔒 fix(tlg_client, tlg_proxy): set minimum SSL/TLS version to 1.2 for enhanced security 27 | 🔧 chore(init): bump version to 3.0.26 in `__init__.py` 28 | 29 | ## 3.0.25 - 2025-04-24 30 | 🔧 fix(pyproject.toml, requirements.txt): update `exchanges-wrapper` version to 2.1.35 for compatibility 31 | 📝 docs(README.md): update configuration instructions for `exchanges-wrapper` to version 3.0.25 32 | 🔄 chore(README.md): change `HUOBI` to `HTX` and `OKEX` to `OKX` in referral links 33 | 34 | ### Update 35 | ✨ [UPDATE](pyproject.toml/CHANGELOG.md): Bump dependencies and version. 36 | 37 | ## 3.0.23 - 2025-03-10 38 | ### Fix 39 | * `restore_strategy_state`: Logical error when processing orders that were sent before shutdown and no response 40 | was received 41 | * `on_balance_update_ex`: doubling the adjustment of the initial balance with a positive replenishment in the opposite 42 | coin 43 | 44 | ### Update 45 | * Added `trade_id` to debug output 46 | * Bump requirements 47 | * `grafana.json` template 48 | * Some minor improvements 49 | 50 | ## 3.0.22 - 2025-02-20 51 | ### Update 52 | * Bump requirements 53 | 54 | ## 3.0.21 - 2025-02-07 55 | ### Update 56 | * Some minor improvements 57 | 58 | ## 3.0.20 - 2025-02-06 59 | ### Update 60 | * `Bybit`: checking for the minimum/maximum Buy/Sell orders price is [excluded](https://announcements.bybit.com/article/title-adjustments-to-bybit-s-spot-trading-limit-order-mechanism-blt786c0c5abf865983/) 61 | * Bump requirements 62 | * Some minor improvements 63 | 64 | ## 3.0.19 - 2025-01-27 65 | ### Fix 66 | * `on_balance_update_ex`: calculating initial balance for opposite coin in Reverse cycle 67 | 68 | ### Update 69 | * `funds_rate_exporter.py`: optimized current price queries on `coinmarketcap` 70 | * Functions for placing and canceling orders have been optimized 71 | * Bump requirements 72 | 73 | ## 3.0.18 - 2025-01-18 74 | ### Update 75 | * Change dependency model for exchanges-wrapper 76 | * Up requirements for exchanges-wrapper>=2.1.26 77 | 78 | ## 3.0.17 - 2025-01-16 79 | ### Fix 80 | * Restored reverse_price value if it is omitted 81 | * `GRID_ONLY` mode: incorrect check of minimum trading volume 82 | * Erroneous correction of the opposite coin when withdrawing in a reverse cycle is excluded 83 | * Restarting a session in case of network failures when sharing a client between pairs 84 | * Interaction with the Telegram service, consolidation of messages and distribution of commands through 85 | a centralized proxy service, default use tlg_proxy service disabled 86 | 87 | ### Added new features 88 | * `funds_rate_exporter.py`: add export data to IPv6 interface 89 | 90 | ### Update 91 | * Bump requirements 92 | * Moving control and updating MAX_QUEUE_SIZE for on_*_update loops to server side 93 | 94 | ## 3.0.16 - 2024-11-01 95 | ### Fix 96 | * Unnecessary restart when updating assets 97 | * Add parameter `SAVE_ASSET`, to selectively suppress this [feature](https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#save-data-for-external-analytics) 98 | 99 | ## 3.0.15 - 2024-09-14 100 | ### Update 101 | * Up requirements for exchanges-wrapper==2.1.20 102 | * Add `betterproto` to requirements 103 | 104 | ## 3.0.14 - 2024-09-13 105 | ### Fix 106 | * `pyproject.toml` 107 | 108 | ## 3.0.13post1 - 2024-09-13 109 | ### Fix 110 | * deepsource found style issue 111 | 112 | ## 3.0.13 - 2024-09-13 113 | ### Fix 114 | * Sometimes get error from backtesting on Exit [#81](https://github.com/DogsTailFarmer/martin-binance/issues/81#issue-2465248437) 115 | 116 | ### Added new features 117 | * (3.0.13b4) Handling missed asset update events of GRID_ONLY and USE_ALL_FUND modes 118 | 119 | ### Update 120 | * Up requirements for exchanges-wrapper==2.1.18 121 | * Dependency 122 | * Some minor improvement 123 | 124 | ## 3.0.12 - 2024-08-13 125 | ### Fix 126 | * Calculating`self.reverse_price` before immediately starting Reverse cycle also. It can be used later in 127 | `on_balance_update_ex` 128 | * `on_balance_update_ex`: initial balance update logic 129 | 130 | ## 3.0.11 - 2024-07-13 131 | ### Added new features 132 | * [Maintaining a supply of BNB in sub-account to pay fees](https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#maintaining-a-supply-of-bnb-in-a-sub-account-to-pay-fees) 133 | 134 | ## 3.0.10 - 2024-07-07 135 | ### Fix 136 | * `on_balance_update_ex`: excessive recalculation of income when withdrawing funds 137 | 138 | ### Update 139 | * Processing account replenishment in all states with increasing turnover 140 | * Dependency 141 | 142 | ## 3.0.9 - 2024-07-07 143 | ### Added new features 144 | * Updated `APY profit rating` Grafana report, used data from selected time range for APY calculation 145 | * Processing [account replenishment](https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#deposit-and-withdraw-assets-on-active-strategy) in all states with increasing turnover 146 | 147 | ## 3.0.8 - 2024-06-26 148 | ### Fix 149 | * Version compatibility 150 | 151 | ### Added new features 152 | * Continue interrupted grid update cycle after restart 153 | 154 | ### Update 155 | * Dependency 156 | 157 | ## 3.0.7 - 2024-05-02 158 | ### Fix 159 | * `Docker`: server-client deployment functionality restored (`exchanges-wrapper` and `martin-binance`), also for Windows 160 | 161 | ### Update 162 | * `RATE_LIMITER` initial settings changed 163 | * Up requirements for exchanges-wrapper==2.1.12 164 | 165 | ## 3.0.6 - 2024-04-19 166 | ### Fix 167 | * Unattended cancelling keepalive tasks 168 | 169 | ### Added new features 170 | * The total income includes withdrawn funds 171 | 172 | ### Update 173 | * Up requirements for exchanges-wrapper==2.1.11 174 | * Refine handling backtest process 175 | 176 | ## 3.0.5 - 2024-04-16 177 | ### Update 178 | * Up requirements for exchanges-wrapper==2.1.10 179 | * Import from `params` module 180 | * Some issues `Mismatch between signature and use of an overridden method` 181 | 182 | ## 3.0.4 - 2024-04-14 183 | ### Fix 184 | * Creating and manage asynchronous tasks 185 | 186 | ### Update 187 | * Up requirements for exchanges-wrapper==2.1.9 188 | * 'Backtesting': some minor improvements 189 | * Remove cross-version updates 190 | 191 | ## 3.0.3 - 2024-04-08 192 | ### Update 193 | * Refine templates handling 194 | * `Backtesting`: extract trial parameters into `/home/ubuntu/.MartinBinance/trial_params.json`, [detail here.](https://github.com/DogsTailFarmer/martin-binance/wiki/Back-testing-and-parameters-optimization#some-details) 195 | * `Backtesting`: add logging importance parameters rating for each optimisation cycle 196 | * Up requirements for exchanges-wrapper==2.1.8 197 | 198 | ## 3.0.2 - 2024-04-03 199 | ### Fix 200 | * `Backtesting`: updating the balances at slippage 201 | * `class PrivateTrade:` convert trade_id to int() 202 | * `Backtest control`: orjson.JSONDecodeError: unexpected character: line 1 column 1 (char 0) 203 | 204 | ### Update 205 | * Up requirements for exchanges-wrapper==2.1.7 206 | * Some minor improvement 207 | 208 | ## 3.0.1 - 2024-03-31 209 | ### Refined and added new features 210 | * Project architecture 211 | * :rocket: Cyclic Backtesting workflow with update base trade parameters 212 | * :rocket: `Backtesting`: handling of partially filling events 213 | * :rocket: Migrate `gRPC` from [grpcio](https://grpc.io/) to [grpclib](https://github.com/vmagamedov/grpclib) + [python-betterproto](https://github.com/danielgtaylor/python-betterproto) 214 | * Logging improvement 215 | * `Analytics`: Refine unload and processing assets data 216 | * Refactoring processing periodically events based on [schedule](https://github.com/dbader/schedule) 217 | * New strategy mode: [Keeping level of first asset](https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#keeping-level-of-first-asset) 218 | 219 | ### Update 220 | * Up requirements for exchanges-wrapper==2.1.6 221 | 222 | ## 3.0.1rc7 - 2024-03-26 223 | ### Update 224 | * Refactoring processing periodically events 225 | * Up requirements for exchanges-wrapper==2.1.5 226 | 227 | ## 3.0.1rc3 - 2024-03-21 228 | ### Added for new features 229 | :rocket: `Backtesting`: handling of partially filling events 230 | 231 | ### Update 232 | * Up requirements for exchanges-wrapper==2.1.3 233 | 234 | ## 3.0.1rc1 - 2024-03-19 235 | ### Fix 236 | * Cyclic Backtesting workflow 237 | 238 | ### Update 239 | * Up requirements for exchanges-wrapper==2.1.2 240 | 241 | 242 | ## 3.0.0rc22 - 2024-03-13 243 | ### Fix 244 | * `Analytics`: Refine unload and processing assets data 245 | 246 | ### Update 247 | * Up requirements for exchanges-wrapper==2.1.0 248 | 249 | ## 3.0.0rc20 - 2024-03-12 250 | ### Fix 251 | * `cancel_order_call()`: incorrect using asyncio.wait_for() 252 | 253 | ### Update 254 | * logging improvement 255 | * fee settings and handling 256 | 257 | ## 3.0.0rc19 - 2024-03-11 258 | ### Update 259 | * Replaced checking price when calculating TP vs updating price based on order book if error occurred 260 | * Don't unload assets on *`test`* accounts for avoid overload limited `coinmarketcap` API 261 | * Startup sequence optimization 262 | * Up requirements for exchanges-wrapper==2.0.1 263 | 264 | ## 3.0.0rc18 - 2024-03-10 265 | ### Update 266 | * Added price check when calculating TP in relation to the average filling price of the last grid order. 267 | Relevant for extreme volatility, was noticed on the test network. 268 | 269 | ## 3.0.0rc17 - 2024-03-10 270 | ### Update 271 | * Refine grid orders control 272 | 273 | ### Added for new features 274 | * `Backtesting`: save session collected date to `session_root/raw_bak.zip` 275 | 276 | ## 3.0.0rc14 - 2024-03-09 277 | ### Update 278 | * Up requirements for exchanges-wrapper==2.0.0 279 | * Refined logging 280 | 281 | ## 3.0.0rc8 - 2024-03-08 282 | ### Fix 283 | * logging for Simulate mode 284 | 285 | ### Update 286 | * :rocket: Migrate `gRPC` from [grpcio](https://grpc.io/) to [grpclib](https://github.com/vmagamedov/grpclib) + [python-betterproto](https://github.com/danielgtaylor/python-betterproto) 287 | * Binance: handling `EXPIRED_IN_MATCH` 288 | * `WSS`: check and restart 289 | 290 | ## 3.0.0rc1 - 2024-03-02 291 | * `on_balance_update_ex()`: don't update direct initial asset values in Reverse cycle 292 | * Partially filled TP in Hold reverse cycle 293 | 294 | ### Update 295 | * Project architecture 296 | 297 | ## 2.1.5 - 2024-02-25 298 | ### Fix 299 | * ModuleNotFoundError: No module named 'optimizer' #73 300 | 301 | ### Update 302 | * Do not send orders if it was getting 'FAILED_PRECONDITION' response 303 | * Do not start Telegram control process for Simulate mode or if bot id is not setting 304 | * Do not start DB control process for Simulate mode 305 | * Changed initialization sequence during recovery from saved state 306 | 307 | ## 2.1.4 - 2024-02-22 308 | ### Fix 309 | * `on_new_funds()` 310 | 311 | ### Update 312 | * Refine initialization backtesting 313 | * Up requirements for exchanges-wrapper==1.4.15 314 | 315 | ## 2.1.3 - 2024-02-20 316 | ### Fix 317 | * `calc_profit_order()`: rounding for `target_amount_first` 318 | * `restore_strategy_state()`: Tlg control thread 319 | 320 | ### Update 321 | * Some minor updates 322 | * Up requirements for exchanges-wrapper==1.4.14 323 | * Running `*_utils.py` modules as daemon 324 | 325 | ## 2.1.2 - 2024-02-17 326 | ### Fix 327 | * Create limit order error handler 328 | * Start simulate mode from saved state 329 | 330 | ## 2.1.1 - 2024-02-16 331 | ### Fix 332 | * `check_min_amount()` 333 | * `restore_strategy_state()` 334 | * Fix issue with csv empty lines on windows (#71) 335 | 336 | ## 2.1.0 - 2024-02-16 337 | ### Added for new features 338 | :rocket: Complete cyclic optimization of strategy parameters based on [`optuna` framework](https://optuna.readthedocs.io/en/stable/index.html) 339 | For reference see [Parameters optimization](https://github.com/DogsTailFarmer/martin-binance/wiki/Back-testing-and-parameters-optimization#parameters-optimization) 340 | 341 | ### Update 342 | * `cancel_order_call`: added timeout handler 343 | * Utilised partially filled TP amount during grid update 344 | * `create_limit_order`: Supress call strategy handler for `grpc.StatusCode.FAILED_PRECONDITION` exception, 345 | fire tlg message only. As a result, there are no attempts to re-place the order with incorrect condition. 346 | 347 | ## 2.1.0rc36 - 2024-02-11 348 | ### Fix 349 | * `on_order_update_handler`: refine 350 | 351 | ### Update 352 | * Up requirements for exchanges-wrapper==1.4.12 353 | 354 | ## 2.1.0rc33 - 2024-02-11 355 | ### Update 356 | * Some minor updates 357 | 358 | ## 2.1.0rc32 - 2024-02-11 359 | ### Fix 360 | * Event updates arrived before the saved state was restored 361 | 362 | ## 2.1.0rc31 - 2024-02-11 363 | ### Fix 364 | * `check_min_amount()`: `self.tp_amount` 365 | 366 | ### Update 367 | * Up requirements for exchanges-wrapper==1.4.11 368 | 369 | ## 2.1.0rc29 - 2024-02-09 370 | ### Update 371 | * `check_min_amount()`: refine check min trade amount for SELL cycle 372 | * Up requirements for exchanges-wrapper==1.4.10 373 | 374 | ## 2.1.0rc28 - 2024-02-07 375 | ### Update 376 | * Up requirements for exchanges-wrapper==1.4.9 377 | 378 | ## 2.1.0rc27 - 2024-02-07 379 | ### Update 380 | * 'relaunch.py': improved 381 | * Up requirements for exchanges-wrapper==1.4.9b5 (test grpc 1.60.1) 382 | 383 | ## 2.1.0rc25 - 2024-02-02 384 | ### Update 385 | * Up requirements for exchanges-wrapper==1.4.8 386 | 387 | ## 2.1.0rc20 - 2024-01-27 388 | ### Fix 389 | * An order that was `PARTIALLY FILLED` at the time of creation was counted as `FILLED` 390 | 391 | ### Update 392 | * margin_wrapper.buffered_funds(): some minor improvements 393 | * executor.get_free_assets(): for "free" mode the volume of the utilized deposit is taken into account when not TP 394 | * rollback saving/restore trades history, but used for it specified file in `/last_state/X_AAABBB.csv` 395 | * executor: remove parameters `SAVE_TRADE_HISTORY` and `SAVED_STATE`, already use for saving/restore trade events 396 | * comprehensive improvement of the internal accounting and recovery system 397 | * Up requirements for exchanges-wrapper==1.4.7 398 | * Dependency: Up requirements for Python>=3.9 399 | 400 | ### Added for new features 401 | * Per 10 mins update trade rules for pair 402 | * If new trade rules prevent the grid from placing orders, then the grid will be forced to be re-set 403 | * Add parameter`SELF_OPTIMIZATION = True` # Cyclic self-optimization of parameters, together with `MODE == 'TC'` 404 | 405 | ## 2.1.0rc1 - 2024-01-05 406 | ### Update 407 | * `SAVE_TRADE_HISTORY = False` set default 408 | * calculation `over price` for Reverse cycle : checking result on true positive, correction it if needed 409 | * [For backtesting change format saved data from Pickle to Feather #64](https://github.com/DogsTailFarmer/martin-binance/issues/64#issue-1858138993) 410 | + Change format to [Apache Parquet](https://parquet.apache.org) 411 | + Restrictions on the size of RAM have been removed due to write/read buffering 412 | + Use orjson for de/serialize stream data 413 | * replacing `json` with `ujson` to improve performance 414 | 415 | ## 2.0.7 - 2023-12-13 416 | ### Update 417 | * Up requirements for exchanges-wrapper==1.4.4 418 | 419 | ## 2.0.6 - 2023-12-12 420 | ### Update 421 | * Up requirements for exchanges-wrapper==1.4.3 [fixed #42](https://github.com/DogsTailFarmer/exchanges-wrapper/issues/42) 422 | 423 | ## 2.0.5 - 2023-12-11 424 | ### Update 425 | * Refining restoration of grid orders after their execution during the grid shift process 426 | * Up requirements for exchanges-wrapper==1.4.2 427 | * Some minor changes 428 | 429 | ## 2.0.4 - 2023-12-08 430 | ### Update 431 | * `funds_rate_exporter.py`: average CPU load present as percent value at 1 minute period 432 | * `solve()`, `calc_grid()`: use [scipy.optimize.minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html#scipy-optimize-minimize) 433 | for calculation Reverse cycle over price 434 | * optimize CPU loads at last_state_update() 435 | * convert_tp(): clear start_hold state and initialize restore grid orders 436 | * Up requirements for exchanges-wrapper==1.4.1 437 | 438 | ## 2.0.3 - 2023-12-01 439 | ### Fix 440 | * Backtesting, Simulation mode 441 | 442 | ### Update 443 | * solve(): The limit of attempts has been increased 444 | * calc_over_price(): The range of changes in the number of orders has been expanded 445 | 446 | ## 2.0.2 - 2023-11-26 447 | ### Fix 448 | * Version tags 449 | 450 | ## 2.0.1 - 2023-11-26 451 | ### Fix 452 | * "Groggy" state after start canceling grid before update it 453 | 454 | ### Update 455 | * Improved calc_over_price() 456 | 457 | ## 2.0.0.post2 - 2023-11-24 458 | ### Fix 459 | * Exception buffered_orders: 'NoneType' object is not subscriptable #68 460 | 461 | ## 2.0.0.post1 - 2023-11-23 462 | ### Fix 463 | * Fix issue detected by SonarCloud 464 | 465 | ## 2.0.0 - 2023-11-23 466 | ### Fix 467 | * Order price must be higher than the minimum price #67 468 | 469 | ### Update 470 | * Up requirements for exchanges-wrapper==1.4.0 471 | * Some minor improvements 472 | 473 | ## 2.0.0rc8 - 2023-11-16 474 | ### Fix 475 | * Handling of partially filled TP when Reverse hold 476 | * Part filled TP and part filled grid orders, unnecessary adjustment sum_amount_ 477 | 478 | ### Update 479 | * Refine cli_X_AAABBB.py 480 | * Remove db support to module 481 | * Remove Telegram support to module 482 | * README.md updated 483 | * Clear from margin code fragments compatibility 484 | * decimal.Decimal calculation for all modules was implemented 485 | * Up requirements for exchanges-wrapper==1.4.0rc6 486 | 487 | ## 2.0.0rc1 - 2023-11-01 488 | ### Added for new features 489 | * Bybit exchange V5 API support implemented. Supported account type is 490 | [Unified Trading Account](https://testnet.bybit.com/en/help-center/article/Introduction-to-Bybit-Unified-Trading-Account), 491 | for main and sub-accounts. Spot Trading only. 492 | 493 | ### Update 494 | * Lost compatibility with margin.de terminal. Scripts developed for use with the terminal can be run as 495 | executable modules, but not the other way around. 496 | * The logic and implementation of monitoring exceptional situations when placing and deleting orders has been updated. 497 | * Improved gRPC outage exception handling 498 | * Up requirements 499 | 500 | ## 1.3.7.post3 - 2023-10-09 501 | ### Fix 502 | * calc_profit_order(): rounding issue when correction tp amount on step_size 503 | 504 | ### Update 505 | * Up requirements for exchanges-wrapper==1.3.7.post4 506 | 507 | ## 1.3.7.post2 - 2023-10-05 508 | ### Fix 509 | * Fix: Refine exception handling when deleting a grid of orders, one of which is partially filled 510 | 511 | ### Update 512 | * Up requirements for exchanges-wrapper==1.3.7.post3 513 | 514 | ## 1.3.7.post1 - 2023-09-30 515 | ### Fix 516 | * Sonarcloud issues 517 | 518 | ## 1.3.7 - 2023-09-30 519 | ### Update 520 | * Up requirements for exchanges-wrapper==1.3.7.post2 521 | 522 | ## 1.3.7b1 2023-09-26 523 | ### Added for new features 524 | * Save trading (filling orders) and transfer asset history into a file `~/.MartinBinance/log/X_AAABBB.csv` 525 | 526 | headers: 527 | ``` 528 | "TRADE","transaction_time","side","order_id","client_order_id","trade_id","order_quantity","order_price","cumulative_filled_quantity","quote_asset_transacted","last_executed 529 | "TRANSFER","event_time","asset","balance_delta" 530 | ``` 531 | data: 532 | ``` 533 | "TRADE","1695745010026","SELL","9850221","4815001","1716764","0.00539700","26193.86000000","0.00539700","141.36826242","0.00539700","26193.86000000" 534 | "TRANSFER","1695745010027","LTC","-0.001" 535 | ``` 536 | 537 | ## 1.3.6.post1 2023-09-25 538 | ### Update 539 | * Limit for grid updates, updated when: 540 | + For Original cycle: the amount of free deposit not involved in the turnover more than 35% 541 | + For Reverse cycle: the amount of free deposit not involved in the turnover more than 65% 542 | 543 | ## 1.3.6 2023-09-24 544 | ### Fix 545 | * Due to a rounding error, the order status was incorrectly fetched if it was partially completed 546 | 547 | ### Update 548 | * Up requirements for exchanges-wrapper==1.3.7.post1 549 | 550 | ### Added for new features 551 | * To boost trade in case of low market volatility: TP order is updated once every 15 minutes, 552 | and in Reverse cycle grid update once an hour 553 | 554 | ## 1.3.5-1.dev1 555 | * Fix: Error getting internal id while re-placing grid order 556 | 557 | ## 1.3.5-1.dev0 558 | * Update: On-the-fly update assets with withdrawal and deposit operations also for Reverse cycle 559 | 560 | ## 1.3.5 2023-09-19 561 | ### Fix 562 | * Fix: Incorrect calculated depo volume for grid update, not include held grid orders volume 563 | * Up requirements for exchanges-wrapper to 1.3.7 564 | 565 | ### Update 566 | * Restored MS_ORDERS, convert from list to dict (inter-version compatibility), clear 567 | 568 | ## 1.3.4rc5-1 2023-08-24 569 | ### Update 570 | * `relaunch.py`: update deprecation method for `libtmux==0.22.1` 571 | 572 | ### Fix 573 | * [ Windows path can't contain ':' symbol #65 ](https://github.com/DogsTailFarmer/martin-binance/issues/65) 574 | 575 | ## 1.3.4rc5 2023-08-20 576 | ### Fix 577 | * Up requirements for exchanges-wrapper to 1.3.6b7 578 | 579 | ## 1.3.4rc4-5 2023-08-19 580 | ### Fix 581 | * Up requirements for exchanges-wrapper to 1.3.6b6 582 | * Up other requirements 583 | * Some minor improvements 584 | 585 | ## 1.3.4rc4-4 586 | ### Fix 587 | * [Handling additional TP filled by market after placed additional grid after filled grid after filled ordinary TP](https://github.com/DogsTailFarmer/martin-binance/issues/62#issue-1856466446) 588 | 589 | ## v1.3.4rc2 2023-08-10 590 | ### Info 591 | * Starting with version `martin-binance 1.3.4`, compatibility with `margin` will be lost, since some new parts 592 | of the code are no longer supported by implemented `Python 3.7`. I'm focused on `Python 3.10`. 593 | I won't rid the code of numerous compatibility elements yet, so if the margin team will update its version, 594 | everything should work. 595 | 596 | ## v1.3.4rc1 2023-08-09 597 | ### Fix 598 | * Processing of the situation when at bulk cancel of the grid orders and one or more of orders are FILLED. 599 | - cancel loop is interrupted 600 | - orders with received CANCELED status are restored 601 | - status is requested for unconfirmed orders and processing is performed: recovery if status is CANCELED, 602 | - normal processing in case of filling in 603 | 604 | * Processing of the situation when after partially filled TP one or some or all grids are filled 605 | 606 | * `executor.get_free_assets(mode='free')`: incorrect balance for opposite asset (always 0), 607 | as a result - a negative value of free funds 608 | * `margin_wrapper.restore_state_before_backtesting()`: tmp upgrade issue - convert cls.orders to dict 609 | 610 | ### Added for new features 611 | * `OoTSP.py`: add interactive option *Get parameters for specific trial* 612 | 613 | ### Update 614 | * Backtesting: 100% repeatability of test and initial sessions at the same parameters is achieved. 615 | Ticker history includes fulfillment events that may not be in the original stream. I.e. the order was executed 616 | at price P, and there was no such price in the stream at a given time period. This led to a divergence of the test 617 | session against the original (reference). 618 | 619 | 620 | 621 | 622 | 623 | * `README.md`: some design correction 624 | * Remove parameter `ex.EXTRA_CHECK_ORDER_STATE` and the corresponding function 625 | * Up requirements for grpcio to 1.56.0 626 | * Up requirements for exchanges-wrapper to 1.3.4 627 | 628 | ## v1.3.3 2023-07-04 629 | ### Added for new features 630 | * Send alarm (to db) if exist unreleased grid orders or hold TP 631 | * Up requirements for exchanges-wrapper to 1.3.3 632 | 633 | ## v1.3.2-3 2023-07-02 634 | ### Fix 635 | * fetch_order(): fixed status for archived order (remove it from StrategyBase.orders) 636 | 637 | ### Update 638 | * get_free_assets() added 'available' mode for initial funds check 639 | 640 | ## v1.3.2-2 2023-07-01 641 | ### Fix 642 | * Incorrect internal accounting of active orders 643 | * Changed StrategyBase.orders from list to dict 644 | * Some improvements in save/restore 645 | 646 | ## v1.3.2 2023-06-29 647 | ### Fix 648 | * buffered_orders() was try fetch orders and generate event on stopped strategy, full refactoring restore 649 | from saved state 650 | 651 | ### Update 652 | * remove all_trades and all_orders class variables from StrategyBase as redundant 653 | * Up requirements for exchanges-wrapper to 1.3.2 654 | 655 | ## v1.3.1-2 2023-06-28 656 | ### Update 657 | * funds_rate_exporter: optimized response rate to free balance change 658 | * client session id changed from uuid to shortuuid 659 | 660 | ### Added for new features 661 | * optuna: add visualization contour_plot and slice_plot 662 | 663 | ## v1.3.1-1 2023-06-22 664 | ### Fix 665 | * BACKTEST "S" mode: Restore an TP from a saved state, was missing 666 | 667 | ### Added for new features 668 | * BACKTEST "S" mode: add progress bar 669 | * BACKTEST "S" mode: supress logging 670 | 671 | ## v1.3.1 2023-06-21 672 | ### Update 673 | * Up requirements for exchanges-wrapper to 1.3.1 674 | * Refactoring the deployment process 675 | 676 | ## v1.3.0-4 2023-06-20 677 | ### Fix 678 | * ```get_free_assets(mode='free', backtest=True)``` return incorrect value for backtest Reverse cycle 679 | 680 | ### Update 681 | * Sometimes Tmux terminal after reboot not restore work path to ```/exch_srv.py``` 682 | For working case: in relaunch.py was changed './exch_srv.py' to 'exch_srv.py' and 683 | path ```~/.local/lib/python3.10/site-packages/exchanges_wrapper``` must be added to system PATH 684 | 685 | For Ubuntu at end of ```~/.bashrc``` add the string 686 | 687 | ``` 688 | export PATH=$PATH:~/.local/lib/python3.10/site-packages/exchanges_wrapper 689 | ``` 690 | and refresh bash ```source ~/.bashrc``` 691 | 692 | ## v1.3.0-3 2023-06-10 693 | ### Fix 694 | * Change ast.literal_eval() to eval() for trusted source 695 | 696 | ## v1.3.0-2 2023-06-19 697 | ### Added for new features 698 | * Ability to start backtesting from saved state, including in Reverse mode 699 | 700 | ### Update 701 | * Monitoring memory usage including swap 702 | * up requirements for exchanges-wrapper to 1.3.0-2 703 | 704 | ## v1.3.0-1 2023-06-10 705 | ### Fix 706 | * Some minor improvements 707 | 708 | ## v1.3.0 2023-06-09 709 | ### Added for new features 710 | * Backtesting capabilities 711 | * Based on [optuna framework](https://optuna.org) search for optimal trading parameters 712 | * Visual comparison of the results of the initial and trial trading sessions 713 | 714 | ### Update 715 | * Doc migrate from readme to wiki 716 | 717 | ## v1.3.0b22-23 2023-06-08 718 | ### Fix 719 | * correct funding check in start() 720 | 721 | ## v1.3.0b18-21 2023-06-06 722 | ### Fix 723 | * #59 724 | 725 | ### Update 726 | * Sync record/play 727 | * Add SAVE_DS = True # Save session result snapshot (ticker, orders) for compare 728 | * up requirements for exchanges-wrapper to 1.3.0-1 729 | * refactoring class StrategyBase 730 | * + backtest_data_control() 731 | 732 | ## v1.3.0b12 2023-06-02 733 | ### Fix 734 | * deepsource issues 735 | 736 | ## v1.3.0b11 2023-06-01 737 | ### Fix 738 | * Bitfinex: rename test pair from AAABBB to TESTBTCTESTUSDT, update template 739 | 740 | ### Update 741 | * protobuf format for CreateLimitOrder() method. **Not compatible with earlier versions** 742 | * for some situation (shift grid, cancel grid after filled TP and so on) changed cancel order method from "one by one" 743 | to bulk mode 744 | 745 | ## v1.2.18-8 2023-06-xx not released 746 | ### Fix 747 | * collect_assets() incorrect convert small decimal.Decimal values to str 748 | 749 | ### Update 750 | * For STANDALONE mode refactoring call environment for place_limit_order(), avoid unnecessary Decimal -> float 751 | conversions 752 | * Add error handling for atr() 753 | * Some minor improvements 754 | 755 | ## v1.2.18-4 2023-05-18 756 | ### Added for new features 757 | * Use ATR (Average True Range with 14 period x 15 min) indicator when 758 | [calculate first grid order volume](https://github.com/DogsTailFarmer/martin-binance/discussions/57#discussion-5167551) 759 | 760 | ### Update 761 | * Grafana template: Consolidated asset valuation - Assets by place: group by exchange name instead of account name 762 | * Halved the amount of buffer data to save memory (KLINES_LIM, ALL_TRADES_LIST_LIMIT, TRADES_LIST_LIMIT) 763 | 764 | ## v1.2.18-1 - 2023-05-07 765 | ### Fix 766 | * [#58](https://github.com/DogsTailFarmer/martin-binance/issues/58#issue-1698914151) 767 | A failed optimization of martin_binance/margin_wrapper.buffered_orders() was in version 1.2.16 768 | 769 | 770 | ## v1.2.18 - 2023-05-06 771 | ### Fix 772 | * Correct ending GRID ONLY cycle if USE_ALL_FUND = False 773 | 774 | ### Update 775 | * Refactoring set_profit() for precision calculate TP order price 776 | * Parameter PROFIT_K excluded 777 | * README.md 778 | 779 | ## v1.2.16-1-HotFix - 2023-04-12 780 | ### Fix 781 | * Binance: REST API update for endpoint: GET /api/v3/exchangeInfo was changed MIN_NOTIONAL filter 782 | 783 | ### Update 784 | * up requirements for exchanges-wrapper to 1.2.10-6 785 | 786 | ## v1.2.16 2023-04-05 787 | ### Fixed 788 | * Handling missed order event after restore from current state 789 | * Exception [] if PROFIT_MAX is not setting 790 | * Some minor improvements 791 | 792 | ### Update 793 | * up requirements for exchanges-wrapper to 1.2.10-5 794 | 795 | ## v1.2.15-2 2023-03-31 796 | ### Fixed 797 | * Bitfinex: was changed balance update order #56 798 | 799 | ## v1.2.15-1 2023-03-13 800 | ### Added for new features 801 | * Refactoring and updated auto-convert possibilities [Grid only mode](https://github.com/DogsTailFarmer/martin-binance#grid-only) 802 | 803 | ### Update 804 | * up requirements for exchanges-wrapper to 1.2.10-4 805 | 806 | ## v1.2.14-1 2023-02-24 807 | ### Added for new features 808 | * Periodically checking WSS status for active trade by client side. 809 | This correctly stops the server-side service in the event of an abnormal client shutdown 810 | and guarantees the service for an active trading session. 811 | 812 | ## v1.2.14 2023-02-22 813 | ### Fixed 814 | * Fixed #53 815 | 816 | ### Added for new features 817 | * Transfer free assets to main account, resolve #54 818 | 819 | ## v1.2.13-6 2023-02-04 820 | ### Fixed 821 | * [PR#47](https://github.com/DogsTailFarmer/martin-binance/pull/47#issue-1561537039) 822 | * [Fix shebang](https://github.com/DogsTailFarmer/martin-binance/issues/48#issue-1561939620) 823 | 824 | ### Update 825 | * Some minor improvements 826 | * up requirements for exchanges-wrapper to 1.2.9-2 827 | 828 | ## v1.2.13-5 2023-01-23 829 | ### Added for new features 830 | * Restart option for Telegram remote control from last saved state 831 | 832 | ### Update 833 | * Some minor improvements 834 | * up requirements for exchanges-wrapper to 1.2.9-1 835 | 836 | ## v1.2.13-3 2023-01-11 837 | ### Fixed 838 | * Optimized numerical solution of grid parameters in Reverse cycle to avoid rare cases of calculation failure 839 | + search for a solution with the specified or maximum possible accuracy 840 | + effectively finding possible accuracy, as a result, reducing the number of iterations by about 4 times 841 | + forced displacement of the starting point during looping allows to find a solution with possible accuracy, 842 | if it exists under given conditions 843 | 844 | ### Update 845 | * Config templates 846 | * Removing FTX smell 847 | * Add cumulative Free assets chart into Grafana template 848 | * up requirements for exchanges-wrapper to 1.2.9 849 | 850 | ## v1.2.12 2023-01-01 851 | ### Added for new features 852 | * Add connection to Binance US (binance.us) 853 | 854 | ### Update 855 | * up requirements for exchanges-wrapper to 1.2.8 856 | 857 | ## v1.2.11 2022-12-20 858 | ### Fixed 859 | * fix #43, import error in margin mode under Windows 860 | 861 | ## v1.2.10-8 2022-12-15 862 | ### Fixed 863 | * fix #42 864 | 865 | ### Update 866 | * up requirements for exchanges-wrapper to 1.2.7-7 867 | 868 | ## v1.2.10-7 2022-12-14 869 | ### Fixed 870 | * fix #39, fix #40, fix #41 871 | * grid_handler: No grid orders after part filled TP, fixed additional order params 872 | and part code refactoring 873 | 874 | ## v1.2.10-6 2022-12-08 875 | ### Update 876 | * up requirements for exchanges-wrapper to 1.2.7-6 877 | * Some minor improvements 878 | 879 | ## v1.2.10-5 2022-12-04 880 | ### Update 881 | * OKX: adding delay in place_limit_order() for avoid exceeding the rate limit (60 orders / 2s) 882 | * Some minor improvements 883 | * up requirements for exchanges-wrapper to 1.2.7-5 884 | 885 | ## v1.2.10-4 2022-11-25 886 | ### Update 887 | * up requirements for exchanges-wrapper to 1.2.7-4 888 | 889 | ## v1.2.10-3 2022-11-25 890 | ### Fixed 891 | * saving the state for partially executed orders as float() instead of Decimal() causes an error when restarting 892 | from the saved state if the last snapshot contains partial execution data. 893 | 894 | ## v1.2.10-2 2022-11-25 895 | ### Fixed 896 | * save/restore internal order_id 897 | 898 | ## v1.2.10-1 2022-11-24 899 | ### Update 900 | * up requirements for exchanges-wrapper to 1.2.7-3 901 | 902 | ## v1.2.10 2022-11-23 903 | ### Update 904 | * internal numbering for orders 905 | * Processing of partial order execution events is linked to a specific order number. 906 | Previously, events were processed sequentially, which led to an error when the order of receiving events was violated. 907 | 908 | ## v1.2.9-18 2022-11-21 909 | ### Update 910 | * Dependency to exchanges-wrapper 1.2.7-1 911 | 912 | ## v1.2.9-18 2022-11-21 913 | ### Fixed 914 | * #36 915 | * #37 916 | 917 | ## v1.2.9-14 2022-11-11 918 | ### Fixed 919 | * After restart from saved state incorrect value for first_run may cause incorrect data to be saved 920 | for the start deposit. Because of this, the control of initial balances may not work correctly. 921 | * Calculate parameters for grid. Adding price limitation based on [PERCENT_PRICE](https://github.com/binance/binance-spot-api-docs/blob/master/filters.md#percent_price) 922 | filter. Without such limit, the task of finding grid parameters for a given volume has several solutions, including in 923 | the area of negative price values. This is an unlikely event that could have been within the Reverse buy cycle and 924 | high volatility 925 | 926 | ### Update 927 | * Changed logic for place order error handling. Before - save to hold uncompleted order and wait event from exchange. 928 | Now - resend order after timeout. 929 | * Refactoring place_grid() and calc_grid() 930 | 931 | ### Added for new features 932 | * Before start of cycle and in periodically report are showed free assets' data, what volume of coins does 933 | not participate in the turnover 934 | 935 | > 19/10 22:26:23 Start 936 | > 937 | >Start process for .db save 938 | > 939 | >Start process for Telegram 940 | > 941 | >Number of unreachable objects collected by GC: 16 942 | > 943 | >19/10 22:26:23 Initial first: 1.009025, second: 9829.04880062 944 | > 945 | >**19/10 22:26:23 Free: First: 1.009025, second: 9629.04880062** 946 | > 947 | >19/10 22:26:24 Start Buy cycle with 200.0 USDT depo 948 | 949 | * #7 Allow to withdraw and deposit on active strategy with autocorrection initial balance and depo. See 950 | [manual](https://github.com/DogsTailFarmer/martin-binance#deposit-and-withdraw-assets-on-active-strategy) for detail 951 | 952 | ## v1.2.9-1 2022-10-14 953 | ### Update 954 | * Refusal to use PROFIT_REVERSE parameter. 955 | Valid for Reverse cycle. For a small deposit, the cycle income can be equal to the minimum order size step. 956 | If you divide it into parts to increase the deposit and make a profit, as a result of rounding the deposit amount does 957 | not increase, which negatively affects the ability of the strategy to complete the reverse cycle. 958 | Instead, all cycle profits are allocated either to increase the deposit (for an odd cycle number) 959 | or to accumulate profits (for an even cycle number) 960 | 961 | ## v1.2.9 2022-10-13 962 | ### Added for new features 963 | * Huobi exchange implemented 964 | 965 | ### Update 966 | * Dependency exchanges-wrapper==1.2.6 967 | 968 | ## v1.2.8-2 2022-09-29 969 | ### Fixed 970 | * For analytic export replace Bitfinex mapping from "IOTA to MIOTA" to "IOT to MIOTA" 971 | * Migration transition solution cleared for saved_state from 1.2.6 - > 1.2.7 972 | * lgtm [py/unused-import] 973 | 974 | ### Update 975 | * get_min_buy_amount() 976 | 977 | ## v1.2.8-1 2022-09-27 978 | ### Fixed 979 | * For analytic export replace IOTA to MIOTA for Bitfinex 980 | 981 | ### Update 982 | * dependency exchanges-wrapper up to 1.2.5-3 983 | 984 | ## v1.2.8 2022-09-26 985 | ### Added for new features 986 | * Powered by [Docker](https://www.docker.com/) deploy 987 | 988 | ## v1.2.7-1 2022-09-25 989 | ### Fixed 990 | * #26 #29 - add preliminary calculation of grid parameters 991 | ```console 992 | 25/09 16:33:07 set_trade_conditions: buy_side: False, depo: 0.050000, base_price: 18950.35, reverse_target_amount: 0, amount_min: 0.000528, step_size: 0.000001, delta_min: 0.01 993 | 25/09 16:33:07 set_trade_conditions: depo: 0.050000, order_q: 48, amount_first_grid: 0.000528, amount_2: 0.0005808, q_max: 24, coarse overprice: 1.210355 994 | 25/09 16:33:07 For Sell cycle will be set 24 orders for 1.2104% over price 995 | ``` 996 | Before start, you can correct value depo and other trade parameters 997 | 998 | * #30 Incorrect conversion to comparable currency at initial check of deposit volume 999 | * For analytic export replace IOT to IOTA for Bitfinex 1000 | 1001 | ## v1.2.7 2022-09-18 1002 | ### Fixed 1003 | * If it is not possible to calculate the price overlap for the cycle reverse, its value set to coarse estimate * 2 1004 | instead of the OVER_PRICE 1005 | * In saved state added StrategyBase.trades. This is for correct restore if order was filled partially 1006 | 1007 | ### Update 1008 | README.md - renewed installation chapter 1009 | 1010 | ### Added for new features 1011 | * Disconnecting the gRPC server is now safe, auto reconnect with full recovery of the current session 1012 | 1013 | ## v1.2.6-8-hotfix 2022-09-03 1014 | ### Fixed 1015 | * [File exist error on Windows 11](https://github.com/DogsTailFarmer/martin-binance/issues/19#issue-1360296628) 1016 | 1017 | ## v1.2.6-7 2022-09-01 1018 | ### Fixed 1019 | * Incorrect settings for max grid orders count at grid update (Filter failure: MIN_NOTIONAL) 1020 | 1021 | ### Update 1022 | * requirements.txt exchanges-wrapper>=1.2.4-5 1023 | 1024 | ## v1.2.6 2022-08-27 1025 | ### Fixed 1026 | * [Incomplete account setup](https://github.com/DogsTailFarmer/martin-binance/issues/17#issue-1347470971) 1027 | 1028 | ### Update 1029 | * up to Python 3.10.6 1030 | * 1.2.5-3 update on_place_order_error_string() to avoid the cyclical sending of an order rejected by the exchange 1031 | * [Update readme - limit for several pair with intersecting coin](https://github.com/DogsTailFarmer/martin-binance/issues/18#issue-1347575119) 1032 | 1033 | ## v1.2.5 2022-08-20 1034 | ### Fixed 1035 | * if not FEE_IN_PAIR and Reverse: underreporting of income as a result of excess fee accrual 1036 | 1037 | ### Update 1038 | * calculate round quote pattern 1039 | * optimize place grid method 1040 | 1041 | ### Added for new features 1042 | * implemented first grid order volume calculating for effective FTX trading 1043 | 1044 | ## v1.2.4 Hotfix - 2022-08-15 1045 | ### Fixed 1046 | * Incorrect calculation TP parameters for TP sell, price < 1, (fee + profit) amount < step_size 1047 | 1048 | ## v1.2.3 - 2022-08-14 1049 | ### Fixed 1050 | * [No status reply](https://github.com/DogsTailFarmer/martin-binance/issues/11#issue-1328210503) 1051 | * [Stopped command in last state when low RAM](https://github.com/DogsTailFarmer/martin-binance/issues/14#issue-1333292978) 1052 | 1053 | ### Added for new features 1054 | * Protect against OS failures when saving a state file 1055 | * [No help it Telegram bot](https://github.com/DogsTailFarmer/martin-binance/issues/8#issue-1315732905) 1056 | For Telegram bot set up command menu and online help 1057 | 1058 | ### Update 1059 | * Dependencies 1060 | * Refactoring calculate TP 1061 | * Refactoring calculate over price 1062 | 1063 | ## v1.2.2 - 2022-08-08 1064 | ### Update 1065 | * Add \n on each input request 1066 | * Handling HTTP 429 error for coinmarketcap 1067 | 1068 | ## v1.2.1 - 2022-08-07 1069 | ### Fixed 1070 | * Restore strategy from saved state after restart - get and handling missed event before the state was loaded 1071 | * Max retries exceeded with url: * for Telegram requests.post() 1072 | 1073 | ### Update 1074 | * After restart save previous last state file into .bak 1075 | 1076 | ## v1.2.0 - 2022-08-04 1077 | ### Added for new features 1078 | * Bitfinex exchange for STANDALONE mode added 1079 | * Control for first and last grid orders volume added 1080 | * For `ex.STATUS_DELAY = 5 # Minute between sending Tlg message about current status, 0 - disable` parameter add 1081 | ability to turn off 1082 | 1083 | ### Fixed 1084 | * Correct rounding for base and quote assets on different exchanges 1085 | 1086 | ### Update 1087 | * Refactoring method for calculate over price in Reverse cycle 1088 | * Up https://pro.coinmarketcap.com API call to v2 1089 | 1090 | If you update from lower versions please change reference in 1091 | `martin_binance/ms_cfg.toml` on next: 1092 | 1093 | `# CoinMarketCap` 1094 | 1095 | `url = "https://pro-api.coinmarketcap.com/v2/tools/price-conversion"` 1096 | 1097 | * ATTENTION: in the required package `exchanges-wrapper` was changed format config file 1098 | `exchanges_wrapper/exch_srv_cfg.toml` from 1.2.0 version. Before update, save old file and transfer configuration 1099 | data into new. 1100 | * Finished implemented Decimal calculation for orders processing 1101 | * Change data type for `REVERSE_TARGET_AMOUNT` parameter in `cli_XX_AAABBB.py`, update it before restart 1102 | * Renewed Grafana template 1103 | 1104 | ## v1.1.0 - 2022-06-16 1105 | ### Added for new features 1106 | * FTX exchange for STANDALONE mode added 1107 | * updating grid if market conditions change 1108 | 1109 | ### Update 1110 | * Grafana template 1111 | * code refactoring 1112 | 1113 | ## v1.0rc7 - 2022-03-01 1114 | ### Added for new features 1115 | * control and structure update for funds_rate.db for future updates 1116 | * alerting for mismatch of number of orders to strategy status 1117 | 1118 | ### Fixed 1119 | * updated balance value for direct cycle for analytics 1120 | 1121 | ### Update 1122 | * refined calculation of order parameters for grid, taking into account rounding of price and volume value and 1123 | correction of parameters of the last order 1124 | * Binance API for /api/v3/exchangeInfo 1125 | 1126 | ## v1.0rc6.1 - 2022-01-19 1127 | ### Update 1128 | * refactoring funds_rate_exporter.py 1129 | * readme.md, add 'For developer' chapter 1130 | 1131 | ## v1.0rc6 - 2022-01-11 1132 | ### Fixed 1133 | * handler for cancel order - part filled - correct cancel non-filled part 1134 | * release hold grid after check, not place grid 1135 | * remove extra messages about receiving the command through the Telegram 1136 | * incorrect balance in USD for assets in trading pair (for analytics) 1137 | 1138 | ### Update 1139 | * set min profit for TP also when calculate it before reverse 1140 | * Grafana dashboard template, some improvement 1141 | 1142 | ### Added for new features 1143 | * some cycle parameters for export into analytic 1144 | * STANDALONE mode (Binance) get asset balances from Funding wallet for analytics 1145 | 1146 | ## v1.0rc5 - 2021-12-27 1147 | ### Fixed 1148 | * Set min profit for TP when executing only the penultimate order of the grid 1149 | 1150 | ### Added for new features 1151 | * Modified and tested for macOS 1152 | 1153 | ## v1.0rc4 - 2021-12-23 1154 | ### Update 1155 | * Readme.md 1156 | 1157 | ### Added for new features 1158 | * Cycle end alert and manual action waiting 1159 | * Set min profit for TP when executing the penultimate order of the grid 1160 | 1161 | ### Fixed 1162 | * TypeError: unsupported operand type(s) for -: 'float' and 'NoneType' At: /executor.py(615): 1163 | 1164 | 1165 | ## v1.0rc2 - 2021-12-21 1166 | ### Fixed 1167 | * margin Windows full functional 1168 | * refactoring service process threading 1169 | 1170 | ## v1.0rc0 - 2021-12-07 1171 | * Added direct access to Binance SPOT API 1172 | * Migrate to Decimal() calculation for necessary precision 1173 | * Added stop-loss checkpoint for possible bug 1174 | * Split logic and initial trading setup 1175 | * Added buy/sell asset with grid options 1176 | 1177 | ### Fixed 1178 | * Full code refactoring 1179 | 1180 | ### Update 1181 | * Readme.md 1182 | 1183 | ## v0.8rc - 2021-07-29 1184 | ### Added for new features. 1185 | * Auto calculate round float multiplier 1186 | * Added fee processing for second currency only, like as KRAKEN 1187 | * For analytic subsystem added consolidated asset valuation 1188 | * Additional check for Error when place and cancel order 1189 | * Recovery state logic implemented 1190 | * For ADAPTIVE_TRADE_CONDITION adaptive calculate grid orders quantity 1191 | 1192 | ### Fixed 1193 | * Message 'Waiting ending cycle for manual action' instead of 'Stop...' 1194 | 1195 | ### Changed for existing functionality. 1196 | * Code refactoring 1197 | * Place grid orders by part block for first ORDER_Q count and one by one for next 1198 | 1199 | ### Update 1200 | * Readme.md 1201 | 1202 | ## v0.7rc - 2021-06-04 1203 | ### Added for new features. 1204 | * Send Telegram messages periodically that bot still alive 1205 | * Added adaptive profit calculate based on Bollinger band 1206 | * Added config for [prometheus_client](https://github.com/prometheus/client_python) 1207 | * Added 'no loss' over price calculate for Reverse cycle grid 1208 | * Added Average Directional Index analysis to optimize Reverse solution 1209 | 1210 | ### Fixed 1211 | * Difference k for top and bottom BB line 1212 | 1213 | ### Update 1214 | * Readme.md 1215 | 1216 | ## v0.5b - 2021-06-08 1217 | ### Added for new features 1218 | * Calculate price of grid orders by logarithmic scale 1219 | * Added cycle result data for save into t_funds 1220 | 1221 | ### Update 1222 | * Readme.md 1223 | 1224 | ### Fixed 1225 | * Refactoring and optimise calculate and place take profit order 1226 | * Optimise saving funds and cycle data to .db 1227 | 1228 | ## v0.4b - 2021-05-29 1229 | ### Added for new features. 1230 | * Check if the take profit order execute by market and process it 1231 | * Optimize send Telegram message and save to .db function 1232 | 1233 | ## v0.3b - 2021-05-28 1234 | ### Added for new features. 1235 | * Create public edition 1236 | 1237 | ## v0.2b - 2021-05-28 1238 | ### Fixed 1239 | * Fix funds call error 1240 | 1241 | ### Added for new features. 1242 | * Commented service functions 1243 | * Added setup info into docstring 1244 | 1245 | ## v0.7a - 2021-05-26 1246 | ### Added for new features. 1247 | * External control from Telegram bot 1248 | 1249 | ## v0.6a - 2021-05-23 1250 | 1251 | ### Added for new features 1252 | ### Changed for changes in existing functionality 1253 | ### Deprecated for soon-to-be removed features 1254 | ### Removed for now removed features 1255 | ### Fixed for any bug fixes 1256 | ### Security 1257 | 1258 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM python:3.10.6-slim 3 | 4 | RUN useradd -ms /bin/bash appuser 5 | USER appuser 6 | 7 | ENV PATH="/home/appuser/.local/bin:${PATH}" 8 | ENV PATH="/home/appuser/.local/lib/python3.10/site-packages:${PATH}" 9 | ENV PYTHONUNBUFFERED=1 10 | 11 | COPY requirements.txt requirements.txt 12 | 13 | RUN pip3 install --no-cache-dir -r requirements.txt 14 | 15 | COPY ./martin_binance /home/appuser/.local/lib/python3.10/site-packages/martin_binance/ 16 | 17 | USER root 18 | RUN chmod +x /home/appuser/.local/lib/python3.10/site-packages/martin_binance/backtest/optimizer.py 19 | USER appuser 20 | 21 | WORKDIR "/home/appuser/.local/lib/python3.10/site-packages" 22 | 23 | LABEL org.opencontainers.image.description="See README.md 'Get started' for setup and run package" 24 | 25 | EXPOSE 50051 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jerry Fedorenko aka VM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Profitable, fault-tolerant, adaptable to the market

3 | 4 | *** 5 |

Modified Martingale

6 | 7 |

Cyclic grid strategy for SPOT market

8 | 9 |

Free trading system for crypto exchanges:

10 |

Binance, Bitfinex, Huobi, OKX, Bybit,

11 | 12 | *** 13 |

martin-binance PyPI version <--> exchanges-wrapper PyPI version

14 | 15 | *** 16 |

17 | DeepSource 18 | DeepSource 19 | sonarcloud 20 | Downloads 21 |

22 | 23 | *** 24 | ## Disclaimer 25 | All risks and possible losses associated with use of this strategy lie with you. 26 | Strongly recommended that you test the strategy in the demo mode before using real bidding. 27 | 28 | ## Important notices 29 | * After update to `3.0.25`, the configuration file `exch_srv_cfg.toml` for [exchanges-wrapper](https://github.com/DogsTailFarmer/exchanges-wrapper) must be updated. [Use templates for reference.](https://github.com/DogsTailFarmer/exchanges-wrapper/blob/master/exchanges_wrapper/exch_srv_cfg.toml.template) 30 | 31 | ## Notices 32 | * You cannot run multiple pairs with overlapping currencies on the same account! 33 | 34 | >Valid: (BTC/USDT), (ETH/BUSD), (SOL/LTC) 35 | > 36 | >Incorrectly: (BTC/USDT), (ETH/USDT), (BTC/ETH) 37 | > 38 | >As a result of the mutual impact on the operating balance sheet, the liquidity control system will block the work. 39 | 40 | * Due to a limitation in the implementation of asyncio under **Windows**, this program cannot be executed. [Use with Docker on Windows instead.](https://github.com/DogsTailFarmer/martin-binance/wiki/Quick-start#docker) 41 | 42 | ## References 43 | * Detailed information about use this strategy placed to [wiki](https://github.com/DogsTailFarmer/martin-binance/wiki) 44 | * [Trade idea](https://github.com/DogsTailFarmer/martin-binance/wiki/Trade-idea) 45 | * [Quick start](https://github.com/DogsTailFarmer/martin-binance/wiki/Quick-start) 46 | * [Back testing and parameters optimization](https://github.com/DogsTailFarmer/martin-binance/wiki/Back-testing-and-parameters-optimization) 47 | 48 | ## Referral link 49 | 50 | 51 | Create account on [Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O) and get 10% discount on all trading fee 52 | 53 | Create account on [HTX](https://www.htx.com/invite/en-us/1f?invite_code=9uaw3223) 54 | 55 | Create account on [Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP) and get 6% rebate fee 56 | 57 | Create account on [OKX](https://okx.com/join/2607649) and will be in for the chance to earn up to 100 USDT 58 | 59 | Create account on [Bybit](https://www.bybit.com/invite?ref=9KEW1K) and get exclusive referral rewards 60 | 61 | Also, you can start strategy on [Hetzner](https://hetzner.cloud/?ref=uFdrF8nsdGMc) cloud VPS only for 3.79 € per month and receive €20 in cloud credits. 62 | 63 | ### Donate 64 | *USDT* (TRC20) TU3kagV9kxbjuUmEi6bUym5MTXjeM7Tm8K 65 | -------------------------------------------------------------------------------- /doc/Create_strategy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/doc/Create_strategy.png -------------------------------------------------------------------------------- /doc/Model of logarithmic grid.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/doc/Model of logarithmic grid.ods -------------------------------------------------------------------------------- /doc/graf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/doc/graf1.png -------------------------------------------------------------------------------- /doc/tlg_notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/doc/tlg_notify.png -------------------------------------------------------------------------------- /doc/tmux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/doc/tmux.png -------------------------------------------------------------------------------- /martin_binance/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Free trading system for Binance SPOT API 5 | """ 6 | __author__ = "Jerry Fedorenko" 7 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 8 | __license__ = "MIT" 9 | __version__ = "3.0.32" 10 | __maintainer__ = "Jerry Fedorenko" 11 | __contact__ = "https://github.com/DogsTailFarmer" 12 | 13 | from pathlib import Path 14 | from shutil import copy 15 | 16 | from exchanges_wrapper.definitions import Interval 17 | 18 | HEARTBEAT = 2 # Sec 19 | ORDER_TIMEOUT = HEARTBEAT * 15 # Sec 20 | KLINES_INIT = [Interval.ONE_MINUTE, Interval.FIFTY_MINUTES, Interval.ONE_HOUR] 21 | # 22 | WORK_PATH = Path(Path.home(), ".MartinBinance") 23 | CONFIG_PATH = Path(WORK_PATH, "config") 24 | CONFIG_FILE = Path(CONFIG_PATH, "ms_cfg.toml") 25 | DB_FILE = Path(WORK_PATH, "funds_rate.db") 26 | LOG_PATH = Path(WORK_PATH, "log") 27 | LAST_STATE_PATH = Path(WORK_PATH, "last_state") 28 | BACKTEST_PATH = Path(WORK_PATH, "back_test") 29 | TRIAL_PARAMS = Path(WORK_PATH, "trial_params.json") 30 | CERT_DIR = Path(WORK_PATH, "keys") 31 | LOG_FILE_TLG = Path(LOG_PATH, "tlg_proxy.log") 32 | EQUAL_STR = "================================================================" 33 | 34 | 35 | def init(): 36 | if CONFIG_FILE.exists(): 37 | print(f"Client config found at {CONFIG_FILE}") 38 | else: 39 | print("Can't find client config file! Creating it...") 40 | for path in [CONFIG_PATH, LOG_PATH, LAST_STATE_PATH, CERT_DIR]: 41 | path.mkdir(parents=True, exist_ok=True) 42 | 43 | templates = Path(Path(__file__).parent.absolute(), "templates") 44 | 45 | copy(Path(templates, "ms_cfg.toml"), CONFIG_FILE) 46 | 47 | files_to_copy = [ 48 | "funds_rate.db", 49 | "trial_params.json", 50 | "cli_0_BTCUSDT.py", 51 | "cli_1_BTCUSDT.py", 52 | "cli_2_TESTBTCTESTUSDT.py", 53 | "cli_3_BTCUSDT.py" 54 | ] 55 | # skipcq: PYL-W0106 56 | [copy(Path(templates, file_name), Path(WORK_PATH, file_name)) for file_name in files_to_copy] 57 | 58 | print(f"Before the first run, set the parameters in {CONFIG_FILE}") 59 | raise SystemExit(1) 60 | 61 | 62 | if __name__ == '__main__': 63 | init() 64 | -------------------------------------------------------------------------------- /martin_binance/backtest/OoTSP.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Optimization of Trading Strategy Parameters 5 | """ 6 | __author__ = "Jerry Fedorenko" 7 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 8 | __license__ = "MIT" 9 | __version__ = "3.0.1" 10 | __maintainer__ = "Jerry Fedorenko" 11 | __contact__ = "https://github.com/DogsTailFarmer" 12 | 13 | from pathlib import Path 14 | import optuna 15 | import inquirer 16 | from inquirer.themes import GreenPassion 17 | from martin_binance import BACKTEST_PATH 18 | from martin_binance.backtest.optimizer import optimize 19 | 20 | SKIP_LOG = True 21 | 22 | vis = optuna.visualization 23 | ii_params = [] 24 | 25 | 26 | def main(): 27 | while 1: 28 | questions = [ 29 | inquirer.List( 30 | "path", 31 | message="Select from saved: exchange_PAIR with the strategy you want to optimize", 32 | choices=[f.name for f in BACKTEST_PATH.iterdir() if f.is_dir() and f.name.count('_') == 1], 33 | ), 34 | inquirer.List( 35 | "mode", 36 | message="New study session or analise from saved one", 37 | choices=["New", "Analise saved study session", "Exit"], 38 | ), 39 | inquirer.Text( 40 | "n_trials", 41 | message="Enter number of cycles, from 2 to 1000", 42 | ignore=lambda x: x["mode"] in ("Analise saved study session", "Exit"), 43 | default='150', 44 | validate=lambda _, c: 15 <= int(c) <= 1000, 45 | ), 46 | ] 47 | 48 | answers = inquirer.prompt(questions, theme=GreenPassion()) 49 | 50 | study_name = answers.get('path') # Unique identifier of the study 51 | storage_name = f"sqlite:///{Path(BACKTEST_PATH, study_name, 'study.db')}" 52 | 53 | if answers.get('mode') == 'New': 54 | Path(BACKTEST_PATH, study_name, 'study.db').unlink(missing_ok=True) 55 | try: 56 | strategy = next(Path(BACKTEST_PATH, study_name).glob("cli_*.py")) 57 | except StopIteration: 58 | raise UserWarning(f"Can't find cli_*.py in {Path(BACKTEST_PATH, study_name)}") 59 | 60 | study = optimize( 61 | study_name, 62 | strategy, 63 | int(answers.get('n_trials', '0')), 64 | storage_name, 65 | skip_log=SKIP_LOG, 66 | show_progress_bar=SKIP_LOG 67 | ) 68 | print_study_result(study) 69 | print(f"Study instance saved to {storage_name} for later use") 70 | elif answers.get('mode') == 'Analise saved study session': 71 | # noinspection PyArgumentList 72 | study = optuna.load_study(study_name=study_name, storage=storage_name) 73 | 74 | print(f"Best value: {study.best_value}") 75 | print(f"Original value: {study.get_trials()[0].value}") 76 | 77 | questions = [ 78 | inquirer.List( 79 | "mode", 80 | message="Make a choice", 81 | choices=["Plot from saved", "Get parameters for specific trial", "Exit"], 82 | ), 83 | inquirer.Text( 84 | "n_trial", 85 | message="Enter the trial number", 86 | ignore=lambda x: x["mode"] in ("Plot from saved", "Exit"), 87 | ), 88 | ] 89 | 90 | answers = inquirer.prompt(questions, theme=GreenPassion()) 91 | if answers.get('mode') == 'Plot from saved': 92 | i_params = print_study_result(study) 93 | for index, p in enumerate(i_params.items()): 94 | ii_params.append(p[0]) 95 | if index == 2: 96 | break 97 | # 98 | try: 99 | fig = vis.plot_optimization_history(study) 100 | fig.show() 101 | contour_plot = vis.plot_contour(study, params=ii_params) 102 | contour_plot.show() 103 | slice_plot = vis.plot_slice(study, params=ii_params) 104 | slice_plot.show() 105 | except ImportError: 106 | print("Can't find GUI, you can copy study instance to another environment for analyze it") 107 | elif answers.get('mode') == 'Get parameters for specific trial': 108 | trial = study.get_trials()[int(answers.get('n_trial', '0'))] 109 | print(f"number: {trial.number}") 110 | print(trial.state) 111 | print(f"value: {trial.value}") 112 | print(f"params: {trial.params}") 113 | else: 114 | break 115 | else: 116 | break 117 | 118 | 119 | def print_study_result(study): 120 | print(f"Optimal parameters: {study.best_params} for get {study.best_value}") 121 | try: 122 | importance_params = optuna.importance.get_param_importances(study) 123 | except RuntimeError as e: 124 | importance_params = {} 125 | print(e) 126 | else: 127 | print("Evaluate parameter importance based on completed trials in the given study:") 128 | for p in importance_params.items(): 129 | print(p) 130 | return importance_params 131 | 132 | 133 | if __name__ == '__main__': 134 | main() 135 | -------------------------------------------------------------------------------- /martin_binance/backtest/VCoSEL.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Visual Comparison of Session Extended Log 5 | """ 6 | __author__ = "Jerry Fedorenko" 7 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 8 | __license__ = "MIT" 9 | __version__ = "2.1.0" 10 | __maintainer__ = "Jerry Fedorenko" 11 | __contact__ = "https://github.com/DogsTailFarmer" 12 | 13 | from dash import Dash, html, dcc 14 | import plotly.graph_objects as go 15 | import pandas as pd 16 | 17 | from pathlib import Path 18 | from tkinter.filedialog import askdirectory 19 | 20 | from martin_binance import BACKTEST_PATH 21 | 22 | clrs = {'background': '#696969', 23 | 'text': '#7FDBFF'} 24 | 25 | source_path = askdirectory(title='Pick a folder for base strategy: "back_test/exchange_AAABBB/snapshot/"', 26 | initialdir=str(BACKTEST_PATH)) 27 | s_sell_df: pd.DataFrame = pd.read_pickle(Path(BACKTEST_PATH, source_path, "sell.pkl")) 28 | s_buy_df = pd.read_pickle(Path(BACKTEST_PATH, source_path, "buy.pkl")) 29 | 30 | df_path = askdirectory(title='Pick a folder for test strategy', initialdir=str(BACKTEST_PATH)) 31 | ds_ticker = pd.read_pickle(Path(BACKTEST_PATH, df_path, "ticker.pkl")) 32 | df_grid_sell = pd.read_pickle(Path(BACKTEST_PATH, df_path, "sell.pkl")) 33 | df_grid_buy = pd.read_pickle(Path(BACKTEST_PATH, df_path, "buy.pkl")) 34 | 35 | app = Dash(__name__) 36 | fig = go.Figure() 37 | fig.update_layout(template='seaborn') 38 | 39 | # Test data 40 | # noinspection PyTypeChecker 41 | fig.add_traces(go.Scatter(x=ds_ticker.index, y=ds_ticker.values, mode='lines', line_color='brown', name='Test')) 42 | 43 | for col in df_grid_sell.columns: 44 | # noinspection PyTypeChecker 45 | fig.add_traces(go.Scatter(x=df_grid_sell.index, y=df_grid_sell[col], mode='lines', line_color='red', 46 | showlegend=False)) 47 | 48 | for col in df_grid_buy.columns: 49 | # noinspection PyTypeChecker 50 | fig.add_traces(go.Scatter(x=df_grid_buy.index, y=df_grid_buy[col], mode='lines', line_color='green', 51 | showlegend=False)) 52 | 53 | # SOURCE data 54 | # noinspection PyTypeChecker 55 | 56 | for col in s_sell_df.columns: 57 | # noinspection PyTypeChecker 58 | fig.add_traces(go.Scatter(x=s_sell_df.index, y=s_sell_df[col], mode='lines', showlegend=False, 59 | line=dict(color='indianred', width=5, dash='dot'))) 60 | 61 | for col in s_buy_df.columns: 62 | # noinspection PyTypeChecker 63 | fig.add_traces(go.Scatter(x=s_buy_df.index, y=s_buy_df[col], mode='lines', showlegend=False, 64 | line=dict(color='forestgreen', width=5, dash='dot'))) 65 | 66 | 67 | fig.update_layout(xaxis_tickformat="%H:%M:%S.%L", height=700, autosize=True) 68 | 69 | app.layout = html.Div( 70 | [ 71 | html.H2(children='Back test data analyser', 72 | style={'textAlign': 'center', 'color': clrs['text'], 'backgroundColor': clrs['background']}), 73 | dcc.Graph(figure=fig) 74 | ] 75 | ) 76 | 77 | 78 | if __name__ == '__main__': 79 | app.run_server(debug=False) 80 | -------------------------------------------------------------------------------- /martin_binance/backtest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/martin_binance/backtest/__init__.py -------------------------------------------------------------------------------- /martin_binance/backtest/exchange_simulator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Simple exchange simulator for backtest purpose 5 | """ 6 | __author__ = "Jerry Fedorenko" 7 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 8 | __license__ = "MIT" 9 | __version__ = "3.0.2" 10 | __maintainer__ = "Jerry Fedorenko" 11 | __contact__ = "https://github.com/DogsTailFarmer" 12 | 13 | from decimal import Decimal 14 | import pandas as pd 15 | 16 | 17 | def any2str(_x) -> str: 18 | return f"{_x:.8f}".rstrip('0').rstrip('.') 19 | 20 | 21 | class Funds: 22 | __slots__ = ("base", "quote") 23 | 24 | def __init__(self): 25 | # {'asset': 'BTC', 'free': '0.0', 'locked': '0.0'} 26 | self.base = {} 27 | self.quote = {} 28 | 29 | def get_funds(self): 30 | base = self.base.copy() 31 | base |= {'free': str(base['free']), 'locked': str(base['locked'])} 32 | quote = self.quote.copy() 33 | quote |= {'free': str(quote['free']), 'locked': str(quote['locked'])} 34 | return [base, quote] 35 | 36 | def on_order_created(self, buy: bool, amount: Decimal, price: Decimal): 37 | if buy: 38 | self.quote['free'] -= amount * price 39 | self.quote['locked'] += amount * price 40 | else: 41 | self.base['free'] -= amount 42 | self.base['locked'] += amount 43 | 44 | def on_order_canceled(self, side: str, amount: Decimal, price: Decimal): 45 | if side == 'BUY': 46 | self.quote['free'] += amount * price 47 | self.quote['locked'] -= amount * price 48 | else: 49 | self.base['free'] += amount 50 | self.base['locked'] -= amount 51 | 52 | def on_order_filled(self, side: str, amount: Decimal, price: Decimal, last_price: Decimal, fee: Decimal): 53 | if side == 'BUY': 54 | self.base['free'] += amount - fee * amount / 100 55 | self.quote['locked'] -= amount * price 56 | self.quote['free'] += amount * (price - last_price) 57 | else: 58 | self.base['locked'] -= amount 59 | self.quote['free'] += amount * last_price - fee * (amount * last_price) / 100 60 | 61 | 62 | class Order: 63 | __slots__ = ( 64 | "symbol", 65 | "order_id", 66 | "order_list_id", 67 | "client_order_id", 68 | "transact_time", 69 | "price", 70 | "orig_qty", 71 | "executed_qty", 72 | "cummulative_quote_qty", 73 | "status", 74 | "time_in_force", 75 | "type", 76 | "side", 77 | "working_time", 78 | "self_trade_prevention_mode", 79 | "event_time", 80 | "last_executed_quantity", 81 | "cumulative_filled_quantity", 82 | "last_executed_price", 83 | "trade_id", 84 | "order_creation_time", 85 | "quote_asset_transacted", 86 | "last_quote_asset_transacted", 87 | "quote_order_quantity", 88 | ) 89 | 90 | def __init__(self, symbol: str, order_id: int, client_order_id: str, buy: bool, amount: str, price: str, lt: int): 91 | self.symbol = symbol 92 | self.order_id = order_id 93 | self.order_list_id = -1 94 | self.client_order_id = client_order_id 95 | self.transact_time = lt # local time 96 | self.price = Decimal(price) 97 | self.orig_qty = Decimal(amount) 98 | self.executed_qty = Decimal('0') 99 | self.cummulative_quote_qty = Decimal('0') 100 | self.status = 'NEW' 101 | self.time_in_force = 'GTC' 102 | self.type = 'LIMIT' 103 | self.side = "BUY" if buy else "SELL" 104 | self.working_time = "-1" 105 | self.self_trade_prevention_mode = 'NONE' 106 | # 107 | self.event_time: int 108 | self.last_executed_quantity = Decimal('0') 109 | self.cumulative_filled_quantity = Decimal('0') 110 | self.last_executed_price = Decimal('0') 111 | self.trade_id: int 112 | self.order_creation_time = lt 113 | self.quote_asset_transacted = Decimal('0') 114 | self.last_quote_asset_transacted = Decimal('0') 115 | self.quote_order_quantity = self.orig_qty * self.price 116 | 117 | 118 | class Account: 119 | __slots__ = ( 120 | "save_ds", 121 | "funds", 122 | "fee_maker", 123 | "fee_taker", 124 | "orders", 125 | "orders_buy", 126 | "orders_sell", 127 | "trade_id", 128 | "ticker", 129 | "grid_buy", 130 | "grid_sell", 131 | "ticker_last", 132 | "market_ids", 133 | ) 134 | 135 | def __init__(self, save_ds: bool): 136 | self.save_ds = save_ds 137 | self.funds = Funds() 138 | self.fee_maker = Decimal('0') 139 | self.fee_taker = Decimal('0') 140 | self.orders = {} 141 | self.orders_buy = pd.Series() 142 | self.orders_sell = pd.Series() 143 | self.trade_id = 0 144 | self.ticker = {} 145 | self.grid_buy = {} 146 | self.grid_sell = {} 147 | self.ticker_last = Decimal('0') 148 | self.market_ids = [] 149 | 150 | def create_order( 151 | self, 152 | symbol: str, 153 | client_order_id: str, 154 | buy: bool, 155 | amount: str, 156 | price: str, 157 | lt: int, 158 | order_id=None) -> {}: 159 | 160 | order_id = order_id or ((max(self.orders.keys()) + 1) if self.orders else 1) 161 | order = Order( 162 | symbol=symbol, 163 | order_id=order_id, 164 | client_order_id=client_order_id, 165 | buy=buy, 166 | amount=amount, 167 | price=price, 168 | lt=lt 169 | ) 170 | 171 | if buy: 172 | self.orders_buy.at[order_id] = Decimal(price) 173 | if self.save_ds: 174 | self.grid_buy[lt] = self.orders_buy 175 | else: 176 | self.orders_sell.at[order_id] = Decimal(price) 177 | if self.save_ds: 178 | self.grid_sell[lt] = self.orders_sell 179 | # 180 | self.funds.on_order_created(buy=buy, amount=Decimal(amount), price=Decimal(price)) 181 | self.orders[order_id] = order 182 | 183 | if self.ticker_last and ((buy and Decimal(price) >= self.ticker_last) or 184 | (not buy and Decimal(price) <= self.ticker_last)): 185 | # Market event 186 | self.market_ids.append(order_id) 187 | 188 | return {'symbol': order.symbol, 189 | 'orderId': order.order_id, 190 | 'orderListId': order.order_list_id, 191 | 'clientOrderId': order.client_order_id, 192 | 'transactTime': order.transact_time, 193 | 'price': order.price, 194 | 'origQty': order.orig_qty, 195 | 'executedQty': order.executed_qty, 196 | 'cummulativeQuoteQty': order.cummulative_quote_qty, 197 | 'status': order.status, 198 | 'timeInForce': order.time_in_force, 199 | 'type': order.type, 200 | 'side': order.side, 201 | 'workingTime': order.working_time, 202 | 'selfTradePreventionMode': order.self_trade_prevention_mode} 203 | 204 | def cancel_order(self, order_id: int, ts: int): 205 | order = self.orders.get(order_id) 206 | if order is None: 207 | raise UserWarning(f"Error on Cancel order, can't find {order_id} anymore") 208 | 209 | order.status = 'CANCELED' 210 | try: 211 | if order.side == 'BUY': 212 | self.orders_buy = self.orders_buy.drop(order_id) 213 | if self.save_ds and self.orders_buy.values.size: 214 | self.grid_buy[ts] = self.orders_buy 215 | else: 216 | self.orders_sell = self.orders_sell.drop(order_id) 217 | if self.save_ds and self.orders_sell.values.size: 218 | self.grid_sell[ts] = self.orders_sell 219 | except Exception as ex: 220 | raise UserWarning(f"Order {order_id} not active: {ex}") from ex 221 | 222 | self.orders[order_id] = order 223 | self.funds.on_order_canceled(order.side, order.orig_qty - order.executed_qty, order.price) 224 | return {'symbol': order.symbol, 225 | 'origClientOrderId': order.client_order_id, 226 | 'orderId': order.order_id, 227 | 'orderListId': order.order_list_id, 228 | 'clientOrderId': 'qwert', 229 | 'price': str(order.price), 230 | 'origQty': str(order.orig_qty), 231 | 'executedQty': str(order.executed_qty), 232 | 'cummulativeQuoteQty': str(order.cummulative_quote_qty), 233 | 'status': order.status, 234 | 'timeInForce': order.time_in_force, 235 | 'type': order.type, 236 | 'side': order.side, 237 | 'selfTradePreventionMode': order.self_trade_prevention_mode} 238 | 239 | def on_ticker_update(self, ticker: {}, ts: int) -> [dict]: 240 | filled_buy_id = [] 241 | filled_sell_id = [] 242 | orders_id = [] 243 | orders_filled = [] 244 | 245 | self.ticker_last = Decimal(ticker['lastPrice']) 246 | qty = Decimal(ticker['Qty']) 247 | part = bool(qty) 248 | 249 | if self.market_ids: 250 | orders_id.extend(self.market_ids) 251 | 252 | orders_id.extend(self.orders_buy[self.orders_buy >= self.ticker_last].index.values) 253 | orders_id.extend(self.orders_sell[self.orders_sell <= self.ticker_last].index.values) 254 | 255 | if self.save_ds: 256 | # Save data for analytics 257 | self.ticker[ts] = ticker['lastPrice'] 258 | if self.orders_sell.values.size: 259 | self.grid_sell[ts] = self.orders_sell 260 | if self.orders_buy.values.size: 261 | self.grid_buy[ts] = self.orders_buy 262 | # 263 | for order_id in orders_id: 264 | if part and not qty: 265 | break 266 | 267 | order = self.orders.get(order_id) 268 | 269 | order.transact_time = int(ticker['closeTime']) 270 | order.event_time = order.transact_time 271 | order.trade_id = self.trade_id = self.trade_id + 1 272 | 273 | order.last_executed_price = self.ticker_last 274 | 275 | delta = order.orig_qty - order.executed_qty 276 | order.last_executed_quantity = last_executed_qty = min(delta, qty) if part else delta 277 | order.executed_qty += last_executed_qty 278 | order.last_quote_asset_transacted = order.last_executed_price * last_executed_qty 279 | order.quote_asset_transacted += order.last_quote_asset_transacted 280 | 281 | if part: 282 | qty -= last_executed_qty 283 | 284 | order.cumulative_filled_quantity = order.executed_qty 285 | order.cummulative_quote_qty = order.quote_asset_transacted 286 | 287 | if order.executed_qty >= order.orig_qty: 288 | order.status = 'FILLED' 289 | if order.side == 'BUY': 290 | filled_buy_id.append(order_id) 291 | else: 292 | filled_sell_id.append(order_id) 293 | elif 0 < order.executed_qty < order.orig_qty: 294 | order.status = 'PARTIALLY_FILLED' 295 | # 296 | self.orders[order_id] = order 297 | # 298 | res = { 299 | 'event_time': order.event_time, 300 | 'symbol': order.symbol, 301 | 'client_order_id': order.client_order_id, 302 | 'side': order.side, 303 | 'order_type': order.type, 304 | 'time_in_force': order.time_in_force, 305 | 'order_quantity': str(order.orig_qty), 306 | 'order_price': str(order.price), 307 | 'stop_price': '0', 308 | 'iceberg_quantity': '0', 309 | 'order_list_id': -1, 310 | 'original_client_id': order.client_order_id, 311 | 'execution_type': 'TRADE', 312 | 'order_status': order.status, 313 | 'order_reject_reason': 'NONE', 314 | 'order_id': order_id, 315 | 'last_executed_quantity': str(order.last_executed_quantity), 316 | 'cumulative_filled_quantity': str(order.cumulative_filled_quantity), 317 | 'last_executed_price': str(order.last_executed_price), 318 | 'commission_amount': '0', 319 | 'commission_asset': '', 320 | 'transaction_time': order.transact_time, 321 | 'trade_id': order.trade_id, 322 | 'ignore_a': 12345678, 323 | 'in_order_book': False, 324 | 'is_maker_side': bool(order_id not in self.market_ids), 325 | 'ignore_b': True, 326 | 'order_creation_time': order.order_creation_time, 327 | 'quote_asset_transacted': str(order.quote_asset_transacted), 328 | 'last_quote_asset_transacted': str(order.last_quote_asset_transacted), 329 | 'quote_order_quantity': str(order.quote_order_quantity) 330 | } 331 | # 332 | orders_filled.append(res) 333 | 334 | self.funds.on_order_filled( 335 | order.side, 336 | order.last_executed_quantity, 337 | order.price, 338 | order.last_executed_price, 339 | self.fee_taker if order_id in self.market_ids else self.fee_maker 340 | ) 341 | # 342 | self.orders_buy = self.orders_buy.drop(filled_buy_id) 343 | self.orders_sell = self.orders_sell.drop(filled_sell_id) 344 | self.market_ids.clear() 345 | 346 | return orders_filled 347 | 348 | def restore_state(self, symbol: str, lt: int, orders: [], sum_amount: ()): 349 | if sum_amount[0]: 350 | self.funds.base['free'] += sum_amount[1] 351 | self.funds.quote['free'] -= sum_amount[2] 352 | else: 353 | self.funds.base['free'] -= sum_amount[1] 354 | self.funds.quote['free'] += sum_amount[2] 355 | 356 | for order in orders: 357 | self.create_order( 358 | symbol=symbol, 359 | client_order_id='', 360 | buy=order['buy'], 361 | amount=any2str(order['amount']), 362 | price=any2str(order['price']), 363 | lt=lt, 364 | order_id=order['id'] 365 | ) 366 | -------------------------------------------------------------------------------- /martin_binance/backtest/optimizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Searches for optimal parameters for a strategy under given conditions 5 | """ 6 | __author__ = "Jerry Fedorenko" 7 | __copyright__ = "Copyright © 2024 Jerry Fedorenko aka VM" 8 | __license__ = "MIT" 9 | __version__ = "3.0.5" 10 | __maintainer__ = "Jerry Fedorenko" 11 | __contact__ = "https://github.com/DogsTailFarmer" 12 | 13 | 14 | import importlib.util as iu 15 | import logging.handlers 16 | import stat 17 | import sys 18 | from decimal import Decimal 19 | from pathlib import Path 20 | 21 | import optuna 22 | import ujson as json 23 | 24 | from martin_binance import LOG_PATH, TRIAL_PARAMS 25 | 26 | OPTIMIZER = Path(__file__).absolute() 27 | try: 28 | OPTIMIZER.chmod(OPTIMIZER.stat().st_mode | stat.S_IEXEC) 29 | except PermissionError: 30 | pass # if executed in Docker environment 31 | 32 | PARAMS_FLOAT = ['KBB'] 33 | STRATEGY = None 34 | 35 | 36 | # noinspection PyUnusedLocal 37 | def notify_exception(*args): 38 | pass # Supress message from sys.excepthook 39 | 40 | 41 | def any2str(_x) -> str: 42 | return f"{_x:.6f}".rstrip('0').rstrip('.') 43 | 44 | 45 | def try_trade(mbs, skip_log, **kwargs): 46 | for key, value in kwargs.items(): 47 | setattr(mbs.ex, key, value if isinstance(value, int) or key in PARAMS_FLOAT else Decimal(f"{value}")) 48 | mbs.ex.MODE = 'S' 49 | mbs.ex.SAVE_DS = False 50 | mbs.ex.LOGGING = not skip_log 51 | global STRATEGY 52 | STRATEGY = mbs.trade(STRATEGY) 53 | return float(mbs.ex.SESSION_RESULT.get('profit', 0)) + float(mbs.ex.SESSION_RESULT.get('free', 0)) 54 | 55 | 56 | def optimize(study_name, cli, n_trials, storage_name=None, _prm_best=None, skip_log=True, show_progress_bar=False): 57 | sys.excepthook = notify_exception 58 | optuna.logging.set_verbosity(optuna.logging.WARNING) 59 | 60 | # Load parameter definitions from JSON file 61 | with open(TRIAL_PARAMS) as f: 62 | param_defs = json.load(f) 63 | 64 | spec = iu.spec_from_file_location("strategy", cli) 65 | mbs = iu.module_from_spec(spec) 66 | spec.loader.exec_module(mbs) 67 | 68 | def objective(_trial): 69 | params = {} 70 | for param_name, param_props in param_defs.items(): 71 | if param_props['type'] == 'int': 72 | params[param_name] = _trial.suggest_int( 73 | param_name, *param_props['range'], step=param_props.get('step', 1) 74 | ) 75 | elif param_props['type'] == 'float': 76 | params[param_name] = _trial.suggest_float( 77 | param_name, *param_props['range'], step=param_props.get('step', 0.1) 78 | ) 79 | return try_trade(mbs, skip_log, **params) 80 | 81 | # noinspection PyArgumentList 82 | _study = optuna.create_study(study_name=study_name, storage=storage_name, direction="maximize") 83 | 84 | if _prm_best: 85 | logger.info(f"Previous best params: {_prm_best}") 86 | _study.enqueue_trial(_prm_best) 87 | 88 | _study.optimize(objective, n_trials=n_trials, gc_after_trial=True, show_progress_bar=show_progress_bar) 89 | return _study 90 | 91 | 92 | if __name__ == "__main__": 93 | logger = logging.getLogger('logger_S') 94 | logger.level = logging.INFO 95 | formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s") 96 | # 97 | fh = logging.handlers.RotatingFileHandler(Path(LOG_PATH, sys.argv[6]), maxBytes=500000, backupCount=5) 98 | fh.setFormatter(formatter) 99 | fh.setLevel(logging.INFO) 100 | logger.addHandler(fh) 101 | # 102 | prm_best = json.loads(sys.argv[5]) 103 | logger.info(f"Previous best params: {prm_best}") 104 | try: 105 | study = optimize( 106 | sys.argv[1], 107 | sys.argv[2], 108 | int(sys.argv[3]), 109 | storage_name=sys.argv[4], 110 | _prm_best=prm_best 111 | ) 112 | except KeyboardInterrupt: 113 | pass # ignore 114 | except Exception as ex: 115 | logger.info(f"optimizer: {ex}") 116 | else: 117 | new_value = round(study.best_value, ndigits=6) 118 | bp = {k: int(any2str(v)) if isinstance(v, int) else float(any2str(v)) for k, v in study.best_params.items()} 119 | 120 | logger.info(f"Optimal parameters: {bp} for get {new_value}") 121 | if new_value: 122 | logger.info(f"Importance parameters: {optuna.importance.get_param_importances(study)}") 123 | 124 | _value = round(study.get_trials()[0].value, ndigits=6) 125 | 126 | if not prm_best or new_value > _value: 127 | bp |= {'new_value': any2str(new_value), '_value': any2str(_value)} 128 | print(json.dumps(bp)) 129 | else: 130 | print(json.dumps({})) 131 | -------------------------------------------------------------------------------- /martin_binance/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | gRPC async client for exchanges-wrapper 3 | """ 4 | __author__ = "Jerry Fedorenko" 5 | __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" 6 | __license__ = "MIT" 7 | __version__ = "3.0.17" 8 | __maintainer__ = "Jerry Fedorenko" 9 | __contact__ = "https://github.com/DogsTailFarmer" 10 | 11 | import asyncio 12 | import random 13 | import logging 14 | 15 | # noinspection PyPackageRequirements 16 | import grpclib.exceptions 17 | import shortuuid 18 | 19 | from exchanges_wrapper import martin as mr, Channel, Status, GRPCError 20 | 21 | logger = logging.getLogger('logger.client') 22 | stream_handler = logging.StreamHandler() 23 | stream_handler.setFormatter(logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")) 24 | stream_handler.setLevel(logging.INFO) 25 | logger.addHandler(stream_handler) 26 | 27 | 28 | class Trade: 29 | def __init__(self, account_name, rate_limiter, symbol): 30 | self.channel = None 31 | self.stub = None 32 | self.account_name = account_name 33 | self.rate_limiter = rate_limiter 34 | self.symbol = symbol 35 | self.client = None 36 | self.wait_connection = False 37 | self.trade_id = shortuuid.uuid() 38 | 39 | async def get_client(self): 40 | if self.wait_connection: 41 | return False 42 | self.wait_connection = True 43 | client = None 44 | while client is None: 45 | if self.channel: 46 | self.channel.close() 47 | self.channel = Channel('127.0.0.1', 50051) 48 | self.stub = mr.MartinStub(self.channel) 49 | try: 50 | client = await self.connect() 51 | except UserWarning as ex: 52 | logger.warning(ex) 53 | client = None 54 | await asyncio.sleep(random.randint(5, 30)) 55 | else: 56 | self.client = client 57 | self.wait_connection = False 58 | return True 59 | 60 | async def connect(self): 61 | try: 62 | _client = await self.stub.open_client_connection( 63 | mr.OpenClientConnectionRequest( 64 | trade_id=self.trade_id, 65 | account_name=self.account_name, 66 | rate_limiter=self.rate_limiter, 67 | symbol=self.symbol 68 | ) 69 | ) 70 | except asyncio.CancelledError: 71 | pass # Task cancellation should not be logged as an error 72 | except ConnectionRefusedError as ex: 73 | raise UserWarning(f"{ex}, reconnect...") from None 74 | except GRPCError as ex: 75 | status_code = ex.status 76 | if status_code == Status.FAILED_PRECONDITION: 77 | raise SystemExit(1) from ex 78 | raise UserWarning(f"Exception on register client: {status_code.name}, {ex.message}") 79 | else: 80 | logger.info(f"gRPC session started for client_id: {_client.client_id}, trade_id: {self.trade_id}") 81 | return _client 82 | 83 | async def send_request(self, _request, _request_type, **kwargs): 84 | if not self.client: 85 | logger.warning("Send gRPC request failed, not active client session, restart") 86 | if not await self.get_client(): 87 | raise UserWarning("Connection to gRPC server in progress...") 88 | kwargs['client_id'] = self.client.client_id 89 | kwargs['trade_id'] = self.trade_id 90 | try: 91 | res = await _request(_request_type(**kwargs)) 92 | except asyncio.CancelledError: 93 | pass # Task cancellation should not be logged as an error 94 | except grpclib.exceptions.StreamTerminatedError: 95 | self.client = None 96 | raise UserWarning("Have not connection to gRPC server") 97 | except ConnectionRefusedError: 98 | self.client = None 99 | raise UserWarning("Connection to gRPC server broken") 100 | except GRPCError as ex: 101 | status_code = ex.status 102 | logger.debug(f"Send request {_request}: {status_code.name}, {ex.message}") 103 | if status_code == Status.UNAVAILABLE: 104 | self.client = None 105 | raise UserWarning("Wait connection to gRPC server") from None 106 | raise 107 | except Exception as ex: 108 | logger.error(f"Exception on send request {ex}") 109 | else: 110 | return res 111 | 112 | async def for_request(self, _request, _request_type, **kwargs): 113 | if not self.client: 114 | raise UserWarning("Start gRPC request loop failed, not active client session") 115 | kwargs['client_id'] = self.client.client_id 116 | kwargs['trade_id'] = self.trade_id 117 | try: 118 | async for res in _request(_request_type(**kwargs)): 119 | yield res 120 | except asyncio.CancelledError: 121 | pass # Task cancellation should not be logged as an error 122 | except grpclib.exceptions.StreamTerminatedError: 123 | pass # handling in send_request() 124 | except GRPCError as ex: 125 | status_code = ex.status 126 | logger.warning(f"Exception on WSS loop: {status_code.name}, {ex.message}") 127 | raise 128 | except Exception as ex: 129 | logger.debug(f"for_request: {ex}") 130 | -------------------------------------------------------------------------------- /martin_binance/db_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions for managing and saving data to a SQLite database from martin-binance strategy 3 | """ 4 | __author__ = "Jerry Fedorenko" 5 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 6 | __license__ = "MIT" 7 | __version__ = "3.0.9" 8 | __maintainer__ = "Jerry Fedorenko" 9 | __contact__ = "https://github.com/DogsTailFarmer" 10 | 11 | import contextlib 12 | import sqlite3 13 | from datetime import datetime, timezone 14 | import logging 15 | 16 | from martin_binance import DB_FILE 17 | 18 | logger = logging.getLogger('logger') 19 | 20 | 21 | def db_management(exchange) -> None: 22 | conn = sqlite3.connect(DB_FILE, check_same_thread=False) 23 | conn.execute("CREATE TABLE IF NOT EXISTS t_orders (\ 24 | id_exchange INTEGER REFERENCES t_exchange (id_exchange)\ 25 | ON DELETE RESTRICT ON UPDATE CASCADE NOT NULL,\ 26 | f_currency TEXT NOT NULL,\ 27 | s_currency TEXT NOT NULL,\ 28 | cycle_buy BOOLEAN NOT NULL,\ 29 | order_buy INTEGER NOT NULL,\ 30 | order_sell INTEGER NOT NULL,\ 31 | order_hold INTEGER NOT NULL,\ 32 | PRIMARY KEY(id_exchange, f_currency, s_currency))") 33 | conn.commit() 34 | # 35 | try: 36 | conn.execute('SELECT active FROM t_funds LIMIT 1') 37 | except sqlite3.Error: 38 | try: 39 | conn.execute('ALTER TABLE t_funds ADD COLUMN active BOOLEAN DEFAULT 0') 40 | conn.commit() 41 | except sqlite3.Error as ex: 42 | logger.error(f"ALTER table t_funds failed: {ex}") 43 | # 44 | cursor = conn.cursor() 45 | # Compliance check t_exchange and EXCHANGE() = exchange() from ms_cfg.toml 46 | cursor.execute("SELECT id_exchange, name FROM t_exchange") 47 | row = cursor.fetchall() 48 | cursor.close() 49 | row_n = len(row) 50 | for i, exch in enumerate(exchange): 51 | if i >= row_n: 52 | logger.info(f"save_to_db: Add exchange {i}, {exch}") 53 | try: 54 | conn.execute("INSERT into t_exchange values(?,?)", (i, exch)) 55 | conn.commit() 56 | except sqlite3.Error as err: 57 | logger.error(f"INSERT into t_exchange: {err}") 58 | conn.close() 59 | 60 | 61 | def save_to_db(queue_to_db) -> None: 62 | connection_analytic = sqlite3.connect(DB_FILE, check_same_thread=False, timeout=10) 63 | # Save data to .db 64 | data = None 65 | result = True 66 | while True: 67 | with contextlib.suppress(KeyboardInterrupt): 68 | if result: 69 | data = queue_to_db.get() 70 | if data is None or data.get('stop_signal'): 71 | break 72 | if data.get('destination') == 't_funds': 73 | # logger.info("save_to_db: Record row into t_funds") 74 | try: 75 | connection_analytic.execute("INSERT INTO t_funds values(\ 76 | ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", 77 | (data.get('ID_EXCHANGE'), 78 | None, 79 | data.get('f_currency'), 80 | data.get('s_currency'), 81 | float(data.get('f_funds')), 82 | float(data.get('s_funds')), 83 | float(data.get('avg_rate')), 84 | data.get('cycle_buy'), 85 | float(data.get('f_depo')), 86 | float(data.get('s_depo')), 87 | float(data.get('f_profit')), 88 | float(data.get('s_profit')), 89 | datetime.now(timezone.utc).replace(tzinfo=None), 90 | float(data.get('PRICE_SHIFT')), 91 | float(data.get('PROFIT')), 92 | float(data.get('over_price')), 93 | data.get('order_q'), 94 | float(data.get('MARTIN')), 95 | data.get('LINEAR_GRID_K'), 96 | data.get('ADAPTIVE_TRADE_CONDITION'), 97 | data.get('KBB'), 98 | 1, 99 | data.get('cycle_time'), 100 | 0)) 101 | connection_analytic.commit() 102 | except sqlite3.Error as err: 103 | result = False 104 | logger.error(f"For save data into t_funds: {err}, retry") 105 | else: 106 | result = True 107 | elif data.get('destination') == 't_orders': 108 | # logger.info("save_to_db: Record row into t_orders") 109 | try: 110 | connection_analytic.execute("INSERT INTO t_orders VALUES(:id_exchange,\ 111 | :f_currency,\ 112 | :s_currency,\ 113 | :cycle_buy,\ 114 | :order_buy,\ 115 | :order_sell,\ 116 | :order_hold)\ 117 | ON CONFLICT(id_exchange, f_currency, s_currency)\ 118 | DO UPDATE SET cycle_buy=:cycle_buy,\ 119 | order_buy=:order_buy,\ 120 | order_sell=:order_sell,\ 121 | order_hold=:order_hold", 122 | {'id_exchange': data.get('ID_EXCHANGE'), 123 | 'f_currency': data.get('f_currency'), 124 | 's_currency': data.get('s_currency'), 125 | 'cycle_buy': data.get('cycle_buy'), 126 | 'order_buy': data.get('order_buy'), 127 | 'order_sell': data.get('order_sell'), 128 | 'order_hold': data.get('order_hold')}) 129 | connection_analytic.commit() 130 | except sqlite3.Error as err: 131 | logger.error(f"INSERT into t_orders: {err}") 132 | connection_analytic.commit() 133 | -------------------------------------------------------------------------------- /martin_binance/lib.py: -------------------------------------------------------------------------------- 1 | """ 2 | martin-binance classes and methods definitions 3 | """ 4 | __author__ = "Jerry Fedorenko" 5 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 6 | __license__ = "MIT" 7 | __version__ = "3.0.29" 8 | __maintainer__ = "Jerry Fedorenko" 9 | __contact__ = "https://github.com/DogsTailFarmer" 10 | 11 | import logging 12 | import time 13 | from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_EVEN 14 | from enum import Enum 15 | from pathlib import Path 16 | 17 | import numpy as np 18 | import ujson as json 19 | from scipy.optimize import minimize 20 | 21 | logger = logging.getLogger('logger') 22 | 23 | O_DEC = Decimal() 24 | 25 | 26 | def any2str(_x) -> str: 27 | return f"{_x:.10f}".rstrip('0').rstrip('.') 28 | 29 | 30 | def f2d(_f: float) -> Decimal: 31 | return Decimal(str(_f)) 32 | 33 | 34 | def solve(fn, value: Decimal, x: Decimal, **kwargs) -> (Decimal, str): 35 | def _fn(_x): 36 | return abs(float(value) - fn(_x, **kwargs)) 37 | res = minimize(_fn, x0=np.array([float(x)]), method='Nelder-Mead') 38 | if res.success: 39 | _res = f2d(res.x[0]) 40 | n = 0 41 | while f2d(fn(_res, **kwargs)) - value < 0: 42 | _res += f2d(0.1) 43 | n += 1 44 | if n > 200: # cycle limit check 45 | return O_DEC, "Number of cycles exceeded" 46 | return _res, f"{res.message} Number of iterations: {res.nit}, correction: +{n*0.1:.2f}" 47 | return O_DEC, res.message 48 | 49 | 50 | def convert_from_minute(m: int) -> str: 51 | intervals = [ 52 | (1, 3, '1m'), 53 | (3, 5, '3m'), 54 | (5, 15, '5m'), 55 | (15, 30, '15m'), 56 | (30, 60, '30m'), 57 | (60, 120, '1h'), 58 | (120, 240, '2h'), 59 | (240, 360, '4h'), 60 | (360, 480, '6h'), 61 | (480, 720, '8h'), 62 | (720, 1440, '12h'), 63 | (1440, 4320, '1d'), 64 | (4320, 10080, '3d'), 65 | (10080, 44640, '1w') 66 | ] 67 | 68 | for start, end, value in intervals: 69 | if start <= m < end: 70 | return value 71 | return '1m' # Default case 72 | 73 | 74 | def load_file(name: Path) -> dict: 75 | _res = {} 76 | if name.exists(): 77 | try: 78 | with name.open() as state_file: 79 | _last_state = json.load(state_file) 80 | except json.JSONDecodeError as er: 81 | print(f"Exception on decode last state file: {er}") 82 | else: 83 | if _last_state.get('ms_start_time_ms', None): 84 | _res = _last_state 85 | return _res 86 | 87 | 88 | def load_last_state(last_state_file) -> dict: 89 | res = {} 90 | if last_state_file.exists(): 91 | res = load_file(last_state_file) 92 | if not res: 93 | print("Can't load last state, try load previous saved state") 94 | res = load_file(last_state_file.with_suffix('.prev')) 95 | if res: 96 | with last_state_file.with_suffix('.bak').open(mode='w') as outfile: 97 | json.dump(res, outfile, sort_keys=True, indent=4, ensure_ascii=False) 98 | return res 99 | 100 | 101 | class Style: 102 | __slots__ = () 103 | 104 | BLACK: str = '\033[30m' 105 | RED: str = '\033[31m' 106 | B_RED: str = '\033[1;31m' 107 | GREEN: str = '\033[32m' 108 | YELLOW: str = '\033[33m' 109 | B_YELLOW: str = "\033[33;1m" 110 | BLUE: str = '\033[34m' 111 | MAGENTA: str = '\033[35m' 112 | CYAN: str = '\033[36m' 113 | GRAY: str = '\033[37m' 114 | WHITE: str = '\033[0;37m' 115 | B_WHITE: str = '\033[1;37m' 116 | UNDERLINE: str = '\033[4m' 117 | RESET: str = '\033[0m' 118 | 119 | @classmethod 120 | def __add__(cls, b): 121 | return Style() + b 122 | 123 | 124 | class Orders: 125 | __slots__ = ("orders_list",) 126 | 127 | def __init__(self): 128 | self.orders_list = [] 129 | 130 | def __iter__(self): 131 | yield from self.orders_list 132 | 133 | def __len__(self): 134 | return len(self.orders_list) 135 | 136 | def append_order(self, _id: int, buy: bool, amount: Decimal, price: Decimal): 137 | self.orders_list.append({'id': _id, 'buy': buy, 'amount': amount, 'price': price}) 138 | 139 | def remove(self, _id: int): 140 | self.orders_list[:] = [i for i in self.orders_list if i['id'] != _id] 141 | 142 | def find_order(self, in_orders: list, place_order_id: int): 143 | """ 144 | Find equal order in_orders[] and self.orders_list[] where in_orders[].id == place_order_id 145 | If exist return order: Order 146 | """ 147 | order = None 148 | for i in self.orders_list: 149 | if i['id'] == place_order_id: 150 | for k, o in enumerate(in_orders): 151 | if o.buy == i['buy'] and o.amount == i['amount'] and o.price == i['price']: 152 | order = in_orders[k] 153 | break 154 | if order: 155 | break 156 | return order 157 | 158 | def get_by_id(self, _id: int) -> dict: 159 | return next((i for i in self.orders_list if i['id'] == _id), None) 160 | 161 | def exist(self, _id: int) -> bool: 162 | return any(i['id'] == _id for i in self.orders_list) 163 | 164 | def get(self) -> list: 165 | """ 166 | Get List of Dict for orders 167 | :return: [] 168 | """ 169 | return self.orders_list 170 | 171 | def get_id_list(self) -> list: 172 | """ 173 | Get List of orders id 174 | :return: [] 175 | """ 176 | return [i['id'] for i in self.orders_list] 177 | 178 | def get_first(self) -> (): 179 | """ 180 | Get first order as tuple 181 | :return: (id, buy, amount, price) 182 | """ 183 | return tuple(self.orders_list[0].values()) 184 | 185 | def get_last(self) -> (): 186 | """ 187 | Get last order as tuple 188 | :return: (id, buy, amount, price) 189 | """ 190 | return tuple(self.orders_list[-1].values()) 191 | 192 | def restore(self, order_list: list): 193 | self.orders_list.clear() 194 | for i in order_list: 195 | i_dec = {'id': i.get('id'), 196 | 'buy': i.get('buy'), 197 | 'amount': f2d(i.get('amount')), 198 | 'price': f2d(i.get('price'))} 199 | self.orders_list.append(i_dec) 200 | 201 | def sort(self, cycle_buy: bool): 202 | if cycle_buy: 203 | self.orders_list.sort(key=lambda x: x['price'], reverse=True) 204 | else: 205 | self.orders_list.sort(key=lambda x: x['price'], reverse=False) 206 | 207 | def sum_amount(self, cycle_buy: bool) -> Decimal: 208 | _sum = O_DEC 209 | for i in self.orders_list: 210 | _sum += i['amount'] * (i['price'] if cycle_buy else 1) 211 | return _sum 212 | 213 | 214 | class PrivateTrade: 215 | __slots__ = ( 216 | "amount", 217 | "buy", 218 | "is_maker", 219 | "id", 220 | "order_id", 221 | "price", 222 | "commission", 223 | "commission_asset", 224 | "timestamp" 225 | ) 226 | 227 | def __init__(self, _trade: dict) -> None: 228 | self.amount = Decimal(_trade["qty"]) 229 | self.buy = _trade.get('isBuyer', False) 230 | self.is_maker = _trade.get('isMaker', False) 231 | self.id = int(_trade["id"]) 232 | self.order_id = int(_trade["orderId"]) 233 | self.price = Decimal(_trade["price"]) 234 | self.commission = Decimal(_trade.get('commission', "0")) 235 | self.commission_asset = _trade.get('commissionAsset', "") 236 | self.timestamp = int(_trade["time"]) 237 | 238 | def __call__(self): 239 | return self 240 | 241 | 242 | class OrderUpdate: 243 | __slots__ = ("original_order", "resulting_trades", "status", "timestamp", "updated_order") 244 | 245 | class Status(Enum): 246 | """ 247 | Update status defining what happened to the order since the last update. 248 | """ 249 | FILLED = 0 250 | ADAPTED = 1 251 | CANCELED = 2 252 | NO_CHANGE = 3 253 | REAPPEARED = 4 254 | DISAPPEARED = 5 255 | OTHER_CHANGE = 6 256 | PARTIALLY_FILLED = 7 257 | ADAPTED_AND_FILLED = 8 258 | 259 | ADAPTED = Status.ADAPTED 260 | ADAPTED_AND_FILLED = Status.ADAPTED_AND_FILLED 261 | CANCELED = Status.CANCELED 262 | DISAPPEARED = Status.DISAPPEARED 263 | FILLED = Status.FILLED 264 | NO_CHANGE = Status.NO_CHANGE 265 | OTHER_CHANGE = Status.OTHER_CHANGE 266 | PARTIALLY_FILLED = Status.PARTIALLY_FILLED 267 | REAPPEARED = Status.REAPPEARED 268 | 269 | def __init__(self, event: dict, trades: list) -> None: 270 | 271 | class OriginalOrder: 272 | __slots__ = ("id",) 273 | 274 | def __init__(self, _event: dict): 275 | self.id = _event['order_id'] 276 | 277 | self.original_order = OriginalOrder(event) 278 | self.resulting_trades = [] 279 | for trade in trades: 280 | if trade.order_id == event['order_id']: 281 | self.resulting_trades.append(trade) 282 | if event['order_status'] == 'FILLED': 283 | self.status = OrderUpdate.FILLED 284 | elif event['order_status'] == 'PARTIALLY_FILLED': 285 | self.status = OrderUpdate.PARTIALLY_FILLED 286 | elif event['order_status'] == 'CANCELED': 287 | self.status = OrderUpdate.CANCELED 288 | else: 289 | self.status = OrderUpdate.OTHER_CHANGE 290 | self.timestamp = event['transaction_time'] 291 | self.updated_order = None 292 | 293 | def __call__(self): 294 | return self 295 | 296 | 297 | class Order: 298 | __slots__ = ("amount", "buy", "id", "order_type", "price", "received_amount", "remaining_amount", "timestamp") 299 | 300 | def __init__(self, order: dict): 301 | self.amount = Decimal(order['origQty']) 302 | self.buy = order['side'] == 'BUY' 303 | self.id = int(order['orderId']) 304 | self.order_type = order['type'] 305 | self.received_amount = Decimal(order['executedQty']) 306 | cummulative_quote_qty = order.get('cummulativeQuoteQty') 307 | if self.received_amount > 0 and cummulative_quote_qty: 308 | self.price = Decimal(cummulative_quote_qty) / self.received_amount 309 | else: 310 | self.price = Decimal(order['price']) 311 | self.remaining_amount = self.amount - self.received_amount 312 | self.timestamp = int(order.get('transactTime', order.get('time', time.time()))) 313 | 314 | def __call__(self): 315 | return self 316 | 317 | 318 | class Candle: 319 | __slots__ = ("min_time", "open", "high", "low", "close", "volume", "max_time", "trade_number", "vwap") 320 | 321 | def __init__(self, _candle: list): 322 | self.min_time = int(_candle[0]) 323 | self.open = float(_candle[1]) 324 | self.high = float(_candle[2]) 325 | self.low = float(_candle[3]) 326 | self.close = float(_candle[4]) 327 | self.volume = float(_candle[5]) 328 | self.max_time = int(_candle[6]) 329 | self.trade_number = int(_candle[8]) 330 | self.vwap = (float(_candle[7]) / self.volume) if self.volume else self.close 331 | 332 | def __call__(self): 333 | return self 334 | 335 | 336 | class TradingCapabilityManager: 337 | __slots__ = ( 338 | "base_asset_precision", 339 | "quote_asset_precision", 340 | "min_qty", 341 | "max_qty", 342 | "step_size", 343 | "min_notional", 344 | "tick_size", 345 | "multiplier_up", 346 | "multiplier_down", 347 | "min_price", 348 | "max_price", 349 | ) 350 | 351 | def __init__(self, _exchange_info_symbol): 352 | self.base_asset_precision = int(_exchange_info_symbol.get('baseAssetPrecision')) 353 | self.quote_asset_precision = int(_exchange_info_symbol.get('quoteAssetPrecision')) 354 | self.min_qty = Decimal(_exchange_info_symbol['filters']['lotSize']['minQty']) 355 | self.max_qty = Decimal(_exchange_info_symbol['filters']['lotSize']['maxQty']) 356 | self.step_size = Decimal(_exchange_info_symbol['filters']['lotSize']['stepSize'].rstrip('0')) 357 | self.min_notional = ( 358 | Decimal(_exchange_info_symbol['filters'].get('notional', {}).get('minNotional', '0')) 359 | or Decimal(_exchange_info_symbol['filters'].get('minNotional', {}).get('minNotional', '0')) 360 | ) 361 | self.tick_size = Decimal(_exchange_info_symbol['filters']['priceFilter']['tickSize'].rstrip('0')) 362 | self.min_price = Decimal(_exchange_info_symbol['filters']['priceFilter']['minPrice']) 363 | self.max_price = Decimal(_exchange_info_symbol['filters']['priceFilter']['maxPrice']) 364 | self.multiplier_up = Decimal(_exchange_info_symbol['filters']['percentPrice']['multiplierUp']) 365 | self.multiplier_down = Decimal(_exchange_info_symbol['filters']['percentPrice']['multiplierDown']) 366 | 367 | def __call__(self): 368 | return self 369 | 370 | def round_amount(self, unrounded_amount: Decimal, rounding_type: str) -> Decimal: 371 | return unrounded_amount.quantize(self.step_size, rounding=rounding_type) 372 | 373 | def round_price(self, unrounded_price: Decimal, rounding_type: str) -> Decimal: 374 | return unrounded_price.quantize(self.tick_size, rounding=rounding_type) 375 | 376 | def get_min_sell_amount(self, price: Decimal) -> Decimal: 377 | return max(self.min_qty, self.round_amount(self.min_notional / price, ROUND_CEILING)) 378 | 379 | def get_max_sell_amount(self, _unused_price: Decimal) -> Decimal: 380 | """ 381 | Returns the maximally possible sell amount that can be placed at a given price. 382 | """ 383 | return self.max_qty 384 | 385 | def get_min_buy_amount(self, price: Decimal) -> Decimal: 386 | return max(self.min_qty, self.round_amount(self.min_notional / price, ROUND_CEILING)) 387 | 388 | def get_minimal_price_change(self) -> Decimal: 389 | return self.tick_size 390 | 391 | def get_minimal_amount_change(self) -> Decimal: 392 | """ 393 | Get the minimal amount change that is possible to use on the exchange. 394 | """ 395 | return self.step_size 396 | 397 | def get_max_sell_price(self, avg_price: Decimal) -> Decimal: 398 | return self.round_price(avg_price * self.multiplier_up, ROUND_FLOOR) 399 | 400 | def get_max_price(self) -> Decimal: 401 | return self.max_price 402 | 403 | def get_min_buy_price(self, avg_price: Decimal) -> Decimal: 404 | return self.round_price(avg_price * self.multiplier_down, ROUND_CEILING) 405 | 406 | def get_min_price(self) -> Decimal: 407 | return self.min_price 408 | 409 | 410 | class Ticker: 411 | __slots__ = ("last_day_price", "last_price", "timestamp") 412 | 413 | def __init__(self, _ticker): 414 | self.last_day_price = Decimal(_ticker['openPrice']) 415 | self.last_price = Decimal(_ticker['lastPrice']) 416 | self.timestamp = int(_ticker['closeTime']) 417 | 418 | def __call__(self): 419 | return self 420 | 421 | 422 | class FundsEntry: 423 | __slots__ = ("available", "reserved", "total_for_currency") 424 | 425 | def __init__(self, _funds): 426 | self.available = Decimal(_funds['free']) 427 | self.reserved = Decimal(_funds['locked']) 428 | self.total_for_currency = self.available + self.reserved 429 | 430 | def __call__(self): 431 | return self 432 | 433 | 434 | class OrderBook: 435 | __slots__ = ("asks", "bids") 436 | 437 | """ 438 | order_book.bids[0].price 439 | order_book.asks[0].amount 440 | """ 441 | 442 | def __init__(self, _order_book, _tcm=None) -> None: 443 | class _OrderBookRow: 444 | __slots__ = ("price", "amount") 445 | 446 | def __init__(self, _order, _tcm=_tcm) -> None: 447 | self.price = Decimal(_order[0]) 448 | self.amount = Decimal(_order[1]) 449 | if _tcm: 450 | self.price = _tcm.round_price(self.price, ROUND_HALF_EVEN) 451 | self.amount = _tcm.round_amount(self.amount, ROUND_HALF_EVEN) 452 | 453 | self.asks = [] 454 | self.bids = [] 455 | self.asks.extend(_OrderBookRow(v) for v in _order_book['asks']) 456 | self.bids.extend(_OrderBookRow(v) for v in _order_book['bids']) 457 | 458 | def __call__(self): 459 | return self 460 | 461 | 462 | class Klines: 463 | klines_series = {} 464 | klines_lim = int() 465 | 466 | def __init__(self, _interval): 467 | self.interval = _interval 468 | self.kline = [] 469 | self.klines_series[_interval] = self.kline 470 | 471 | def refresh(self, _candle): 472 | candle = Candle(_candle) 473 | new_time = candle.min_time 474 | last_time = self.kline[-1].min_time if self.kline else 0 475 | if new_time >= last_time: 476 | if new_time == last_time: 477 | self.kline[-1] = candle 478 | else: 479 | self.kline.append(candle) 480 | if len(self.kline) > self.klines_lim: 481 | del self.kline[0] 482 | self.klines_series[self.interval] = self.kline 483 | 484 | @classmethod 485 | def get_kline(cls, _interval) -> list: 486 | return cls.klines_series.get(_interval, []) 487 | -------------------------------------------------------------------------------- /martin_binance/params.py: -------------------------------------------------------------------------------- 1 | """ 2 | martin-binance strategy parameters 3 | """ 4 | __author__ = "Jerry Fedorenko" 5 | __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" 6 | __license__ = "MIT" 7 | __version__ = "3.0.20" 8 | __maintainer__ = "Jerry Fedorenko" 9 | __contact__ = "https://github.com/DogsTailFarmer" 10 | 11 | import logging 12 | from decimal import Decimal 13 | from pathlib import Path 14 | 15 | __all__ = [ 16 | 'SYMBOL', 'EXCHANGE', 'ID_EXCHANGE', 'FEE_MAKER', 'FEE_TAKER', 'FEE_FIRST', 'FEE_SECOND', 'FEE_BNB', 17 | 'SAVE_ASSET', 'GRID_MAX_COUNT', 'START_ON_BUY', 'AMOUNT_FIRST', 'USE_ALL_FUND', 'AMOUNT_SECOND', 18 | 'PRICE_SHIFT', 'ROUND_BASE', 'ROUND_QUOTE', 'PROFIT', 'PROFIT_MAX', 'OVER_PRICE', 'ORDER_Q', 19 | 'MARTIN', 'SHIFT_GRID_DELAY', 'GRID_UPDATE_INTERVAL', 'STATUS_DELAY', 'GRID_ONLY', 'LOG_LEVEL', 20 | 'HOLD_TP_ORDER_TIMEOUT', 'COLLECT_ASSETS', 'GRID_ONLY_DELAY', 'TP_REFRESH', 'ADAPTIVE_TRADE_CONDITION', 21 | 'BB_CANDLE_SIZE_IN_MINUTES', 'BB_NUMBER_OF_CANDLES', 'KBB', 'LINEAR_GRID_K', 'ADX_CANDLE_SIZE_IN_MINUTES', 22 | 'ADX_NUMBER_OF_CANDLES', 'ADX_PERIOD', 'ADX_THRESHOLD', 'ADX_PRICE_THRESHOLD', 'REVERSE', 'REVERSE_TARGET_AMOUNT', 23 | 'REVERSE_INIT_AMOUNT', 'REVERSE_STOP', 'HEAD_VERSION', 'LOAD_LAST_STATE', 'LAST_STATE_FILE', 'VPS_NAME', 'PARAMS', 24 | 'TELEGRAM_CONFIG', 'MODE', 'XTIME', 'SAVE_DS', 'SAVE_PERIOD', 'LOGGING', 'SELF_OPTIMIZATION', 'N_TRIALS', 25 | 'SESSION_RESULT' 26 | ] 27 | 28 | SYMBOL = str() 29 | EXCHANGE = () 30 | # Exchange setup 31 | ID_EXCHANGE = int() 32 | FEE_MAKER = Decimal() 33 | FEE_TAKER = Decimal() 34 | FEE_FIRST = False 35 | FEE_SECOND = False 36 | FEE_BNB = { 37 | 'id_exchange': 0, 38 | 'symbol': 'BNB/USDT', 39 | 'email': str(), 40 | 'target_amount': '0', 41 | 'tranche_volume': '0' 42 | } 43 | SAVE_ASSET = True 44 | GRID_MAX_COUNT = int() 45 | # Trade parameter 46 | START_ON_BUY = bool() 47 | AMOUNT_FIRST = Decimal() 48 | USE_ALL_FUND = bool() 49 | AMOUNT_SECOND = Decimal() 50 | PRICE_SHIFT = Decimal() 51 | # Round pattern 52 | ROUND_BASE = str() 53 | ROUND_QUOTE = str() 54 | # 55 | PROFIT = Decimal() 56 | PROFIT_MAX = Decimal() 57 | OVER_PRICE = Decimal() 58 | ORDER_Q = int() 59 | MARTIN = Decimal() 60 | SHIFT_GRID_DELAY = int() 61 | GRID_UPDATE_INTERVAL = 60 * 60 # sec between grid update in Reverse cycle 62 | # Other 63 | STATUS_DELAY = int() 64 | GRID_ONLY = bool() 65 | LOG_LEVEL = logging.DEBUG # Default level for console output 66 | HOLD_TP_ORDER_TIMEOUT = 30 67 | COLLECT_ASSETS = bool() 68 | GRID_ONLY_DELAY = 150 # sec delay before try restart GRID_ONLY cycle 69 | TP_REFRESH = 60 * 10 # sec between TP refresh 70 | # 71 | ADAPTIVE_TRADE_CONDITION = bool() 72 | BB_CANDLE_SIZE_IN_MINUTES = int() 73 | BB_NUMBER_OF_CANDLES = int() 74 | KBB = float() 75 | # 76 | LINEAR_GRID_K = int() 77 | # 78 | ADX_CANDLE_SIZE_IN_MINUTES = int() 79 | ADX_NUMBER_OF_CANDLES = int() 80 | ADX_PERIOD = int() 81 | ADX_THRESHOLD = int() 82 | ADX_PRICE_THRESHOLD = float() 83 | # Reverse cycle 84 | REVERSE = bool() 85 | REVERSE_TARGET_AMOUNT = Decimal() 86 | REVERSE_INIT_AMOUNT = Decimal() 87 | REVERSE_STOP = bool() 88 | # Config variables 89 | HEAD_VERSION = str() 90 | LOAD_LAST_STATE = int() 91 | # Path and files name 92 | LAST_STATE_FILE: Path 93 | VPS_NAME = str() 94 | PARAMS: Path 95 | # Telegram 96 | TLG_SERVICE = False 97 | TELEGRAM_CONFIG = {} 98 | TLG_INLINE = False 99 | # Backtesting 100 | MODE = 'T' # 'T' - Trade, 'TC' - Trade and Collect, 'S' - Simulate 101 | XTIME = 1000 # Time accelerator 102 | SAVE_DS = False # Save session result data (ticker, orders) for compare 103 | SAVE_PERIOD = 1 * 60 * 60 # sec, timetable for save data portion 104 | LOGGING = True 105 | SELF_OPTIMIZATION = True # Cyclic self-optimization of parameters, together with MODE == 'TC' 106 | N_TRIALS = 250 # Number of optimization cycles for optuna study 107 | SESSION_RESULT = {} 108 | -------------------------------------------------------------------------------- /martin_binance/service/.tmux.conf: -------------------------------------------------------------------------------- 1 | set -g @plugin 'tmux-plugins/tpm' 2 | set -g @plugin 'tmux-plugins/tmux-sensible' 3 | set -g @plugin 'tmux-plugins/tmux-resurrect' 4 | set -g @plugin 'tmux-plugins/tmux-continuum' 5 | set -g @continuum-restore 'on' 6 | set -g @continuum-save-interval '15' 7 | set -g @continuum-boot 'on' 8 | set -g status-right 'Status: #{continuum_status}' 9 | set -g assume-paste-time 1 10 | set -g base-index 0 11 | set -g bell-action none 12 | set -g history-file ~/.tmux_history 13 | set -g mouse on 14 | set -g prefix C-b 15 | run '~/.tmux/plugins/tpm/tpm' 16 | -------------------------------------------------------------------------------- /martin_binance/service/funds_export.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Crypto Exporter 3 | After=syslog.target 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | User=ubuntu 9 | Group=sudo 10 | WorkingDirectory=/home/ubuntu/.local/lib/python3.8/site-packages/martin_binance/service/ 11 | ExecStart=python3 funds_rate_exporter.py 12 | RestartSec=10 13 | Restart=always 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /martin_binance/service/funds_rate_exporter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | #################################################################### 4 | # Config for prometheus_client 5 | # See README.md for detail 6 | #################################################################### 7 | __author__ = "Jerry Fedorenko" 8 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 9 | __license__ = "MIT" 10 | __version__ = "3.0.23" 11 | __maintainer__ = "Jerry Fedorenko" 12 | __contact__ = 'https://github.com/DogsTailFarmer' 13 | 14 | import os 15 | import time 16 | import sqlite3 17 | import psutil 18 | from requests import Session 19 | import toml 20 | import platform 21 | from prometheus_client import start_http_server, Gauge 22 | from concurrent.futures import ThreadPoolExecutor 23 | 24 | from martin_binance import Path, CONFIG_FILE, DB_FILE 25 | from exchanges_wrapper import CONFIG_FILE as SRV_CONFIG_FILE 26 | 27 | # region Import parameters 28 | 29 | if not CONFIG_FILE.exists(): 30 | if platform.system() == 'Darwin': 31 | user = (lambda: os.environ["USERNAME"] if "C:" in os.getcwd() else os.environ["USER"])() 32 | WORK_PATH = Path("Users", user, ".margin") 33 | else: 34 | WORK_PATH = Path().resolve() 35 | CONFIG_FILE = Path(WORK_PATH, "ms_cfg.toml") 36 | SRV_CONFIG_FILE = Path(WORK_PATH, "exch_srv_cfg.toml") 37 | DB_FILE = Path(WORK_PATH, "funds_rate.db") 38 | 39 | config = toml.load(str(CONFIG_FILE)).get('Exporter') 40 | accounts = toml.load(str(SRV_CONFIG_FILE)).get('accounts') 41 | 42 | names = {acc['name']: acc['exchange'] for acc in accounts} 43 | # external port for prometheus 44 | PORT = config.get('port') 45 | 46 | # sec delay for .db polling 47 | SLEEP_TIME_S = config.get('sleep_time_s') 48 | 49 | # Server name 50 | VPS_NAME = config.get('vps_name') 51 | 52 | # CoinMarketCap 53 | URL = config.get('url') 54 | API = config.get('api') 55 | request_delay = 60 / config.get('rate_limit') 56 | # endregion 57 | 58 | GET_RATE = True # For develop purpose can skip getting asset rate from coinmarketcap 59 | CURRENCY_RATE_LAST_TIME = int(time.time()) 60 | 61 | try: 62 | SQL_CONN = sqlite3.connect(DB_FILE, check_same_thread=False, timeout=10) 63 | except sqlite3.Error as error: 64 | SQL_CONN = None 65 | print("SQLite error:", error) 66 | 67 | # region Metric declare 68 | STATUS_ALARM = Gauge("margin_alarm", "1 when not order", ['exchange', 'pair', 'vps_name']) 69 | REQUEST_DELAY_G = Gauge("request_delay_g", "request delay in sec", ['vps_name']) 70 | 71 | SUM_F_PROFIT = Gauge("margin_f_profit", "first profit", ['exchange', 'pair', 'vps_name']) 72 | SUM_S_PROFIT = Gauge("margin_s_profit", "second profit", ['exchange', 'pair', 'vps_name']) 73 | LAST_RATE = Gauge("margin_last_rate", "pair last rate", ['exchange', 'pair', 'vps_name']) 74 | LAST_RATE_USD = Gauge("margin_last_rate_usd", "last rate second coin to USD", ['exchange', 'pair', 'vps_name']) 75 | SUM_PROFIT = Gauge("margin_sum_profit", "sum profit on last rate", ['exchange', 'pair', 'vps_name']) 76 | SUM_PROFIT_USD = Gauge("margin_sum_profit_usd", "sum profit on last rate on USD", ['exchange', 'pair', 'vps_name']) 77 | 78 | CYCLE_COUNT = Gauge("margin_cycle_count", "cycle count", ['exchange', 'pair', 'vps_name']) 79 | BUY_COUNT = Gauge("margin_buy_count", "cycle buy count", ['exchange', 'pair', 'vps_name']) 80 | SELL_COUNT = Gauge("margin_sell_count", "cycle sell count", ['exchange', 'pair', 'vps_name']) 81 | 82 | BUY_TIME = Gauge("margin_buy_time", "cycle buy time", ['exchange', 'pair', 'vps_name']) 83 | SELL_TIME = Gauge("margin_sell_time", "cycle sell time", ['exchange', 'pair', 'vps_name']) 84 | BUY_INTEREST = Gauge("margin_buy_interest", "sum buy interest", ['exchange', 'pair', 'vps_name']) 85 | SELL_INTEREST = Gauge("margin_sell_interest", "sum sell interest", ['exchange', 'pair', 'vps_name']) 86 | 87 | F_BALANCE = Gauge("margin_f_balance", "first balance amount", ['exchange', 'pair', 'vps_name']) 88 | S_BALANCE = Gauge("margin_s_balance", "second balance amount", ['exchange', 'pair', 'vps_name']) 89 | TOTAL_BALANCE = Gauge("margin_balance", "total balance amount by last rate", ['exchange', 'pair', 'vps_name']) 90 | BALANCE_USD = Gauge("margin_balance_usd", "balance amount in USD", ['name', 'exchange', 'currency', 'vps_name']) 91 | 92 | # Cycle parameters 93 | CYCLE_BUY = Gauge("margin_cycle_buy", "cycle buy", ['exchange', 'pair', 'vps_name']) 94 | OVER_PRICE = Gauge("margin_over_price", "over price", ['exchange', 'pair', 'vps_name']) 95 | F_DEPO = Gauge("margin_f_depo", "first depo", ['exchange', 'pair', 'vps_name']) 96 | S_DEPO = Gauge("margin_s_depo", "second depo", ['exchange', 'pair', 'vps_name']) 97 | 98 | # VPS control 99 | VPS_CPU = Gauge("margin_vps_cpu", "average cpu load", ['vps_name']) 100 | VPS_MEMORY = Gauge("margin_vps_memory", "average memory use in %", ['vps_name']) 101 | 102 | SET_ACTIVE = Gauge("margin_set_active", "mark rows as loaded and reset some metrics", ['vps_name']) 103 | SET_ACTIVE.labels(VPS_NAME).set_function(lambda: set_active()) # skipcq: PYL-W0108 104 | 105 | ''' Cycle parameters for future use 106 | PRICE_SHIFT = Gauge("margin_price_shift", "price shift", ['exchange', 'pair']) 107 | PROFIT = Gauge("margin_profit", "profit", ['exchange', 'pair']) 108 | ORDER_Q = Gauge("margin_order_q", "order_q", ['exchange', 'pair']) 109 | MARTIN = Gauge("margin_martin", "martin", ['exchange', 'pair']) 110 | LINEAR_GRID_K = Gauge("margin_linear_grid_k", "linear_grid_k", ['exchange', 'pair']) 111 | ADAPTIVE_TRADE_CONDITION = Gauge("margin_adaptive_trade_condition", "adaptive_trade_condition", ['exchange', 'pair']) 112 | KB = Gauge("margin_kb", "bollinger band k bottom", ['exchange', 'pair']) 113 | KT = Gauge("margin_kt", "bollinger band k top", ['exchange', 'pair']) 114 | ''' 115 | # endregion 116 | 117 | 118 | def set_active(): 119 | if PREPARED: 120 | try: 121 | SQL_CONN.execute('UPDATE t_funds SET active = 1 WHERE active = 0') 122 | SQL_CONN.commit() 123 | except sqlite3.Error as ex: 124 | print(f"Update t_funds failed: {ex}") 125 | return 0.0 126 | with ThreadPoolExecutor(max_workers=2) as executor: 127 | executor.submit(reset) 128 | return 1.0 129 | 130 | 131 | # noinspection PyProtectedMember 132 | def reset(): 133 | time.sleep(5) 134 | metrics = [BUY_TIME, BUY_INTEREST, SELL_TIME, SELL_INTEREST] 135 | for metric in metrics: 136 | # noinspection PyUnresolvedReferences 137 | label_names = metric._labelnames 138 | label_values = metric._metrics.keys() 139 | for label_value in label_values: 140 | labels = dict(zip(label_names, label_value)) 141 | metric.labels(**labels).set(0.0) 142 | 143 | 144 | def get_rate(_currency_rate) -> {}: 145 | global request_delay 146 | replace = { 147 | 'UST': 'USDT', 148 | 'IOT': 'MIOTA', 149 | 'TESTUSDT': 'USDT', 150 | 'TESTBTC': 'BTC' 151 | } 152 | headers = {'Accepts': 'application/json', 'X-CMC_PRO_API_KEY': API} 153 | session = Session() 154 | session.headers.update(headers) 155 | buffer_rate = {} 156 | 157 | for currency in _currency_rate: 158 | _currency = replace.get(currency, currency) 159 | price = buffer_rate.get(_currency) 160 | if price is None and GET_RATE: 161 | price = -1 162 | parameters = {'amount': 1, 'symbol': 'USD', 'convert': _currency} 163 | try: 164 | response = session.get(URL, params=parameters) 165 | except Exception as er: 166 | print(er) 167 | else: 168 | if response.status_code == 429: 169 | time.sleep(61) 170 | request_delay *= 1.5 171 | try: 172 | response = session.get(URL, params=parameters) 173 | except Exception as er: 174 | print(er) 175 | if response.status_code == 200: 176 | data = response.json() 177 | price = data['data'][0]['quote'][_currency]['price'] or -1 178 | buffer_rate[_currency] = price 179 | _currency_rate[currency] = price if GET_RATE else 1 180 | return _currency_rate 181 | 182 | 183 | def db_handler(sql_conn, _currency_rate, currency_rate_last_time): 184 | global PREPARED 185 | PREPARED = False 186 | global request_delay 187 | cursor = sql_conn.cursor() 188 | # Aggregate score for pair on exchange 189 | cursor.execute('SELECT tex.name, tf.id_exchange,\ 190 | tf.f_currency, tf.s_currency,\ 191 | count(*) as cycle_count,\ 192 | sum(f_profit) as sum_f_profit,\ 193 | sum(s_profit) as sum_s_profit\ 194 | FROM t_funds as tf LEFT JOIN t_exchange tex USING(id_exchange)\ 195 | GROUP BY tex.name, tf.id_exchange, tf.f_currency, tf.s_currency') 196 | records = cursor.fetchall() 197 | # Get assets 198 | cursor.execute('SELECT tex.id_exchange, tex.name, ta.currency, ta.value\ 199 | FROM t_asset as ta LEFT JOIN t_exchange tex USING(id_exchange)\ 200 | WHERE ta.value > 0') 201 | assets = cursor.fetchall() 202 | # Create dict of used currencies 203 | for asset in assets: 204 | _currency_rate.setdefault(asset[2]) 205 | for row in records: 206 | _currency_rate.setdefault(str(row[2])) 207 | _currency_rate.setdefault(str(row[3])) 208 | # Get currency rate for all currency from CoinMarketCap in relation to USD 209 | time_for_refresh = time.time() - currency_rate_last_time > 86400 210 | if None in _currency_rate.values() or time_for_refresh: 211 | get_rate(_currency_rate) 212 | currency_rate_last_time = int(time.time()) 213 | REQUEST_DELAY_G.labels(VPS_NAME).set(request_delay) 214 | if request_delay > 60: 215 | request_delay = 60 / config.get('rate_limit') 216 | # 217 | F_BALANCE.clear() 218 | S_BALANCE.clear() 219 | TOTAL_BALANCE.clear() 220 | BALANCE_USD.clear() 221 | CYCLE_BUY.clear() 222 | F_DEPO.clear() 223 | S_DEPO.clear() 224 | OVER_PRICE.clear() 225 | BUY_TIME.clear() 226 | BUY_INTEREST.clear() 227 | SELL_TIME.clear() 228 | SELL_INTEREST.clear() 229 | # 230 | for row in records: 231 | # print(f"row: {row}") 232 | exchange = str(row[0]) 233 | id_exchange = int(row[1]) 234 | f_currency = str(row[2]) 235 | s_currency = str(row[3]) 236 | pair = f"{f_currency}/{s_currency}" 237 | CYCLE_COUNT.labels(exchange, pair, VPS_NAME).set(int(row[4])) 238 | sum_f_profit = float(row[5]) 239 | SUM_F_PROFIT.labels(exchange, pair, VPS_NAME).set(sum_f_profit) 240 | sum_s_profit = float(row[6]) 241 | SUM_S_PROFIT.labels(exchange, pair, VPS_NAME).set(sum_s_profit) 242 | # Alarm 243 | cursor.execute('SELECT order_buy, order_sell\ 244 | FROM t_orders\ 245 | WHERE id_exchange=:id_exchange\ 246 | AND f_currency=:f_currency\ 247 | AND s_currency=:s_currency', 248 | {'id_exchange': id_exchange, 'f_currency': f_currency, 's_currency': s_currency}) 249 | status_alarm = cursor.fetchone() 250 | alarm = 0 251 | if status_alarm: 252 | order_buy = int(status_alarm[0]) 253 | order_sell = int(status_alarm[1]) 254 | alarm = 0 if order_buy and order_sell else 1 255 | STATUS_ALARM.labels(exchange, pair, VPS_NAME).set(alarm) 256 | # Last rate 257 | cursor.execute('SELECT rate\ 258 | FROM t_funds\ 259 | WHERE id_exchange=:id_exchange\ 260 | AND f_currency=:f_currency\ 261 | AND s_currency=:s_currency\ 262 | ORDER BY id DESC LIMIT 1', 263 | {'id_exchange': id_exchange, 'f_currency': f_currency, 's_currency': s_currency}) 264 | if last_rate_row := cursor.fetchone(): 265 | last_rate = float(last_rate_row[0]) 266 | LAST_RATE.labels(exchange, pair, VPS_NAME).set(last_rate) 267 | else: 268 | last_rate = 0.0 269 | # Sum profit 270 | sum_profit = sum_f_profit * last_rate + sum_s_profit 271 | SUM_PROFIT.labels(exchange, pair, VPS_NAME).set(sum_profit) 272 | # Convert sum profit to USD by last rate 273 | sum_profit_usd = -1 274 | if _currency_rate.get(s_currency): 275 | try: 276 | sum_profit_usd = sum_profit / _currency_rate[s_currency] 277 | LAST_RATE_USD.labels(exchange, pair, VPS_NAME).set(_currency_rate[s_currency]) 278 | except ZeroDivisionError: 279 | sum_profit_usd = -1 280 | SUM_PROFIT_USD.labels(exchange, pair, VPS_NAME).set(sum_profit_usd) 281 | 282 | # Cycle count, calculated by each buy and sell cycle 283 | cursor.execute('SELECT count(*)\ 284 | FROM t_funds\ 285 | WHERE id_exchange=:id_exchange\ 286 | AND f_currency=:f_currency\ 287 | AND s_currency=:s_currency\ 288 | AND cycle_buy = 1', 289 | {'id_exchange': id_exchange, 'f_currency': f_currency, 's_currency': s_currency}) 290 | cycle_buy_row = cursor.fetchone() 291 | cycle_buy_count = int(cycle_buy_row[0]) if cycle_buy_row[0] else 0 292 | cursor.execute('SELECT count(*)\ 293 | FROM t_funds\ 294 | WHERE id_exchange=:id_exchange\ 295 | AND f_currency=:f_currency\ 296 | AND s_currency=:s_currency\ 297 | AND cycle_buy = 0', 298 | {'id_exchange': id_exchange, 'f_currency': f_currency, 's_currency': s_currency}) 299 | cycle_sell_row = cursor.fetchone() 300 | cycle_sell_count = int(cycle_sell_row[0]) if cycle_sell_row[0] else 0 301 | BUY_COUNT.labels(exchange, pair, VPS_NAME).set(cycle_buy_count) 302 | SELL_COUNT.labels(exchange, pair, VPS_NAME).set(cycle_sell_count) 303 | 304 | # Sum income interest, calculated by each buy and sell cycle 305 | cursor.execute('SELECT sum(100 * s_profit / s_depo), sum(cycle_time)\ 306 | FROM t_funds\ 307 | WHERE id_exchange=:id_exchange\ 308 | AND f_currency=:f_currency\ 309 | AND s_currency=:s_currency\ 310 | AND cycle_buy = 1\ 311 | AND active = 0', 312 | {'id_exchange': id_exchange, 'f_currency': f_currency, 's_currency': s_currency}) 313 | cycle_buy_row = cursor.fetchone() 314 | cycle_buy_interest = float(cycle_buy_row[0]) if cycle_buy_row[0] else 0.0 315 | cycle_buy_time = float(cycle_buy_row[1]) if cycle_buy_row[1] else 0.0 316 | 317 | cursor.execute('SELECT sum(100 * f_profit / f_depo), sum(cycle_time)\ 318 | FROM t_funds\ 319 | WHERE id_exchange=:id_exchange\ 320 | AND f_currency=:f_currency\ 321 | AND s_currency=:s_currency\ 322 | AND cycle_buy = 0\ 323 | AND active = 0', 324 | {'id_exchange': id_exchange, 'f_currency': f_currency, 's_currency': s_currency}) 325 | cycle_sell_row = cursor.fetchone() 326 | cycle_sell_interest = float(cycle_sell_row[0]) if cycle_sell_row[0] else 0.0 327 | cycle_sell_time = float(cycle_sell_row[1]) if cycle_sell_row[1] else 0.0 328 | 329 | BUY_TIME.labels(exchange, pair, VPS_NAME).set(cycle_buy_time) 330 | BUY_INTEREST.labels(exchange, pair, VPS_NAME).set(cycle_buy_interest) 331 | SELL_TIME.labels(exchange, pair, VPS_NAME).set(cycle_sell_time) 332 | SELL_INTEREST.labels(exchange, pair, VPS_NAME).set(cycle_sell_interest) 333 | 334 | # Balance amount 335 | cursor.execute('SELECT f_balance, s_balance, cycle_buy, f_depo, s_depo, over_price\ 336 | FROM t_funds\ 337 | WHERE id_exchange=:id_exchange\ 338 | AND f_currency=:f_currency\ 339 | AND s_currency=:s_currency\ 340 | ORDER BY id DESC LIMIT 1', 341 | {'id_exchange': id_exchange, 'f_currency': f_currency, 's_currency': s_currency}) 342 | if balance_row := cursor.fetchone(): 343 | f_balance = balance_row[0] 344 | s_balance = balance_row[1] 345 | balance = f_balance * last_rate + s_balance 346 | F_BALANCE.labels(exchange, pair, VPS_NAME).set(f_balance) 347 | S_BALANCE.labels(exchange, pair, VPS_NAME).set(s_balance) 348 | TOTAL_BALANCE.labels(exchange, pair, VPS_NAME).set(balance) 349 | # Cycle parameters 350 | CYCLE_BUY.labels(exchange, pair, VPS_NAME).set(balance_row[2]) 351 | F_DEPO.labels(exchange, pair, VPS_NAME).set(balance_row[3]) 352 | S_DEPO.labels(exchange, pair, VPS_NAME).set(balance_row[4]) 353 | OVER_PRICE.labels(exchange, pair, VPS_NAME).set(balance_row[5]) 354 | 355 | for asset in assets: 356 | if _rate := _currency_rate.get(asset[2]): 357 | try: 358 | usd_amount = asset[3] / _rate 359 | except ZeroDivisionError: 360 | usd_amount = -1 361 | if usd_amount >= 1.0: 362 | BALANCE_USD.labels(names.get(asset[1]), asset[1], asset[2], VPS_NAME).set(usd_amount) 363 | 364 | cursor.close() 365 | return currency_rate_last_time 366 | 367 | 368 | if __name__ == '__main__': 369 | # Start up the server to expose the metrics. 370 | currency_rate = {} 371 | start_http_server(PORT, addr='::') 372 | while True: 373 | try: 374 | CURRENCY_RATE_LAST_TIME = db_handler(SQL_CONN, currency_rate, CURRENCY_RATE_LAST_TIME) 375 | except sqlite3.Error as error: 376 | print("DB operational error:", error) 377 | else: 378 | PREPARED = True 379 | VPS_CPU.labels(VPS_NAME).set(100 * psutil.getloadavg()[0] / psutil.cpu_count()) 380 | # 381 | memory = psutil.virtual_memory() 382 | swap = psutil.swap_memory() 383 | total_used_percent = 100 * float(swap.used + memory.used) / (swap.total + memory.total) 384 | VPS_MEMORY.labels(VPS_NAME).set(total_used_percent) 385 | # 386 | time.sleep(SLEEP_TIME_S) 387 | -------------------------------------------------------------------------------- /martin_binance/service/relaunch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | #################################################################### 4 | # Restart trade sessions saved in /last_state 5 | ################################################################## 6 | __author__ = "Jerry Fedorenko" 7 | __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" 8 | __license__ = "MIT" 9 | __version__ = "2.1.0" 10 | __maintainer__ = "Jerry Fedorenko" 11 | __contact__ = 'https://github.com/DogsTailFarmer' 12 | ################################################################## 13 | 14 | import libtmux 15 | import time 16 | from martin_binance import Path, WORK_PATH, LAST_STATE_PATH 17 | 18 | server = libtmux.Server() 19 | 20 | while not server.has_session("Trade") or not server.windows.filter(name='srv'): 21 | time.sleep(1) 22 | 23 | time.sleep(1) 24 | session = server.sessions.get(session_name="Trade") 25 | 26 | for window in session.windows: 27 | if window.name == 'srv': 28 | window.attached_pane.send_keys('exchanges-wrapper-srv', enter=True) 29 | else: 30 | last_state = Path(LAST_STATE_PATH, f"{window.name.replace('-', '_').replace('/', '')}.json") 31 | pair = Path(WORK_PATH, f"cli_{window.name.replace('-', '_').replace('/', '')}.py") 32 | if pair.exists() and last_state.exists(): 33 | window.attached_pane.send_keys(f"{pair} 1", enter=True) 34 | time.sleep(4) 35 | -------------------------------------------------------------------------------- /martin_binance/service/relaunch.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=restart martin-binance 3 | After=syslog.target 4 | After=network.target 5 | 6 | [Service] 7 | User=ubuntu 8 | Group=sudo 9 | WorkingDirectory=/home/ubuntu/.local/lib/python3.8/site-packages/martin_binance/service/ 10 | ExecStart=python3 relaunch.py 11 | 12 | [Install] 13 | WantedBy=default.target 14 | -------------------------------------------------------------------------------- /martin_binance/service/tlg-proxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=restart tlg-proxy service 3 | After=syslog.target 4 | After=network.target 5 | 6 | [Service] 7 | User=ubuntu 8 | Group=sudo 9 | WorkingDirectory=/home/ubuntu/.local/lib/python3.10/site-packages/martin_binance/telegram_proxy/ 10 | ExecStart=python3 tlg_proxy.py 11 | 12 | [Install] 13 | WantedBy=default.target 14 | -------------------------------------------------------------------------------- /martin_binance/telegram_proxy/tlg_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This module contains Telegram proxy client implementation with secure SSL context. 5 | It provides high-level abstraction for communication with Telegram proxy service. 6 | 7 | Generate ssl certificates 8 | cd ~/.MartinBinance/keys 9 | 10 | Proxy Service pair 11 | 12 | openssl req -x509 -days 365 -newkey rsa:2048 -nodes -subj '/CN=localhost' --addext 'subjectAltName=IP:aaa.bbb.ccc.ddd'\ 13 | -keyout tlg-proxy.key -out tlg-proxy.pem 14 | 15 | Client pair 16 | 17 | openssl req -x509 -days 365 -newkey rsa:2048 -nodes -subj '/CN=localhost' -keyout tlg-client.key -out tlg-client.pem 18 | """ 19 | __author__ = "Jerry Fedorenko" 20 | __copyright__ = "Copyright © 2025 Jerry Fedorenko aka VM" 21 | __license__ = "MIT" 22 | __version__ = "3.0.26" 23 | __maintainer__ = "Jerry Fedorenko" 24 | __contact__ = "https://github.com/DogsTailFarmer" 25 | 26 | import ssl 27 | from pathlib import Path 28 | import asyncio 29 | from typing import Any 30 | import ujson as json 31 | import toml 32 | import random 33 | import logging.handlers 34 | 35 | import martin_binance.tlg as tlg 36 | from martin_binance import LOG_FILE_TLG, CONFIG_FILE, CERT_DIR 37 | 38 | from exchanges_wrapper import Channel, exceptions 39 | # 40 | logger = logging.getLogger('tlg_client') 41 | logger.setLevel(logging.INFO) 42 | formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s") 43 | # 44 | fh = logging.handlers.RotatingFileHandler(LOG_FILE_TLG, maxBytes=1000000, backupCount=10) 45 | fh.setFormatter(formatter) 46 | logger.addHandler(fh) 47 | fh.setLevel(logging.DEBUG) 48 | # 49 | sh = logging.StreamHandler() 50 | sh.setFormatter(formatter) 51 | logger.addHandler(sh) 52 | sh.setLevel(logging.INFO) 53 | # 54 | config = toml.load(str(CONFIG_FILE))['Telegram'] 55 | TLG_PROXY_HOST = config['tlg_proxy_host'] 56 | TLG_PROXY_PORT = config['tlg_proxy_port'] 57 | SERVER_CERT = Path(CERT_DIR, "tlg-proxy.pem") 58 | CLIENT_CERT = Path(CERT_DIR, "tlg-client.pem") 59 | CLIENT_KEY = Path(CERT_DIR, "tlg-client.key") 60 | 61 | 62 | def create_secure_context(client_cert: Path, client_key: Path, *, trusted: Path) -> ssl.SSLContext: 63 | ctx = ssl.create_default_context(cafile=str(trusted)) 64 | ctx.minimum_version = ssl.TLSVersion.TLSv1_2 65 | ctx.load_cert_chain(str(client_cert), str(client_key)) 66 | ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20') 67 | ctx.set_alpn_protocols(['h2']) 68 | return ctx 69 | 70 | 71 | SSL_CONTEXT = create_secure_context(CLIENT_CERT, CLIENT_KEY, trusted=SERVER_CERT) 72 | 73 | 74 | class TlgClient: 75 | def __init__(self, bot_id, token, chat_id): 76 | self.bot_id = bot_id 77 | self.token = token 78 | self.chat_id = chat_id 79 | # 80 | self.channel = None 81 | self.stub = None 82 | self.init_event = asyncio.Event() 83 | self.init_event.set() 84 | # 85 | self.tasks = set() 86 | 87 | def tasks_manage(self, coro, name=None, add_done_callback=True): 88 | _t = asyncio.create_task(coro, name=name) 89 | self.tasks.add(_t) 90 | if add_done_callback: 91 | _t.add_done_callback(self.tasks.discard) 92 | 93 | def task_cancel(self): 94 | [task.cancel() for task in self.tasks if not task.done()] # skipcq: PYL-W0106 95 | 96 | async def connect(self): 97 | self.init_event.clear() 98 | delay = 0 99 | while True: 100 | try: 101 | if self.channel: 102 | self.channel.close() 103 | self.channel = Channel(TLG_PROXY_HOST, TLG_PROXY_PORT, ssl=SSL_CONTEXT) 104 | self.stub = tlg.TlgProxyStub(self.channel) 105 | await self.post_message("Connected", reraise=True) 106 | self.init_event.set() 107 | break 108 | except ConnectionRefusedError: 109 | delay += random.randint(1, 15) # NOSONAR python:S2245 110 | logger.warning(f"Try connecting to Telegram proxy, retrying in {delay} second... ") 111 | await asyncio.sleep(delay) 112 | except ssl.SSLCertVerificationError as e: 113 | logger.error(f"Connect to Telegram proxy server failed: {e}") 114 | break 115 | except Exception as e: 116 | logger.error(f"Connect to Telegram proxy server failed, check certificate expiration date first: {e}") 117 | break 118 | 119 | async def post_message(self, text, inline_buttons=False, reraise=False): 120 | try: 121 | res = await self.stub.post_message( 122 | tlg.Request( 123 | bot_id=self.bot_id, 124 | token=self.token, 125 | chat_id=self.chat_id, 126 | inline_buttons=inline_buttons, 127 | data=f"{self.bot_id}. {text}" 128 | ) 129 | ) 130 | return res 131 | except (ConnectionRefusedError, exceptions.StreamTerminatedError): 132 | if self.init_event.is_set(): 133 | self.tasks_manage(self.connect()) 134 | elif reraise: 135 | raise 136 | except ssl.SSLCertVerificationError as e: 137 | logger.error(f"Post message to Telegram proxy failed: {e}") 138 | except (asyncio.CancelledError, KeyboardInterrupt): 139 | pass # user interrupt 140 | 141 | async def get_update(self) -> Any | None: 142 | try: 143 | res = await self.stub.get_update( 144 | tlg.Request( 145 | bot_id=self.bot_id, 146 | ) 147 | ) 148 | return json.loads(res.data) if res else None 149 | except (ConnectionRefusedError, exceptions.StreamTerminatedError): 150 | if self.init_event.is_set(): 151 | self.tasks_manage(self.connect()) 152 | except (asyncio.CancelledError, KeyboardInterrupt): 153 | pass # user interrupt 154 | except ssl.SSLCertVerificationError as e: 155 | logger.error(f"Get update from Telegram proxy failed: {e}") 156 | 157 | def close(self): 158 | self.channel.close() 159 | self.task_cancel() 160 | -------------------------------------------------------------------------------- /martin_binance/telegram_proxy/tlg_proxy.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Protocol buffer definition for telegram proxy service 3 | 4 | For compile use next commands 5 | pip install --pre "betterproto[compiler]" 6 | pip install grpcio-tools 7 | python -m grpc_tools.protoc -I . --python_betterproto_out=. telegram_proxy/tlg_proxy.proto 8 | */ 9 | 10 | syntax = "proto3"; 11 | 12 | package tlg; 13 | 14 | service TlgProxy { 15 | rpc PostMessage (Request) returns (Response) {} 16 | rpc GetUpdate (Request) returns (Response) {} 17 | } 18 | 19 | message Request { 20 | string bot_id = 1; 21 | string token = 2; 22 | string chat_id = 3; 23 | bool inline_buttons = 4; 24 | string data = 5; 25 | } 26 | 27 | message Response { 28 | string bot_id = 1; 29 | string data = 2; 30 | } 31 | -------------------------------------------------------------------------------- /martin_binance/telegram_proxy/tlg_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This script sets up a Telegram proxy server with secure SSL context. 5 | It configures logging, loads configuration from a TOML file, and establishes 6 | retry policies for HTTP requests. The script also defines a function to create 7 | a secure context for SSL connections and provides a method to generate keyboard 8 | markup for Telegram bot commands. 9 | """ 10 | __author__ = "Jerry Fedorenko" 11 | __copyright__ = "Copyright © 2025 Jerry Fedorenko aka VM" 12 | __license__ = "MIT" 13 | __version__ = "3.0.26" 14 | __maintainer__ = "Jerry Fedorenko" 15 | __contact__ = "https://github.com/DogsTailFarmer" 16 | 17 | import ssl 18 | from pathlib import Path 19 | import socket 20 | import asyncio 21 | import logging.handlers 22 | import requests 23 | import toml 24 | from requests.adapters import HTTPAdapter, Retry 25 | import ujson as json 26 | 27 | import martin_binance.tlg as tlg 28 | from martin_binance import LOG_FILE_TLG, CONFIG_FILE, CERT_DIR 29 | from exchanges_wrapper import Server, graceful_exit 30 | # 31 | logger = logging.getLogger('tlg_proxy') 32 | logger.setLevel(logging.DEBUG) 33 | formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s") 34 | # 35 | fh = logging.handlers.RotatingFileHandler(LOG_FILE_TLG, maxBytes=1000000, backupCount=10) 36 | fh.setFormatter(formatter) 37 | logger.addHandler(fh) 38 | fh.setLevel(logging.DEBUG) 39 | # 40 | sh = logging.StreamHandler() 41 | sh.setFormatter(formatter) 42 | logger.addHandler(sh) 43 | sh.setLevel(logging.INFO) 44 | # 45 | CLIENT_CERT = Path(CERT_DIR, "tlg-client.pem") 46 | SERVER_CERT = Path(CERT_DIR, "tlg-proxy.pem") 47 | SERVER_KEY = Path(CERT_DIR, "tlg-proxy.key") 48 | 49 | config = toml.load(str(CONFIG_FILE))['Telegram'] 50 | TLG_URL = config['tlg_url'] 51 | TLG_PROXY_HOST = config['tlg_proxy_host'] 52 | TLG_PROXY_PORT = config['tlg_proxy_port'] 53 | HEARTBEAT = config['heartbeat'] 54 | 55 | SESSION = requests.Session() 56 | retries = Retry(total=50, backoff_factor=1, status_forcelist=[101, 104, 111, 502, 503, 504]) 57 | SESSION.mount('https://', HTTPAdapter(max_retries=retries)) 58 | 59 | 60 | def create_secure_context(server_cert: Path, server_key: Path, *, trusted: Path) -> ssl.SSLContext: 61 | ctx = ssl.create_default_context( 62 | ssl.Purpose.CLIENT_AUTH, 63 | cafile=str(trusted), 64 | ) 65 | ctx.minimum_version = ssl.TLSVersion.TLSv1_2 66 | ctx.verify_mode = ssl.CERT_REQUIRED 67 | ctx.load_cert_chain(str(server_cert), str(server_key)) 68 | ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20') 69 | ctx.set_alpn_protocols(['h2']) 70 | return ctx 71 | 72 | 73 | def get_keyboard_markup(): 74 | return json.dumps({ 75 | "inline_keyboard": [ 76 | [ 77 | {'text': 'status', 'callback_data': 'status_callback'}, 78 | {'text': 'stop', 'callback_data': 'stop_callback'}, 79 | {'text': 'end', 'callback_data': 'end_callback'}, 80 | {'text': 'restart', 'callback_data': 'restart_callback'}, 81 | ] 82 | ]}) 83 | 84 | 85 | def requests_post(_method, _data, inline_buttons=False): 86 | if inline_buttons: 87 | keyboard = get_keyboard_markup() 88 | _data['reply_markup'] = keyboard 89 | _res = None 90 | try: 91 | _res = SESSION.post(_method, data=_data) 92 | except requests.exceptions.RetryError as _exc: 93 | logger.error(f"Telegram: {_exc}") 94 | except Exception as _exc: 95 | logger.error(f"Telegram: {_exc}") 96 | return _res 97 | 98 | 99 | def parse_command(token, chat_id, update_inner): 100 | update_id = update_inner.get('update_id') 101 | message = update_inner.get('message') 102 | from_id = message.get('from').get('id') 103 | if from_id != int(chat_id): 104 | return None 105 | message_id = message.get('message_id') 106 | text_in = update_inner.get('message').get('text') 107 | try: 108 | reply_to_message = message.get('reply_to_message').get('text') 109 | except AttributeError: 110 | reply_to_message = None 111 | _text = "The command must be a response to any message from a specific strategy," \ 112 | " use Reply + Menu combination" 113 | requests_post(f'{TLG_URL}{token}/sendMessage', _data={'chat_id': chat_id, 'text': _text}) 114 | return { 115 | 'update_id': update_id, 116 | 'message_id': message_id, 117 | 'text_in': text_in, 118 | 'reply_to_message': reply_to_message 119 | } 120 | 121 | 122 | def parse_query(token, chat_id, update_inner): 123 | update_id = update_inner.get('update_id') 124 | query = update_inner.get('callback_query') 125 | from_id = query.get('from').get('id') 126 | if from_id != int(chat_id): 127 | return None 128 | message = query.get('message') 129 | message_id = message.get('message_id') 130 | query_data = query.get('data') 131 | reply_to_message = message.get('text') 132 | command = None 133 | if query_data == 'status_callback': 134 | command = 'status' 135 | elif query_data == 'stop_callback': 136 | command = 'stop' 137 | elif query_data == 'end_callback': 138 | command = 'end' 139 | elif query_data == 'restart_callback': 140 | command = 'restart' 141 | requests_post( 142 | f'{TLG_URL}{token}//answerCallbackQuery', 143 | {'callback_query_id': query.get('id')} 144 | ) 145 | return { 146 | 'update_id': update_id, 147 | 'message_id': message_id, 148 | 'text_in': command, 149 | 'reply_to_message': reply_to_message 150 | } 151 | 152 | 153 | def telegram_get(token, chat_id, offset=None) -> []: 154 | command_list = [] 155 | _method = f'{TLG_URL}{token}/getUpdates' 156 | _res = requests_post(_method, _data={'chat_id': chat_id, 'offset': offset}) 157 | if not _res or _res.status_code != 200: 158 | if _res and _res.status_code != 200: 159 | logger.error(_res) 160 | return command_list 161 | __result = _res.json().get('result') 162 | for result_in in __result: 163 | parsed = None 164 | if result_in.get('message') is not None: 165 | parsed = parse_command(token, chat_id, result_in) 166 | if result_in.get('callback_query') is not None: 167 | parsed = parse_query(token, chat_id, result_in) 168 | if parsed: 169 | command_list.append(parsed) 170 | return command_list 171 | 172 | 173 | def process_update(token, chat_id, update_inner): 174 | logger.debug(f"process_update.update_inner: {update_inner}") 175 | reply = update_inner.get('reply_to_message') 176 | if not reply: 177 | return 178 | in_bot_id = reply.split('.')[0] 179 | if in_bot_id not in TlgProxy.bot_ids: 180 | return 181 | msg_in = str(update_inner['text_in']).lower().strip().replace('/', '') 182 | TlgProxy.command[in_bot_id] = msg_in 183 | post_text = f"{in_bot_id}. received '{msg_in}' command, ran to do" 184 | requests_post( 185 | f'{TLG_URL}{token}/sendMessage', 186 | _data={'chat_id': chat_id, 'text': post_text} 187 | ) 188 | 189 | 190 | def set_bot_commands(token): 191 | # Set command for Telegram bot 192 | _command = requests_post(f'{TLG_URL}{token}/getMyCommands', _data=None) 193 | if _command and _command.status_code == 200 and (not _command.json().get('result') or 194 | len(_command.json().get('result')) < 4): 195 | _commands = { 196 | "commands": json.dumps([ 197 | {"command": "status", 198 | "description": "Get strategy status"}, 199 | {"command": "stop", 200 | "description": "Stop strategy after end of cycle, not for Reverse"}, 201 | {"command": "end", 202 | "description": "Stop strategy after executed TP order, in Direct and Reverse, all the same"}, 203 | {"command": "restart", 204 | "description": "Restart current pair with recovery"} 205 | ]) 206 | } 207 | res = requests_post(f'{TLG_URL}{token}/setMyCommands', _data=_commands) 208 | logger.info(f"Set or update command menu for Telegram bot: code: {res.status_code}, result: {res.json()}," 209 | f" restart Telegram bot by /start command for update it") 210 | 211 | 212 | async def poll_update(request: tlg.Request): 213 | offset_id = None 214 | logger.info(f"New bot started, on request: {request.bot_id}") 215 | while True: 216 | await asyncio.sleep(HEARTBEAT) 217 | # Get external command from Telegram bot 218 | updates = telegram_get(request.token, request.chat_id, offset_id) 219 | if not updates: 220 | continue 221 | offset_id = updates[-1].get('update_id') + 1 222 | for update in updates: 223 | process_update(request.token, request.chat_id, update) 224 | 225 | 226 | class TlgProxy(tlg.TlgProxyBase): 227 | bot_tokens = set() 228 | bot_ids = set() 229 | tasks = set() 230 | command = {} 231 | 232 | def tasks_manage(self, coro, name=None, add_done_callback=True): 233 | _t = asyncio.create_task(coro, name=name) 234 | self.tasks.add(_t) 235 | if add_done_callback: 236 | _t.add_done_callback(self.tasks.discard) 237 | 238 | async def post_message(self, request: tlg.Request) -> tlg.Response: 239 | TlgProxy.bot_ids.add(request.bot_id) 240 | if request.token not in self.bot_tokens: 241 | TlgProxy.bot_tokens.add(request.token) 242 | set_bot_commands(request.token) 243 | # Start polling task 244 | self.tasks_manage(poll_update(request), name='poll_update') 245 | # Post message 246 | res = requests_post( 247 | f'{TLG_URL}{request.token}/sendMessage', 248 | _data={'chat_id': request.chat_id, 'text': request.data}, 249 | inline_buttons=request.inline_buttons 250 | ) 251 | return tlg.Response(bot_id=request.bot_id, data=json.dumps(res.status_code if res else None)) 252 | 253 | async def get_update(self, request: tlg.Request): 254 | return tlg.Response(bot_id=request.bot_id, data=json.dumps(TlgProxy.command.pop(request.bot_id, None))) 255 | 256 | 257 | def is_port_in_use(host: str, port: int) -> bool: 258 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 259 | return s.connect_ex((host, port)) == 0 260 | 261 | 262 | async def main(host=TLG_PROXY_HOST, port=TLG_PROXY_PORT): 263 | if is_port_in_use(host, port): 264 | raise SystemExit(f"gRPC Telegram proxy: local port {port} already used") 265 | 266 | server = Server([TlgProxy()]) 267 | with graceful_exit([server]): 268 | await server.start( 269 | host, 270 | port, 271 | ssl=create_secure_context(SERVER_CERT, SERVER_KEY, trusted=CLIENT_CERT) 272 | ) 273 | logger.info(f"Starting Telegram proxy service on {host}:{port}") 274 | await server.wait_closed() 275 | 276 | 277 | if __name__ == '__main__': 278 | asyncio.run(main()) 279 | -------------------------------------------------------------------------------- /martin_binance/templates/cli_0_BTCUSDT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | #################################################################### 4 | # Cyclic grid strategy based on martingale 5 | # See README.md for detail 6 | #################################################################### 7 | __author__ = "Jerry Fedorenko" 8 | __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" 9 | __license__ = "MIT" 10 | __version__ = "3.0.20" 11 | __maintainer__ = "Jerry Fedorenko" 12 | __contact__ = "https://github.com/DogsTailFarmer" 13 | """ 14 | ################################################################## 15 | Disclaimer 16 | 17 | All risks and possible losses associated with use of this strategy lie with you. 18 | Strongly recommended that you test the strategy in the demo mode before using real bidding. 19 | ################################################################## 20 | Check and set parameter at the TOP part of script 21 | Verify init message in Strategy output window for no error 22 | """ 23 | ################################################################ 24 | from decimal import Decimal 25 | import sys 26 | import asyncio 27 | from pathlib import Path 28 | import toml 29 | import logging.handlers 30 | 31 | from martin_binance import CONFIG_FILE, LOG_PATH, LAST_STATE_PATH 32 | import martin_binance.params as ex 33 | ################################################################ 34 | # Exchange setup and parameter settings 35 | ################################################################ 36 | # Set trading pair for Strategy 37 | ex.SYMBOL = 'BTCUSDT' 38 | # Exchange setup, see list of exchange in ms_cfg.toml 39 | ex.ID_EXCHANGE = 0 # See ms_cfg.toml Use for collection of statistics *and get client connection* 40 | ex.FEE_MAKER = Decimal('0.1') # standard exchange Fee for maker 41 | ex.FEE_TAKER = Decimal('0.1') # standard exchange Fee for taker 42 | ex.FEE_FIRST = False # For example fee in BNB and BNB in pair, and it is base asset 43 | ex.FEE_SECOND = False # For example fee in BNB and BNB in pair, and it is quote asset 44 | # Setting for auto deposit BNB on subaccount for fee payment. For Binance subaccount only. 45 | # See also https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#keeping-level-of-first-asset 46 | ex.FEE_BNB = { 47 | 'id_exchange': 0, # Where collected assets and keeping BNB volume 48 | 'symbol': 'BNB/USDT', # Specified on the source strategy (id_exchange above) 49 | 'email': 'sub-account@email.com', # Email registered on this subaccount 50 | 'target_amount': '0', # BNB in USD equivalent, no less than min_notional 51 | 'tranche_volume': '0' # BNB in USD equivalent, no less than min_notional 52 | } 53 | ex.GRID_MAX_COUNT = 5 # Maximum counts for placed grid orders 54 | # Trade parameter 55 | ex.START_ON_BUY = True # First cycle direction 56 | ex.AMOUNT_FIRST = Decimal('0.05') # Deposit for Sale cycle in first currency 57 | ex.USE_ALL_FUND = False # Use all available fund for initial cycle or alltime for GRID_ONLY 58 | ex.AMOUNT_SECOND = Decimal('1000.0') # Deposit for Buy cycle in second currency 59 | ex.PRICE_SHIFT = Decimal('0.01') # 'No market' shift price in % from current bid/ask price 60 | # Round pattern, set pattern 1.0123456789 or if not set used exchange settings 61 | ex.ROUND_BASE = str() 62 | ex.ROUND_QUOTE = str() 63 | ex.PROFIT = Decimal('0.15') # recommended FEE_MAKER*2 1 else 0 118 | ex.LAST_STATE_FILE = Path(LAST_STATE_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.json") 119 | # 120 | if ex.MODE == 'S': 121 | _logger = logging.getLogger('logger_S') 122 | else: 123 | _logger = logging.getLogger('logger') 124 | log_file = Path(LOG_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.log") 125 | formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s") 126 | fh = logging.handlers.RotatingFileHandler(log_file, maxBytes=1000000, backupCount=10) 127 | fh.setFormatter(formatter) 128 | _logger.addHandler(fh) 129 | _logger.setLevel(logging.DEBUG) # Default level for files output 130 | 131 | logging.getLogger('hpack').setLevel(logging.INFO) 132 | _logger.propagate = False 133 | # 134 | if strategy is None: 135 | from martin_binance.executor import Strategy 136 | strategy = Strategy() 137 | loop = asyncio.new_event_loop() 138 | asyncio.set_event_loop(loop) 139 | try: 140 | loop.create_task(strategy.main(ex.SYMBOL)) 141 | loop.run_forever() 142 | except KeyboardInterrupt: 143 | pass # user interrupt 144 | finally: 145 | try: 146 | loop.run_until_complete(strategy.ask_exit()) 147 | except (asyncio.CancelledError, KeyboardInterrupt): 148 | pass # user interrupt 149 | loop.run_until_complete(loop.shutdown_asyncgens()) 150 | return strategy 151 | 152 | 153 | if __name__ == "__main__": 154 | trade() 155 | -------------------------------------------------------------------------------- /martin_binance/templates/cli_1_BTCUSDT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | #################################################################### 4 | # Cyclic grid strategy based on martingale 5 | # See README.md for detail 6 | #################################################################### 7 | __author__ = "Jerry Fedorenko" 8 | __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" 9 | __license__ = "MIT" 10 | __version__ = "3.0.20" 11 | __maintainer__ = "Jerry Fedorenko" 12 | __contact__ = "https://github.com/DogsTailFarmer" 13 | """ 14 | ################################################################## 15 | Disclaimer 16 | 17 | All risks and possible losses associated with use of this strategy lie with you. 18 | Strongly recommended that you test the strategy in the demo mode before using real bidding. 19 | ################################################################## 20 | Check and set parameter at the TOP part of script 21 | Verify init message in Strategy output window for no error 22 | """ 23 | ################################################################ 24 | from decimal import Decimal 25 | import sys 26 | import asyncio 27 | from pathlib import Path 28 | import toml 29 | import logging.handlers 30 | 31 | from martin_binance import CONFIG_FILE, LOG_PATH, LAST_STATE_PATH 32 | import martin_binance.params as ex 33 | ################################################################ 34 | # Exchange setup and parameter settings 35 | ################################################################ 36 | # Set trading pair for Strategy 37 | ex.SYMBOL = 'BTCUSDT' 38 | # Exchange setup, see list of exchange in ms_cfg.toml 39 | ex.ID_EXCHANGE = 1 # See ms_cfg.toml Use for collection of statistics *and get client connection* 40 | ex.FEE_MAKER = Decimal('0.08') # standard exchange Fee for maker 41 | ex.FEE_TAKER = Decimal('0.1') # standard exchange Fee for taker 42 | ex.FEE_FIRST = False # For example fee in BNB and BNB in pair, and it is base asset 43 | ex.FEE_SECOND = False # For example fee in BNB and BNB in pair, and it is quote asset 44 | # Setting for auto deposit BNB on subaccount for fee payment. For Binance subaccount only. 45 | # See also https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#keeping-level-of-first-asset 46 | ex.FEE_BNB = { 47 | 'id_exchange': 0, # Where collected assets and keeping BNB volume 48 | 'symbol': 'BNB/USDT', # Specified on the source strategy (id_exchange above) 49 | 'email': 'sub-account@email.com', # Email registered on this subaccount 50 | 'target_amount': '0', # BNB in USD equivalent, no less than min_notional 51 | 'tranche_volume': '0' # BNB in USD equivalent, no less than min_notional 52 | } 53 | ex.GRID_MAX_COUNT = 5 # Maximum counts for placed grid orders 54 | # Trade parameter 55 | ex.START_ON_BUY = True # First cycle direction 56 | ex.AMOUNT_FIRST = Decimal('0.05') # Deposit for Sale cycle in first currency 57 | ex.USE_ALL_FUND = False # Use all available fund for initial cycle or alltime for GRID_ONLY 58 | ex.AMOUNT_SECOND = Decimal('1000.0') # Deposit for Buy cycle in second currency 59 | ex.PRICE_SHIFT = Decimal('0.01') # 'No market' shift price in % from current bid/ask price 60 | # Round pattern, set pattern 1.0123456789 or if not set used exchange settings 61 | ex.ROUND_BASE = str() 62 | ex.ROUND_QUOTE = str() 63 | ex.PROFIT = Decimal('0.15') # recommended FEE_MAKER*2 1 else 0 118 | ex.LAST_STATE_FILE = Path(LAST_STATE_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.json") 119 | # 120 | if ex.MODE == 'S': 121 | _logger = logging.getLogger('logger_S') 122 | else: 123 | _logger = logging.getLogger('logger') 124 | log_file = Path(LOG_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.log") 125 | formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s") 126 | fh = logging.handlers.RotatingFileHandler(log_file, maxBytes=1000000, backupCount=10) 127 | fh.setFormatter(formatter) 128 | _logger.addHandler(fh) 129 | _logger.setLevel(logging.DEBUG) # Default level for files output 130 | 131 | logging.getLogger('hpack').setLevel(logging.INFO) 132 | _logger.propagate = False 133 | # 134 | if strategy is None: 135 | from martin_binance.executor import Strategy 136 | strategy = Strategy() 137 | loop = asyncio.new_event_loop() 138 | asyncio.set_event_loop(loop) 139 | try: 140 | loop.create_task(strategy.main(ex.SYMBOL)) 141 | loop.run_forever() 142 | except KeyboardInterrupt: 143 | pass # user interrupt 144 | finally: 145 | try: 146 | loop.run_until_complete(strategy.ask_exit()) 147 | except (asyncio.CancelledError, KeyboardInterrupt): 148 | pass # user interrupt 149 | loop.run_until_complete(loop.shutdown_asyncgens()) 150 | return strategy 151 | 152 | 153 | if __name__ == "__main__": 154 | trade() 155 | -------------------------------------------------------------------------------- /martin_binance/templates/cli_2_TESTBTCTESTUSDT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | #################################################################### 4 | # Cyclic grid strategy based on martingale 5 | # See README.md for detail 6 | #################################################################### 7 | __author__ = "Jerry Fedorenko" 8 | __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" 9 | __license__ = "MIT" 10 | __version__ = "3.0.20" 11 | __maintainer__ = "Jerry Fedorenko" 12 | __contact__ = "https://github.com/DogsTailFarmer" 13 | """ 14 | ################################################################## 15 | Disclaimer 16 | 17 | All risks and possible losses associated with use of this strategy lie with you. 18 | Strongly recommended that you test the strategy in the demo mode before using real bidding. 19 | ################################################################## 20 | Check and set parameter at the TOP part of script 21 | Verify init message in Strategy output window for no error 22 | """ 23 | ################################################################ 24 | from decimal import Decimal 25 | import sys 26 | import asyncio 27 | from pathlib import Path 28 | import toml 29 | import logging.handlers 30 | 31 | from martin_binance import CONFIG_FILE, LOG_PATH, LAST_STATE_PATH 32 | import martin_binance.params as ex 33 | ################################################################ 34 | # Exchange setup and parameter settings 35 | ################################################################ 36 | # Set trading pair for Strategy 37 | ex.SYMBOL = 'TESTBTCTESTUSDT' 38 | # Exchange setup, see list of exchange in ms_cfg.toml 39 | ex.ID_EXCHANGE = 2 # See ms_cfg.toml Use for collection of statistics *and get client connection* 40 | ex.FEE_MAKER = Decimal('0.1') # standard exchange Fee for maker 41 | ex.FEE_TAKER = Decimal('0.17') # standard exchange Fee for taker 42 | ex.FEE_FIRST = False # For example fee in BNB and BNB in pair, and it is base asset 43 | ex.FEE_SECOND = False # For example fee in BNB and BNB in pair, and it is quote asset 44 | # Setting for auto deposit BNB on subaccount for fee payment. For Binance subaccount only. 45 | # See also https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#keeping-level-of-first-asset 46 | ex.FEE_BNB = { 47 | 'id_exchange': 0, # Where collected assets and keeping BNB volume 48 | 'symbol': 'BNB/USDT', # Specified on the source strategy (id_exchange above) 49 | 'email': 'sub-account@email.com', # Email registered on this subaccount 50 | 'target_amount': '0', # BNB in USD equivalent, no less than min_notional 51 | 'tranche_volume': '0' # BNB in USD equivalent, no less than min_notional 52 | } 53 | ex.GRID_MAX_COUNT = 5 # Maximum counts for placed grid orders 54 | # Trade parameter 55 | ex.START_ON_BUY = True # First cycle direction 56 | ex.AMOUNT_FIRST = Decimal('0.1') # Deposit for Sale cycle in first currency 57 | ex.USE_ALL_FUND = False # Use all available fund for initial cycle or alltime for GRID_ONLY 58 | ex.AMOUNT_SECOND = Decimal('1000.0') # Deposit for Buy cycle in second currency 59 | ex.PRICE_SHIFT = Decimal('0.01') # 'No market' shift price in % from current bid/ask price 60 | # Round pattern, set pattern 1.0123456789 or if not set used exchange settings 61 | ex.ROUND_BASE = str() 62 | ex.ROUND_QUOTE = str() 63 | ex.PROFIT = Decimal('0.15') # recommended FEE_MAKER*2 1 else 0 118 | ex.LAST_STATE_FILE = Path(LAST_STATE_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.json") 119 | # 120 | if ex.MODE == 'S': 121 | _logger = logging.getLogger('logger_S') 122 | else: 123 | _logger = logging.getLogger('logger') 124 | log_file = Path(LOG_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.log") 125 | formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s") 126 | fh = logging.handlers.RotatingFileHandler(log_file, maxBytes=1000000, backupCount=10) 127 | fh.setFormatter(formatter) 128 | _logger.addHandler(fh) 129 | _logger.setLevel(logging.DEBUG) # Default level for files output 130 | 131 | logging.getLogger('hpack').setLevel(logging.INFO) 132 | _logger.propagate = False 133 | # 134 | if strategy is None: 135 | from martin_binance.executor import Strategy 136 | strategy = Strategy() 137 | loop = asyncio.new_event_loop() 138 | asyncio.set_event_loop(loop) 139 | try: 140 | loop.create_task(strategy.main(ex.SYMBOL)) 141 | loop.run_forever() 142 | except KeyboardInterrupt: 143 | pass # user interrupt 144 | finally: 145 | try: 146 | loop.run_until_complete(strategy.ask_exit()) 147 | except (asyncio.CancelledError, KeyboardInterrupt): 148 | pass # user interrupt 149 | loop.run_until_complete(loop.shutdown_asyncgens()) 150 | return strategy 151 | 152 | 153 | if __name__ == "__main__": 154 | trade() 155 | -------------------------------------------------------------------------------- /martin_binance/templates/cli_3_BTCUSDT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | #################################################################### 4 | # Cyclic grid strategy based on martingale 5 | # See README.md for detail 6 | #################################################################### 7 | __author__ = "Jerry Fedorenko" 8 | __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" 9 | __license__ = "MIT" 10 | __version__ = "3.0.20" 11 | __maintainer__ = "Jerry Fedorenko" 12 | __contact__ = "https://github.com/DogsTailFarmer" 13 | """ 14 | ################################################################## 15 | Disclaimer 16 | 17 | All risks and possible losses associated with use of this strategy lie with you. 18 | Strongly recommended that you test the strategy in the demo mode before using real bidding. 19 | ################################################################## 20 | Check and set parameter at the TOP part of script 21 | Verify init message in Strategy output window for no error 22 | """ 23 | ################################################################ 24 | from decimal import Decimal 25 | import sys 26 | import asyncio 27 | from pathlib import Path 28 | import toml 29 | import logging.handlers 30 | 31 | from martin_binance import CONFIG_FILE, LOG_PATH, LAST_STATE_PATH 32 | import martin_binance.params as ex 33 | ################################################################ 34 | # Exchange setup and parameter settings 35 | ################################################################ 36 | # Set trading pair for Strategy 37 | ex.SYMBOL = 'BTCUSDT' 38 | # Exchange setup, see list of exchange in ms_cfg.toml 39 | ex.ID_EXCHANGE = 3 # See ms_cfg.toml Use for collection of statistics *and get client connection* 40 | ex.FEE_MAKER = Decimal('0.1') # standard exchange Fee for maker 41 | ex.FEE_TAKER = Decimal('0.15') # standard exchange Fee for taker 42 | ex.FEE_FIRST = False # For example fee in BNB and BNB in pair, and it is base asset 43 | ex.FEE_SECOND = False # For example fee in BNB and BNB in pair, and it is quote asset 44 | # Setting for auto deposit BNB on subaccount for fee payment. For Binance subaccount only. 45 | # See also https://github.com/DogsTailFarmer/martin-binance/wiki/How-it's-work#keeping-level-of-first-asset 46 | ex.FEE_BNB = { 47 | 'id_exchange': 0, # Where collected assets and keeping BNB volume 48 | 'symbol': 'BNB/USDT', # Specified on the source strategy (id_exchange above) 49 | 'email': 'sub-account@email.com', # Email registered on this subaccount 50 | 'target_amount': '0', # BNB in USD equivalent, no less than min_notional 51 | 'tranche_volume': '0' # BNB in USD equivalent, no less than min_notional 52 | } 53 | ex.GRID_MAX_COUNT = 5 # Maximum counts for placed grid orders 54 | # Trade parameter 55 | ex.START_ON_BUY = True # First cycle direction 56 | ex.AMOUNT_FIRST = Decimal('0') # Deposit for Sale cycle in first currency 57 | ex.USE_ALL_FUND = False # Use all available fund for initial cycle or alltime for GRID_ONLY 58 | ex.AMOUNT_SECOND = Decimal('1000.0') # Deposit for Buy cycle in second currency 59 | ex.PRICE_SHIFT = Decimal('0.01') # 'No market' shift price in % from current bid/ask price 60 | # Round pattern, set pattern 1.0123456789 or if not set used exchange settings 61 | ex.ROUND_BASE = str() 62 | ex.ROUND_QUOTE = str() 63 | ex.PROFIT = Decimal('0.15') # recommended FEE_MAKER*2 1 else 0 118 | ex.LAST_STATE_FILE = Path(LAST_STATE_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.json") 119 | # 120 | if ex.MODE == 'S': 121 | _logger = logging.getLogger('logger_S') 122 | else: 123 | _logger = logging.getLogger('logger') 124 | log_file = Path(LOG_PATH, f"{ex.ID_EXCHANGE}_{ex.SYMBOL}.log") 125 | formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s") 126 | fh = logging.handlers.RotatingFileHandler(log_file, maxBytes=1000000, backupCount=10) 127 | fh.setFormatter(formatter) 128 | _logger.addHandler(fh) 129 | _logger.setLevel(logging.DEBUG) # Default level for files output 130 | 131 | logging.getLogger('hpack').setLevel(logging.INFO) 132 | _logger.propagate = False 133 | # 134 | if strategy is None: 135 | from martin_binance.executor import Strategy 136 | strategy = Strategy() 137 | loop = asyncio.new_event_loop() 138 | asyncio.set_event_loop(loop) 139 | try: 140 | loop.create_task(strategy.main(ex.SYMBOL)) 141 | loop.run_forever() 142 | except KeyboardInterrupt: 143 | pass # user interrupt 144 | finally: 145 | try: 146 | loop.run_until_complete(strategy.ask_exit()) 147 | except (asyncio.CancelledError, KeyboardInterrupt): 148 | pass # user interrupt 149 | loop.run_until_complete(loop.shutdown_asyncgens()) 150 | return strategy 151 | 152 | 153 | if __name__ == "__main__": 154 | trade() 155 | -------------------------------------------------------------------------------- /martin_binance/templates/funds_rate.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/martin_binance/templates/funds_rate.db -------------------------------------------------------------------------------- /martin_binance/templates/ms_cfg.toml: -------------------------------------------------------------------------------- 1 | # Parameters for martin-binance strategy 2 | # Copyright © 2021-2025 Jerry Fedorenko aka VM 3 | # __version__ = "3.0.17" 4 | 5 | 6 | # List exchanges names, don't remove or edit row, add only! 7 | # Accounts name would be identically accounts.name from exchanges-wrapper 8 | exchange = [ 9 | 'Demo - Binance', # 0 10 | 'Demo - OKX', # 1 11 | "Demo - Bitfinex", # 2 12 | "Demo - Bybit", # 3 13 | "Binance", # 4 14 | "OKX", # 5 15 | "Bitfinex", # 6 16 | "Huobi", # 7 17 | "Binance US", # 8 18 | "Bybit", # 9 19 | ] 20 | 21 | [Exporter] 22 | port = 8000 23 | sleep_time_s = 60 24 | vps_name = "**** Place VPS Name there ****" 25 | # CoinMarketCap 26 | url = "https://pro-api.coinmarketcap.com/v2/tools/price-conversion" 27 | api = "********** Place API key for CoinMarketCap there ***********" 28 | rate_limit = 30 # Requests per minute 29 | 30 | [Telegram] 31 | tlg_url = "https://api.telegram.org/bot" 32 | tlg_proxy_host = "aaa.bbb.ccc.ddd" # IP Proxy for Telegram 33 | tlg_proxy_port = 50061 34 | chat_id = "*** Place channel_id there ***" 35 | heartbeat = 10 # sec 36 | 37 | [[Bots]] 38 | id_exchange = [0, 4, 8] # 'Binance', 'Demo - Binance', 'Binance US' 39 | token = "********** Place Token for Telegram bot there ***********" 40 | 41 | [[Bots]] 42 | id_exchange = [1, 5] # 'Demo - OKEX', 'OKEX' 43 | token = "********** Place Token for Telegram bot there ***********" 44 | 45 | [[Bots]] 46 | id_exchange = [2, 6] # "Demo - Bitfinex", "Bitfinex" 47 | token = "********** Place Token for Telegram bot there ***********" 48 | 49 | [[Bots]] 50 | id_exchange = [3, 9] # "Demo - Bitfinex", "Bitfinex" 51 | token = "********** Place Token for Telegram bot there ***********" 52 | -------------------------------------------------------------------------------- /martin_binance/templates/trial_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "GRID_MAX_COUNT": {"type": "int", "range": [3, 5]}, 3 | "PRICE_SHIFT": {"type": "float", "range": [0, 0.05], "step": 0.01}, 4 | "PROFIT": {"type": "float", "range": [0.05, 0.2], "step": 0.05}, 5 | "PROFIT_MAX": {"type": "float", "range": [0.4, 1.0], "step": 0.05}, 6 | "OVER_PRICE": {"type": "float", "range": [0.1, 1], "step": 0.1}, 7 | "ORDER_Q": {"type": "int", "range": [6, 12]}, 8 | "MARTIN": {"type": "float", "range": [5, 15], "step": 0.5}, 9 | "SHIFT_GRID_DELAY": {"type": "int", "range": [10, 150], "step": 10}, 10 | "KBB": {"type": "float", "range": [0.5, 4], "step": 0.5}, 11 | "LINEAR_GRID_K": {"type": "int", "range": [0, 500], "step": 50} 12 | } 13 | -------------------------------------------------------------------------------- /martin_binance/tlg/__init__.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # sources: telegram_proxy/tlg_proxy.proto 3 | # plugin: python-betterproto 4 | # This file has been @generated 5 | 6 | from dataclasses import dataclass 7 | from typing import ( 8 | TYPE_CHECKING, 9 | Dict, 10 | Optional, 11 | ) 12 | 13 | import betterproto 14 | import grpclib 15 | from betterproto.grpc.grpclib_server import ServiceBase 16 | 17 | 18 | if TYPE_CHECKING: 19 | import grpclib.server 20 | from betterproto.grpc.grpclib_client import MetadataLike 21 | from grpclib.metadata import Deadline 22 | 23 | 24 | @dataclass(eq=False, repr=False) 25 | class Request(betterproto.Message): 26 | bot_id: str = betterproto.string_field(1) 27 | token: str = betterproto.string_field(2) 28 | chat_id: str = betterproto.string_field(3) 29 | inline_buttons: bool = betterproto.bool_field(4) 30 | data: str = betterproto.string_field(5) 31 | 32 | 33 | @dataclass(eq=False, repr=False) 34 | class Response(betterproto.Message): 35 | bot_id: str = betterproto.string_field(1) 36 | data: str = betterproto.string_field(2) 37 | 38 | 39 | class TlgProxyStub(betterproto.ServiceStub): 40 | async def post_message( 41 | self, 42 | request: "Request", 43 | *, 44 | timeout: Optional[float] = None, 45 | deadline: Optional["Deadline"] = None, 46 | metadata: Optional["MetadataLike"] = None 47 | ) -> "Response": 48 | return await self._unary_unary( 49 | "/tlg.TlgProxy/PostMessage", 50 | request, 51 | Response, 52 | timeout=timeout, 53 | deadline=deadline, 54 | metadata=metadata, 55 | ) 56 | 57 | async def get_update( 58 | self, 59 | request: "Request", 60 | *, 61 | timeout: Optional[float] = None, 62 | deadline: Optional["Deadline"] = None, 63 | metadata: Optional["MetadataLike"] = None 64 | ) -> "Response": 65 | return await self._unary_unary( 66 | "/tlg.TlgProxy/GetUpdate", 67 | request, 68 | Response, 69 | timeout=timeout, 70 | deadline=deadline, 71 | metadata=metadata, 72 | ) 73 | 74 | 75 | class TlgProxyBase(ServiceBase): 76 | 77 | async def post_message(self, request: "Request") -> "Response": 78 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 79 | 80 | async def get_update(self, request: "Request") -> "Response": 81 | raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) 82 | 83 | async def __rpc_post_message( 84 | self, stream: "grpclib.server.Stream[Request, Response]" 85 | ) -> None: 86 | request = await stream.recv_message() 87 | response = await self.post_message(request) 88 | await stream.send_message(response) 89 | 90 | async def __rpc_get_update( 91 | self, stream: "grpclib.server.Stream[Request, Response]" 92 | ) -> None: 93 | request = await stream.recv_message() 94 | response = await self.get_update(request) 95 | await stream.send_message(response) 96 | 97 | def __mapping__(self) -> Dict[str, grpclib.const.Handler]: 98 | return { 99 | "/tlg.TlgProxy/PostMessage": grpclib.const.Handler( 100 | self.__rpc_post_message, 101 | grpclib.const.Cardinality.UNARY_UNARY, 102 | Request, 103 | Response, 104 | ), 105 | "/tlg.TlgProxy/GetUpdate": grpclib.const.Handler( 106 | self.__rpc_get_update, 107 | grpclib.const.Cardinality.UNARY_UNARY, 108 | Request, 109 | Response, 110 | ), 111 | } 112 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "martin-binance" 7 | authors = [{name = "Jerry Fedorenko", email = "jerry.fedorenko@yahoo.com"}] 8 | readme = "README.md" 9 | license = {file = "LICENSE"} 10 | classifiers=["Programming Language :: Python :: 3", 11 | "Development Status :: 5 - Production/Stable", 12 | "License :: OSI Approved :: MIT License", 13 | "Operating System :: Unix", 14 | "Operating System :: Microsoft :: Windows", 15 | "Operating System :: MacOS"] 16 | dynamic = ["version", "description"] 17 | requires-python = ">=3.10" 18 | 19 | dependencies = [ 20 | "exchanges-wrapper>=2.1.37", 21 | "betterproto==2.0.0b7", 22 | "colorama==0.4.6", 23 | "dash==2.18.2", 24 | "future==1.0.0", 25 | "inquirer==3.4.0", 26 | "jsonpickle==4.0.2", 27 | "libtmux==0.44.2", 28 | "numpy~=2.2.3", 29 | "optuna==4.2.1", 30 | "orjson~=3.10.15", 31 | "pandas==2.2.3", 32 | "plotly==6.0.0", 33 | "prometheus-client==0.21.1", 34 | "psutil==7.0.0", 35 | "pyarrow~=19.0.1", 36 | "requests==2.32.3", 37 | "schedule~=1.2.2", 38 | "scikit-learn==1.6.1", 39 | "scipy==1.15.2", 40 | "shortuuid~=1.0.13", 41 | "toml~=0.10.2", 42 | "tqdm==4.67.1", 43 | "ujson~=5.10.0" 44 | ] 45 | 46 | [tool.flit.module] 47 | name = "martin_binance" 48 | 49 | [project.urls] 50 | Source = "https://github.com/DogsTailFarmer/martin-binance" 51 | 52 | [project.scripts] 53 | martin-binance-init = "martin_binance.__init__:init" 54 | martin-binance-backtest = "martin_binance.backtest.OoTSP:main" 55 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | exchanges-wrapper>=2.1.37 2 | betterproto==2.0.0b7 3 | colorama==0.4.6 4 | dash==2.18.2 5 | future==1.0.0 6 | inquirer==3.4.0 7 | jsonpickle==4.0.2 8 | libtmux==0.44.2 9 | numpy~=2.2.3 10 | optuna==4.2.1 11 | orjson~=3.10.15 12 | pandas==2.2.3 13 | plotly==6.0.0 14 | prometheus-client==0.21.1 15 | psutil==7.0.0 16 | pyarrow~=19.0.1 17 | requests==2.32.3 18 | schedule~=1.2.2 19 | scikit-learn==1.6.1 20 | scipy==1.15.2 21 | shortuuid~=1.0.13 22 | toml~=0.10.2 23 | tqdm==4.67.1 24 | ujson~=5.10.0 -------------------------------------------------------------------------------- /uml/architecture-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/9e665521eb7e714013743d95f328d10e4a30c6a8/uml/architecture-0.png -------------------------------------------------------------------------------- /uml/architecture.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 'https://plantuml.com/component-diagram 3 | 4 | node "binance.com" { 5 | package "Sub account n" { 6 | [BTCUSDT] <--> (API n) 7 | [ETHTUSD] <--> (API n) 8 | } 9 | package "Main account" { 10 | [BTCBNB] <--> (API 1) 11 | [ETHUSDT] <--> (API 1) 12 | } 13 | } 14 | 15 | node "okx.com" { 16 | package "Sub account i" { 17 | [OKBUSDT] <--> (API i) 18 | } 19 | package "Main account on okx" { 20 | [PAXGUSDT] <--> (API main) 21 | } 22 | } 23 | 24 | node "..." { 25 | package "Account" { 26 | [AVAXUSDT] <--> (API k) 27 | } 28 | } 29 | 30 | package "exchanges-wrapper" #D3D3D3 { 31 | component ew [API/WSS async wrapper with gRPC powered server 32 | === 33 | https://github.com/DogsTailFarmer/exchanges-wrapper] 34 | (aiohttp/websockets) 35 | (exch_srv.py) as srv 36 | } 37 | 38 | package "martin-binance" #D3D3D3 { 39 | [funds_rate_exporter.py] as exp 40 | database funds_rate.db #F8F8FF { 41 | [assets] -up-> exp 42 | [trade raw] -up-> exp 43 | } 44 | component mw [ 45 | strategy_base.py 46 | === 47 | gRPC client / API layer 48 | ] 49 | frame python_strategy #E6E6FA { 50 | component ex [ 51 | executor.py 52 | === 53 | "Logic of trading strategy" 54 | ] 55 | component c1 [ 56 | cli_0_BTCUSDT.py 57 | === 58 | Executable module 59 | with trade parameters 60 | setup 61 | ] 62 | component cn [ 63 | cli_X_ETHTUSD.py 64 | === 65 | Executable module 66 | with trade parameter 67 | setup 68 | ] 69 | } 70 | [mw] <-up-> [srv] 71 | [ex] <-up-> [mw] 72 | [c1] <-up-> [ex] 73 | [cn] <-up-> [ex] 74 | } 75 | 76 | cloud Telegram { 77 | [Bot "Sub account n"] <-down-> (API) 78 | [Bot "Main account"] <-down-> (API) 79 | note left of API : Control and status 80 | } 81 | 82 | cloud data_collect_and_view { 83 | [https://grafana.com] as gr 84 | [https://github.com/prometheus/client_python] as prom 85 | } 86 | 87 | 88 | [aiohttp/websockets] <-up-> (API 1) 89 | [aiohttp/websockets] <-up-> (API n) 90 | [aiohttp/websockets] <-up-> (API main) 91 | [aiohttp/websockets] <-up-> (API i) 92 | [aiohttp/websockets] <-up-> (API k) 93 | [ex] --> [trade raw] 94 | [mw] --> [assets] 95 | [exp] -left-> prom 96 | prom -up-> gr 97 | [ex] <--> API 98 | 99 | @enduml --------------------------------------------------------------------------------