├── .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
<--> exchanges-wrapper 
14 |
15 | ***
16 |
17 |
18 |
19 |
20 |
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
--------------------------------------------------------------------------------