Compare commits

...

65 Commits

Author SHA1 Message Date
algizn97
704249d0af Fixed and updated tasks 2025-08-27 14:28:53 +05:00
algizn97
bf44b481e9 Added new handlers for tasks 2025-08-27 14:28:23 +05:00
algizn97
02fa03c824 Added documentation, added event_loop for tasks, added WebSocket 2025-08-27 13:28:44 +05:00
algizn97
4406003a6e Fixed 2025-08-27 13:28:33 +05:00
algizn97
3c282975c1 Updated 2025-08-27 12:56:32 +05:00
algizn97
aec8fea628 Updated 2025-08-27 12:56:22 +05:00
algizn97
78b76b4aa6 Updated README.md 2025-08-27 12:56:01 +05:00
algizn97
91cfdbc37b Added loggers 2025-08-27 12:55:43 +05:00
algizn97
f822220c40 Added documentation and update functions 2025-08-27 12:55:21 +05:00
algizn97
9032957631 Added documentation, added websocket and tasks 2025-08-27 12:54:45 +05:00
algizn97
a140e0eb6f Added documentation 2025-08-27 12:53:35 +05:00
algizn97
511b08e8e5 Added WebSocket 2025-08-27 12:52:33 +05:00
algizn97
50afefeb5f Update 2025-08-26 19:36:55 +05:00
algizn97
07df16dbe9 Added new functions and documentation 2025-08-26 19:36:11 +05:00
algizn97
8bc4c634fe Update 2025-08-26 19:35:40 +05:00
algizn97
7c48336a62 Update 2025-08-26 19:35:01 +05:00
algizn97
fd279f0562 Added loggers for models 2025-08-26 19:34:47 +05:00
algizn97
43e62fdeff Update keyboards 2025-08-26 19:11:53 +05:00
algizn97
f23bda38f4 Added requirements.txt 2025-08-26 19:11:23 +05:00
algizn97
29a5df0b1a Added tasks 2025-08-26 19:11:07 +05:00
algizn97
2597615630 Added states 2025-08-26 19:10:57 +05:00
algizn97
d29b4465ad added documentation 2025-08-26 19:10:43 +05:00
algizn97
73f0c67564 updated timer functions 2025-08-25 17:08:33 +05:00
algizn97
554166eeaf update 2025-08-25 17:08:14 +05:00
algizn97
964c0a09b8 Added keyboard 2025-08-25 17:08:04 +05:00
algizn97
61979653e0 Updated 2025-08-25 17:07:50 +05:00
algizn97
39b8d17498 Updated timer functions 2025-08-25 17:07:41 +05:00
algizn97
dd63f4c015 added 2025-08-23 14:41:23 +05:00
algizn97
6267663015 Added States.py 2025-08-23 14:34:38 +05:00
algizn97
89ea511072 Update 2025-08-23 14:34:19 +05:00
algizn97
afe61ea7d6 Added documentation 2025-08-23 14:33:33 +05:00
algizn97
2da06481f7 Update 2025-08-23 14:32:11 +05:00
algizn97
6c3f13f372 Update 2025-08-23 14:32:04 +05:00
algizn97
1ec9732607 Update loggers 2025-08-23 14:31:45 +05:00
algizn97
c7da20d577 Update 2025-08-23 14:31:09 +05:00
algizn97
8a2497bcac Added logger_helper.py 2025-08-23 14:30:48 +05:00
algizn97
0746490786 Delete func_min_qty.py and logs.py 2025-08-23 14:30:24 +05:00
algizn97
f895c19b14 fixed the method of getting the timer value. 2025-08-22 16:57:59 +05:00
algizn97
c4b35be053 Updated the risk management function 2025-08-22 16:49:17 +05:00
algizn97
54667db29b Added a martingale step 2025-08-22 16:48:17 +05:00
algizn97
12a00a1f3a Added a martingale step 2025-08-22 16:48:09 +05:00
algizn97
6ec99dc9a7 Update functions 2025-08-22 16:47:33 +05:00
algizn97
812920f46d The timer function has been updated 2025-08-22 13:56:56 +05:00
algizn97
cd7180c3d7 Added the function of getting and updating data in the database for the UserTimer table 2025-08-22 13:55:50 +05:00
algizn97
fc381ae6c2 Added timer 2025-08-22 13:54:13 +05:00
algizn97
8ab308d4b9 Added keyboards 2025-08-22 13:53:48 +05:00
algizn97
0c4204fb6e Added UserTimer 2025-08-22 13:53:17 +05:00
algizn97
c9c6a5b7f0 Fixed filename 2025-08-22 13:52:50 +05:00
algizn97
4ca6e1fb2c Added dp 2025-08-22 13:52:31 +05:00
algizn97
1a20c1a9d2 Added keyboard 2025-08-22 13:52:20 +05:00
algizn97
eaf8458835 Update 2025-08-21 16:10:01 +05:00
algizn97
f8cedf4cb4 Fixed 2025-08-21 16:09:32 +05:00
algizn97
d0577f163b Added get user trades 2025-08-21 14:22:54 +05:00
algizn97
8293c44864 Added USER DEALS 2025-08-21 14:22:34 +05:00
algizn97
c1a9f16faa Added commission_fee 2025-08-21 13:47:19 +05:00
algizn97
99b51d4cc0 Added limit and market orders functions 2025-08-21 13:46:12 +05:00
algizn97
b46d8d7af9 Update handlers.py 2025-08-21 13:41:58 +05:00
algizn97
c8b0dad7c2 Added commission_fee and order_type functions 2025-08-21 13:39:43 +05:00
algizn97
7f53caaac6 Added commission_fee and entry_order_type 2025-08-21 13:38:50 +05:00
algizn97
fe1c0b16ce Added reply keyboard (base_buttons_markup) 2025-08-21 13:37:58 +05:00
algizn97
4ebe7399ba Update function 2025-08-21 13:36:11 +05:00
algizn97
2dc639d59a Added market order and limit order 2025-08-21 13:35:42 +05:00
algizn97
a6ba949061 Added buttons To the main page, Open a deal, My Deals and select the input type 2025-08-21 13:34:56 +05:00
algizn97
de7b5ce557 Update 2025-08-21 13:23:17 +05:00
algizn97
cb7d4b1f66 Fixed the letter 2025-08-20 17:51:06 +05:00
41 changed files with 3533 additions and 680 deletions

View File

@@ -1 +1,3 @@
TOKEN_TELEGRAM_BOT= TOKEN_TELEGRAM_BOT_1=
TOKEN_TELEGRAM_BOT_2=
TOKEN_TELEGRAM_BOT_3=

5
.gitignore vendored
View File

@@ -7,6 +7,5 @@ __pycache__/
env/ env/
venv/ venv/
.venv/ .venv/
.idea
requirements.txt /.idea

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

610
.idea/dbnavigator.xml generated Normal file
View File

@@ -0,0 +1,610 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DDLFileAttachmentManager">
<mappings />
<preferences />
</component>
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="false" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DatabaseAssistantManager">
<assistants>
<assistant-state connection-id="539567a8-9e28-4dbe-b015-5c44ee3cfda5" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
<assistant-state connection-id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
<assistant-state connection-id="be96bc1e-ca0e-4a7f-a163-9127fdee15c6" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
<assistant-state connection-id="8d8de23e-fc36-46b8-a593-c47740aca91b" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
<assistant-state connection-id="d0ab0625-5cb0-46d3-9aa4-4d4b02bd8890" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
<assistant-state connection-id="4a35d891-e512-4ca8-85c4-6f6647deefbc" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
<assistant-state connection-id="431aa026-563e-40e3-a8f7-50e01f1f1af7" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
<assistant-state connection-id="f5fd2325-996b-4e52-a9f8-b1dcee5d149d" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
<conversations />
</assistant-state>
</assistants>
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="false" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.DatabaseConsoleManager">
<connection id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20">
<console name="Connection" type="STANDARD" schema="main" session="Main" />
</connection>
</component>
<component name="DBNavigator.Project.DatabaseEditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.DatabaseSessionManager">
<connection id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" />
</component>
<component name="DBNavigator.Project.DatasetFilterManager">
<filter-actions connection-id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" dataset="main.user_bybit_api" active-filter-id="EMPTY_FILTER" />
</component>
<component name="DBNavigator.Project.ExecutionManager">
<retain-sticky-names value="false" />
</component>
<component name="DBNavigator.Project.MethodExecutionManager">
<method-browser />
<execution-history>
<group-entries value="true" />
<execution-inputs />
</execution-history>
<execution-variables-cache />
</component>
<component name="DBNavigator.Project.ObjectQuickFilterManager">
<last-used-operator value="EQUAL" />
<filters />
</component>
<component name="DBNavigator.Project.Settings">
<connections>
<connection source-id="" id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" active="true" signed="true">
<database>
<name value="Connection" />
<description value="" />
<database-type value="SQLITE" />
<config-type value="BASIC" />
<database-version value="3.5" />
<driver-source value="BUNDLED" />
<driver-library value="" />
<driver value="" />
<url-type value="FILE" />
<host value="" />
<port value="" />
<database value="" />
<tns-folder value="" />
<tns-profile value="" />
<server-type value="" />
<protocol value="" />
<url-parameters />
<files>
<file path="C:\Users\Algiz_N\PycharmProjects\stcs\db.sqlite3" schema="main" />
</files>
<type value="NONE" />
<user value="" />
<token-type value="" />
<token-config-file value="" />
<token-profile value="" />
<session-user value="" />
</database>
<properties>
<auto-commit value="true" />
</properties>
<ssh-settings>
<active value="false" />
<proxy-host value="" />
<proxy-port value="22" />
<proxy-user value="" />
<auth-type value="PASSWORD" />
<key-file value="" />
</ssh-settings>
<ssl-settings>
<active value="false" />
<certificate-authority-file value="" />
<client-certificate-file value="" />
<client-key-file value="" />
</ssl-settings>
<details>
<charset value="UTF-8" />
<session-management value="true" />
<ddl-file-binding value="true" />
<database-logging value="true" />
<connect-automatically value="true" />
<restore-workspace value="true" />
<restore-workspace-deep value="false" />
<environment-type value="default" />
<connectivity-timeout value="30" />
<idle-time-to-disconnect value="30" />
<idle-time-to-disconnect-pool value="5" />
<credential-expiry-time value="10" />
<max-connection-pool-size value="7" />
<alternative-statement-delimiter value="" />
</details>
<debugger>
<compile-dependencies value="true" />
<tcp-driver-tunneling value="false" />
<tcp-host-address value="" />
<tcp-port-from value="4000" />
<tcp-port-to value="4999" />
<debugger-type value="JDBC" />
</debugger>
<object-filters hide-empty-schemas="false" hide-pseudo-columns="false" hide-audit-columns="false">
<object-filters />
<object-type-filter use-master-settings="true">
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="JSON_VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="JAVA_CLASS" enabled="true" />
<object-type name="JAVA_FIELD" enabled="true" />
<object-type name="JAVA_METHOD" enabled="true" />
<object-type name="JAVA_RESOURCE" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
<object-type name="CREDENTIAL" enabled="true" />
<object-type name="AI_PROFILE" enabled="true" />
</object-type-filter>
</object-filters>
</connection>
</connections>
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
<enable-sticky-paths value="true" />
<enable-quick-filters value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="JSON_VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="JAVA_CLASS" enabled="true" />
<object-type name="JAVA_FIELD" enabled="true" />
<object-type name="JAVA_METHOD" enabled="true" />
<object-type name="JAVA_RESOURCE" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
<object-type name="CREDENTIAL" enabled="true" />
<object-type name="AI_PROFILE" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="JSON VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="JAVA CLASS" enabled="true" />
<object-type name="INNER CLASS" enabled="true" />
<object-type name="JAVA FIELD" enabled="true" />
<object-type name="JAVA METHOD" enabled="true" />
<object-type name="JAVA PARAMETER" enabled="true" />
<object-type name="JAVA RESOURCE" enabled="true" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="false" />
<object-type name="CREDENTIAL" enabled="false" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<audit-columns>
<column-names value="" />
<visible value="true" />
<editable value="false" />
</audit-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="YAML" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
<exit-on-changes value="ASK" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="json view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="json view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
<mapping file-type-id="JAVA_SOURCE" extensions="sql" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<assistant-settings>
<credential-settings>
<credentials />
</credential-settings>
</assistant-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
</project>

View File

@@ -0,0 +1,44 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="span" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="PyCompatibilityInspection" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ourVersions">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="3.13" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredNames">
<list>
<option value="format" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="type.*" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 virtualenv at C:\Users\Algiz_N\PycharmProjects\stcs\.venv" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/stcs.iml" filepath="$PROJECT_DIR$/.idea/stcs.iml" />
</modules>
</component>
</project>

8
.idea/stcs.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.13 virtualenv at C:\Users\Algiz_N\PycharmProjects\stcs\.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,37 +0,0 @@
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.filters import Command, CommandStart
from aiogram.types import Message
from app.telegram.database.models import async_main
from app.telegram.handlers.handlers import router
from app.telegram.functions.main_settings.settings import router_main_settings
from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings
from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api
from app.services.Bybit.functions.functions import router_functions_bybit_trade
from config import TOKEN_TG_BOT
from app.telegram.logs import logger
bot = Bot(token=TOKEN_TG_BOT)
dp = Dispatcher()
async def main():
await async_main()
dp.include_router(router)
dp.include_router(router_main_settings)
dp.include_router(router_risk_management_settings)
dp.include_router(router_register_bybit_api)
dp.include_router(router_functions_bybit_trade)
await dp.start_polling(bot)
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Bot is off")

73
BybitBot_API.py Normal file
View File

@@ -0,0 +1,73 @@
import asyncio
import logging.config
from aiogram import Bot, Dispatcher
import app.services.Bybit.functions.bybit_ws as bybit_ws
import app.telegram.database.requests as rq
from app.telegram.database.models import async_main
from app.telegram.handlers.handlers import router
from app.telegram.functions.main_settings.settings import router_main_settings
from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings
from app.telegram.functions.condition_settings.settings import condition_settings_router
from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api
from app.services.Bybit.functions.functions import router_functions_bybit_trade
from logger_helper.logger_helper import LOGGING_CONFIG
from config import TOKEN_TG_BOT_1
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("main")
bot = Bot(token=TOKEN_TG_BOT_1)
dp = Dispatcher()
def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
"""
Возвращает текущий активный цикл событий asyncio или создает новый, если его нет.
"""
try:
return asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
loop = get_or_create_event_loop()
bybit_ws.set_event_loop(loop)
async def run_ws_for_user(tg_id, message) -> None:
"""
Запускает WebSocket Bybit для пользователя с указанным tg_id.
"""
api_key = await rq.get_bybit_api_key(tg_id)
api_secret = await rq.get_bybit_secret_key(tg_id)
await bybit_ws.start_execution_ws(api_key, api_secret, message)
async def main() -> None:
"""
Основная асинхронная функция запуска бота:
"""
await async_main()
dp.include_router(router)
dp.include_router(router_main_settings)
dp.include_router(router_risk_management_settings)
dp.include_router(condition_settings_router)
dp.include_router(router_register_bybit_api)
dp.include_router(router_functions_bybit_trade)
await dp.start_polling(bot)
if __name__ == '__main__':
try:
logger.info("Bot is on")
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Bot is off")

View File

@@ -1,20 +1,74 @@
# Чат-робот STCS Crypto Trading Telegram Bot
__
**Функционал:** Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
+ **Настройки**
+ Основные параметры *(Настроен, работает)* ##Основные возможности
+ Режим торговли Лонг/Шорт *(настроены)*, Switch/Smart *(не настроены)*
+ Тип маржи: Изолированная / Кросс *(настроено)* - Поддержка работы с биржей Bybit через официальный API.
+ Размер кредитного плеча: от x1 до x100 *(настроено)*
+ Начальная ставка: числовое значение *(настроено)* - Открытие и закрытие позиций по выбранным торговым парам.
+ Коэффициент мартингейла: число *(настроено)*
+ Максимальное количество ставок в серии: число *(настроено)* - Поддержка рыночных и лимитных ордеров.
+ Риск-менеджмент (Настроен, работает)
+ Процент изменения цены для фиксации прибыли (TP%): число *(настроено)* - Установка уровней тейк-профита (TP) и стоп-лосса (SL).
+ Процент изменения цены для фиксации убытков (SL%): число (пример: 1%) *(настроено)*
+ Максимальный риск на сделку (в % от баланса): число (опционально) *(настроено)* - Управление кредитным плечом (leverage).
+ Условия запуска *(Не настроен)*
+ Дополнительные параметры *(Не настроен)* - Реализация стратегии мартингейла с настройками шага, коэффициента и лимитов.
+ Подключение Bybit *(настроено)*
+ Информация о правильном получении и сохранении Bybit-API keys *(настроено)* - Контроль максимального риска на сделку по балансу пользователя.
- Обработка ошибок API, логирование событий и информирование пользователя.
- Таймеры для отложенного открытия и закрытия сделок.
- Интерактивное меню и ввод настроек через Telegram.
- Хранение пользовательских настроек и статистики в базе данных.
##Установка и запуск
1. Клонируйте репозиторий:
git clone <URL_репозитория>
2. Установите зависимости:
pip install -r requirements.txt
3. Создайте файл .env и настройте переменные окружения.
4. Запустите бота:
python BybitBot_API.py
##Настройки пользователя
- Кредитное плечо (например, 15x)
- Торговая пара (например, DOGEUSDT, BTCUSDT)
- Начальное количество для сделок
- Тип ордера (Market или Limit)
- Уровни Take Profit и Stop Loss (в процентах или цене)
- Коэффициент мартингейла и максимальное количество шагов
- Максимально допустимый риск на одну сделку (% от баланса)
- Таймеры для старта и закрытия сделок
##Безопасность и риски
- Бот требует аккуратной настройки параметров риска.
- Храните API ключи в безопасности, избегайте публикации.

View File

@@ -1,22 +1,27 @@
from aiogram import F, Router from aiogram import F, Router
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
# FSM - Механизм состояния from app.states.States import state_reg_bybit_api
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("add_bybit_api")
router_register_bybit_api = Router() router_register_bybit_api = Router()
class state_reg_bybit_api(StatesGroup):
api_key = State()
secret_key = State()
@router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api_message') @router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api_message')
async def info_for_bybit_api_message(callback: CallbackQuery): async def info_for_bybit_api_message(callback: CallbackQuery) -> None:
"""
Отвечает пользователю подробной инструкцией по подключению аккаунта Bybit.
Показывает как создать API ключ и передать его чат-боту.
"""
text = '''<b>Подключение Bybit аккаунта</b> text = '''<b>Подключение Bybit аккаунта</b>
<b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b> <b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b>
@@ -38,17 +43,27 @@ async def info_for_bybit_api_message(callback: CallbackQuery):
await callback.answer() await callback.answer()
@router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api') @router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api')
async def add_api_key_message(callback: CallbackQuery, state: FSMContext): async def add_api_key_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Инициирует процесс добавления API ключа.
Переводит пользователя в состояние ожидания ввода API Key.
"""
await state.set_state(state_reg_bybit_api.api_key) await state.set_state(state_reg_bybit_api.api_key)
text = 'Отправьте KEY_API ниже: ' text = 'Отправьте KEY_API ниже: '
await callback.message.answer(text=text) await callback.message.answer(text=text)
@router_register_bybit_api.message(state_reg_bybit_api.api_key) @router_register_bybit_api.message(state_reg_bybit_api.api_key)
async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext): async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext) -> None:
await state.update_data(api_key = message.text) """
Сохраняет API Key во временное состояние FSM,
затем запрашивает у пользователя ввод Secret Key.
"""
await state.update_data(api_key=message.text)
text = 'Отправьте SECRET_KEY ниже' text = 'Отправьте SECRET_KEY ниже'
@@ -56,9 +71,14 @@ async def add_api_key_and_message_for_secret_key(message: Message, state: FSMCon
await state.set_state(state_reg_bybit_api.secret_key) await state.set_state(state_reg_bybit_api.secret_key)
@router_register_bybit_api.message(state_reg_bybit_api.secret_key) @router_register_bybit_api.message(state_reg_bybit_api.secret_key)
async def add_secret_key(message: Message, state: FSMContext): async def add_secret_key(message: Message, state: FSMContext) -> None:
await state.update_data(secret_key = message.text) """
Сохраняет Secret Key и финализирует регистрацию,
обновляет базу данных, устанавливает символ пользователя и очищает состояние.
"""
await state.update_data(secret_key=message.text)
data = await state.get_data() data = await state.get_data()
@@ -68,6 +88,5 @@ async def add_secret_key(message: Message, state: FSMContext):
await state.clear() await state.clear()
await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!') await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!',
reply_markup=reply_markup.base_buttons_markup)

View File

@@ -1,254 +1,669 @@
import time import asyncio
import json
import time
from typing import Optional import logging.config
from asyncio import Handle
from annotated_types import T
from pybit import exceptions from pybit import exceptions
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP
from pybit.unified_trading import WebSocket from logger_helper.logger_helper import LOGGING_CONFIG
import app.services.Bybit.functions.price_symbol as price_symbol
from app.services.Bybit.functions import price_symbol
import app.services.Bybit.functions.balance as balance_g import app.services.Bybit.functions.balance as balance_g
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
import logging logging.config.dictConfig(LOGGING_CONFIG)
logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger("futures")
def handle_message(message):
print(message)
async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty):
match margin_mode:
case 'ISOLATED_MARGIN':
margin_mode = 'Isolated'
case 'REGULAR_MARGIN':
margin_mode = 'Cross'
text = f'''Позиция была успешна открыта!
Торговая пара: {symbol}
Движение: {trade_mode}
Тип-маржи: {margin_mode}
Кредитное плечо: {leverage}
Количество: {qty}
'''
await message.answer(text=text, parse_mode='html')
async def error_max_step(message):
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок')
async def error_max_risk(message):
await message.answer('Сделка не была совершена, слишком высокий риск')
async def contract_long(tg_id, message, margin_mode):
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
SYMBOL = await rq.get_symbol(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id)
data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id)
match margin_mode:
case 'Isolated':
margin_mode = 'ISOLATED_MARGIN'
case 'Cross':
margin_mode = 'REGULAR_MARGIN'
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
def safe_float(val) -> float:
"""
Безопасное преобразование значения в float.
Возвращает 0.0, если значение None, пустое или некорректное.
"""
try: try:
balance = 0 if val is None or val == '':
price = 0 return 0.0
return float(val)
except (ValueError, TypeError):
logger.error("Некорректное значение для преобразования в float")
return 0.0
balance = await balance_g.get_balance(tg_id)
price = await price_symbol.get_price(tg_id, message)
client.set_margin_mode( def format_trade_details_position(data, commission_fee):
setMarginMode=margin_mode # margin_type """
Форматирует информацию о сделке в виде строки.
"""
msg = data.get('data', [{}])[0]
closed_size = float(msg.get('closedSize', 0))
symbol = msg.get('symbol', 'N/A')
entry_price = float(msg.get('execPrice', 0))
qty = float(msg.get('execQty', 0))
order_type = msg.get('orderType', 'N/A')
side = msg.get('side', '')
commission = float(msg.get('execFee', 0))
pnl = float(msg.get('execPnl', 0))
if commission_fee == "Да":
if pnl >= 0:
pnl -= commission
else:
pnl -= commission
movement = ''
if side.lower() == 'buy':
movement = 'Покупка'
elif side.lower() == 'sell':
movement = 'Продажа'
else:
movement = side
if closed_size > 0:
return (
f"Сделка закрыта:\n"
f"Торговая пара: {symbol}\n"
f"Цена исполнения: {entry_price:.6f}\n"
f"Количество: {qty}\n"
f"Закрыто позиций: {closed_size}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
f"Комиссия за сделку: {commission:.6f}\n"
f"Реализованная прибыль: {pnl:.6f} USDT"
)
else:
return (
f"Сделка открыта:\n"
f"Торговая пара: {symbol}\n"
f"Цена исполнения: {entry_price:.6f}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
f"Комиссия за сделку: {commission:.6f}"
) )
martingale_factor = float(data_main_stgs['martingale_factor']) # Исправлено: было maximal_quantity
max_martingale_steps = int(data_main_stgs['maximal_quantity'])
starting_quantity = float(data_main_stgs['starting_quantity'])
max_risk_percent = float(data_risk_management_stgs['max_risk_deal'])
loss_profit = float(data_risk_management_stgs['price_loss'])
takeProfit= float(data_risk_management_stgs['price_profit'])
# Инициализация переменных def parse_pnl_from_msg(msg) -> float:
next_quantity = starting_quantity """
last_quantity = starting_quantity Извлекает реализованную прибыль/убыток из сообщения.
realised_pnl = 0.0 """
try:
current_martingale_step = 0 # Текущая ставка в серии return float(msg.get('realisedPnl', 0))
next_quantity = 0
realised_pnl = 0
last_quantity = starting_quantity
# Пример расчёта следующего размера позиции
try:
position_info = client.get_positions(category='linear', symbol=SYMBOL)
position = position_info['result']['list'][0] # или другой нужный индекс
realised_pnl = float(position['unrealisedPnl'])
if realised_pnl > 0:
starting_quantity = next_quantity
current_martingale_step = 0
elif not realised_pnl:
next_quantity = starting_quantity
current_martingale_step += 1
else:
current_martingale_step += 1
next_quantity = last_quantity * martingale_factor
starting_quantity = next_quantity
except Exception as e:
print("Не получены позиции")
next_quantity = starting_quantity
potential_loss = (next_quantity * float(price)) * (loss_profit / 100)
allowed_loss = float(balance) * (max_risk_percent / 100)
if current_martingale_step >= max_martingale_steps:
print("Достигнут максимум ставок в серии (8)!")
print("Торговля не продолжится")
await error_max_step(message)
else:
if potential_loss > allowed_loss:
print(f"ОШИБКА: Риск превышен!")
print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT")
print(f"Разрешено = {allowed_loss} USDT (1% от баланса)")
await error_max_risk(message)
else:
print(f"Риск в допустимых пределах. Qty = {next_quantity}")
r = client.place_order(
category='linear',
symbol=SYMBOL,
side='Buy',
orderType="Market",
leverage=int(data_main_stgs['size_leverage']),
qty=next_quantity,
takeProfit=takeProfit, # TP - закрывает позицию, когда цена достигает нужного уровня
stopProfit=float(data_risk_management_stgs['price_loss']), # SL - закрывает позицию, когда убыток достигает нужного уровня
orderLinkId=f"deal_{SYMBOL}_{time.time()}"
)
await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity)
except exceptions.InvalidRequestError as e:
await message.answer('Недостаточно баланса')
except Exception as e: except Exception as e:
await message.answer('Непредвиденная оишбка') logger.error(f"Ошибка при извлечении реализованной прибыли: {e}")
return 0.0
async def contract_short(tg_id, message, margin_mode):
api_key = await rq.get_bybit_api_key(tg_id) async def handle_execution_message(message, msg: dict) -> None:
secret_key = await rq.get_bybit_secret_key(tg_id) """
SYMBOL = await rq.get_symbol(tg_id) Обработчик сообщений об исполнении сделки.
Логирует событие и проверяет условия для мартингейла и TP.
"""
# logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}")
pnl = parse_pnl_from_msg(msg)
tg_id = message.from_user.id
data_main_stgs = await rq.get_user_main_settings(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id)
data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id) data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
take_profit_percent = safe_float(data_main_stgs.get('take_profit_percent', 2))
commission_fee = data_main_risk_stgs.get('commission_fee', "ДА")
symbol = await rq.get_symbol(tg_id)
api_key = await rq.get_bybit_api_key(tg_id)
api_secret = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=api_secret)
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions_list = positions_resp.get('result', {}).get('list', [])
position = positions_list[0] if positions_list else None
match margin_mode: trade_info = format_trade_details_position(msg, commission_fee=commission_fee)
case 'Isolated': await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
margin_mode = 'ISOLATED_MARGIN'
case 'Cross':
margin_mode = 'REGULAR_MARGIN'
client = HTTP( liquidation_threshold = -100
api_key=api_key,
api_secret=secret_key if pnl <= liquidation_threshold:
) current_step = int(await rq.get_martingale_step(tg_id))
current_step += 1
await rq.update_martingale_step(tg_id, current_step)
side = 'Buy' if position and position.get('side', '').lower() == 'long' else 'Sell'
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
elif position:
entry_price = safe_float(position.get('avgPrice'))
side = position.get('side', '')
current_price = float(position.get('markPrice', 0))
if side.lower() == 'long':
take_profit_trigger_price = entry_price * (1 + take_profit_percent / 100)
if current_price >= take_profit_trigger_price:
await close_user_trade(tg_id, symbol, message)
await rq.update_martingale_step(tg_id, 0)
elif side.lower() == 'short':
take_profit_trigger_price = entry_price * (1 - take_profit_percent / 100)
if current_price <= take_profit_trigger_price:
await close_user_trade(tg_id, symbol, message)
await rq.update_martingale_step(tg_id, 0)
async def error_max_step(message) -> None:
"""
Сообщение об ошибке превышения максимального количества шагов мартингейла.
"""
logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.')
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.',
reply_markup=inline_markup.back_to_main)
async def error_max_risk(message) -> None:
"""
Сообщение об ошибке превышения риск-лимита сделки.
"""
logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.')
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.',
reply_markup=inline_markup.back_to_main)
async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full'):
"""
Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска.
Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type')
limit_price = None
if order_type == 'Limit':
limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN'
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full')
except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e):
logger.info("Режим TP/SL уже установлен - пропускаем")
else:
raise
try: try:
balance = 0 balance = await balance_g.get_balance(tg_id, message)
price = 0 price = await price_symbol.get_price(tg_id)
balance = await balance_g.get_balance(tg_id) client.set_margin_mode(setMarginMode=bybit_margin_mode)
price = await price_symbol.get_price(tg_id, message)
client.set_margin_mode( martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
setMarginMode=margin_mode # margin_type max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0))
) starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
martingale_factor = float(data_main_stgs['martingale_factor']) # Исправлено: было maximal_quantity max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal'))
max_martingale_steps = int(data_main_stgs['maximal_quantity']) loss_profit = safe_float(data_risk_stgs.get('price_loss'))
starting_quantity = float(data_main_stgs['starting_quantity']) take_profit = safe_float(data_risk_stgs.get('price_profit'))
max_risk_percent = float(data_risk_management_stgs['max_risk_deal']) positions_resp = client.get_positions(category='linear', symbol=symbol)
loss_profit = float(data_risk_management_stgs['price_loss']) positions_list = positions_resp.get('result', {}).get('list', [])
takeProfit = float(data_risk_management_stgs['price_profit'])
# Инициализация переменных if positions_list:
position = positions_list[0]
size = safe_float(position.get('size', 0))
side_pos = position.get('side', '')
if size > 0 and side_pos:
entry_price = safe_float(position.get('avgPrice', price))
else:
entry_price = price
else:
entry_price = price
if order_type == 'Market':
base_price = entry_price
else:
base_price = limit_price
if side.lower() == 'buy':
take_profit_price = base_price * (1 + take_profit / 100)
stop_loss_price = base_price * (1 - loss_profit / 100)
else:
take_profit_price = base_price * (1 - take_profit / 100)
stop_loss_price = base_price * (1 + loss_profit / 100)
take_profit_price = max(take_profit_price, 0)
stop_loss_price = max(stop_loss_price, 0)
current_martingale_step = 0
next_quantity = starting_quantity next_quantity = starting_quantity
last_quantity = starting_quantity
realised_pnl = 0.0 realised_pnl = 0.0
current_martingale_step = 0 # Текущая ставка в серии current_martingale = await rq.get_martingale_step(tg_id)
current_martingale_step = int(current_martingale)
next_quantity = 0
realised_pnl = 0
last_quantity = starting_quantity
# Пример расчёта следующего размера позиции
try:
position_info = client.get_positions(category='linear', symbol=SYMBOL)
position = position_info['result']['list'][0] # или другой нужный индекс
realised_pnl = float(position['unrealisedPnl'])
if positions_list:
if realised_pnl > 0: if realised_pnl > 0:
starting_quantity = next_quantity
current_martingale_step = 0 current_martingale_step = 0
elif not realised_pnl:
next_quantity = starting_quantity next_quantity = starting_quantity
current_martingale_step += 1
else: else:
current_martingale_step += 1 current_martingale_step += 1
next_quantity = last_quantity * martingale_factor if current_martingale_step > max_martingale_steps:
starting_quantity = next_quantity await error_max_step(message)
except Exception as e: return
print("Не получены позиции") next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step)
next_quantity = starting_quantity
potential_loss = (next_quantity * float(price)) * (loss_profit / 100)
allowed_loss = float(balance) * (max_risk_percent / 100)
if current_martingale_step >= max_martingale_steps:
print("Достигнут максимум ставок в серии (8)!")
print("Торговля не продолжится")
await error_max_step(message)
else: else:
if potential_loss > allowed_loss: next_quantity = starting_quantity
print(f"ОШИБКА: Риск превышен!") current_martingale_step = 0
print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT")
print(f"Разрешено = {allowed_loss} USDT (1% от баланса)")
await error_max_risk(message) potential_loss = safe_float(next_quantity) * safe_float(price) * (loss_profit / 100)
allowed_loss = safe_float(balance) * (max_risk_percent / 100)
if potential_loss > allowed_loss:
await error_max_risk(message)
return
instruments_resp = client.get_instruments_info(category='linear', symbol=symbol)
if instruments_resp.get('retCode') == 0:
instrument_info = instruments_resp.get('result', {}).get('list', [])
if instrument_info:
instrument = instrument_info[0]
min_order_qty = float(instrument.get('minOrderQty', 0))
min_order_value_api = float(instrument.get('minOrderValue', 0))
if min_order_value_api == 0:
min_order_value_api = 5.0
# Рассчитываем по формуле:
min_order_value_calc = min_order_qty * price if min_order_qty > 0 else 0
# Минимальное значение из значений параметров на бирже
min_order_value = max(min_order_value_calc, min_order_value_api)
else: else:
print(f"Риск в допустимых пределах. Qty = {next_quantity}") min_order_value = 5.0
r = client.place_order( order_value = float(next_quantity) * float(price)
category='linear',
symbol=SYMBOL,
side='Sell',
orderType="Market",
leverage=int(data_main_stgs['size_leverage']),
qty=next_quantity,
orderLinkId=f"deal_{SYMBOL}_{time.time()}"
)
await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity) if order_value < min_order_value:
await message.answer(
f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
f"Минимум для торговли — {min_order_value} USDT. "
f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main)
return False
leverage = int(data_main_stgs.get('size_leverage', 1))
try:
resp = client.set_leverage(
category='linear',
symbol=symbol,
buyLeverage=str(leverage),
sellLeverage=str(leverage)
)
except exceptions.InvalidRequestError as e:
if "110043" in str(e):
logger.info(f"Leverage already set to {leverage} for {symbol}")
else:
raise e
if tpsl_mode == 'Full':
tp_order_type = 'Market'
sl_order_type = 'Market'
tp_limit_price = None
sl_limit_price = None
else: # Partial
tp_order_type = 'Limit'
sl_order_type = 'Limit'
tp_limit_price = take_profit_price
sl_limit_price = stop_loss_price
response = client.place_order(
category='linear',
symbol=symbol,
side=side,
orderType=order_type,
qty=str(next_quantity),
price=str(limit_price) if order_type == 'Limit' and limit_price else None,
takeProfit=str(take_profit_price),
tpOrderType=tp_order_type,
tpLimitPrice=str(tp_limit_price) if tp_limit_price else None,
stopLoss=str(stop_loss_price),
slOrderType=sl_order_type,
slLimitPrice=str(sl_limit_price) if sl_limit_price else None,
tpslMode=tpsl_mode,
timeInForce='GTC',
orderLinkId=f"deal_{symbol}_{int(time.time())}"
)
if response.get('retCode', -1) == 0:
await rq.update_martingale_step(tg_id, current_martingale_step)
return True
else:
logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main)
return False
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
await message.answer('Недостаточно баланса') logger.error(f"InvalidRequestError: {e}")
await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.',
reply_markup=inline_markup.back_to_main)
except Exception as e: except Exception as e:
await message.answer('Непредвиденная оишбка') logger.error(f"Ошибка при совершении сделки: {e}")
await message.answer('Возникла ошибка при попытке открыть позицию.', reply_markup=inline_markup.back_to_main)
async def trading_cycle(tg_id, message):
"""
Цикл торговой логики с учётом таймера пользователя.
"""
try:
timer_data = await rq.get_user_timer(tg_id)
timer_min = 0
if isinstance(timer_data, dict):
timer_min = timer_data.get('timer_minutes') or timer_data.get('timer') or 0
else:
timer_min = timer_data or 0
timer_sec = timer_min * 60
if timer_sec > 0:
await asyncio.sleep(timer_sec)
data_main_stgs = await rq.get_user_main_settings(tg_id)
side = 'Buy' if data_main_stgs.get('trading_mode', '') == 'Long' else 'Sell'
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
except asyncio.CancelledError:
logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.")
async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: float, stop_loss_price: float,
tpsl_mode='Full'):
"""
Устанавливает уровни Take Profit и Stop Loss для открытой позиции.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type')
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
limit_price = None
if order_type == 'Limit':
limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
trading_mode = data_main_stgs.get('trading_mode')
side = None
if trading_mode == 'Long':
side = 'Buy'
elif trading_mode == 'Short':
side = 'Sell'
if side is None:
await message.answer("Не удалось определить сторону сделки.")
return
client = HTTP(api_key=api_key, api_secret=secret_key)
await cancel_all_tp_sl_orders(tg_id, symbol)
try:
try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode=tpsl_mode)
except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e).lower():
logger.info(f"Режим TP/SL уже установлен для {symbol}")
else:
raise
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions = positions_resp.get('result', {}).get('list', [])
if not positions or abs(float(positions[0].get('size', 0))) == 0:
params = dict(
category='linear',
symbol=symbol,
side=side,
orderType=order_type,
qty=str(starting_quantity),
timeInForce='GTC',
orderLinkId=f"deal_{symbol}_{int(time.time())}",
takeProfit=str(take_profit_price),
stopLoss=str(stop_loss_price),
tpOrderType='Limit' if tpsl_mode == 'Partial' else 'Market',
slOrderType='Limit' if tpsl_mode == 'Partial' else 'Market',
tpslMode=tpsl_mode
)
if order_type == 'Limit' and limit_price is not None:
params['price'] = str(limit_price)
if tpsl_mode == 'Partial':
params['tpLimitPrice'] = str(take_profit_price)
params['slLimitPrice'] = str(stop_loss_price)
response = client.place_order(**params)
if response.get('retCode') != 0:
await message.answer(f"Ошибка создания ордера с TP/SL: {response.get('retMsg')}",
reply_markup=inline_markup.back_to_main)
return
else:
resp = client.set_trading_stop(
category='linear',
symbol=symbol,
takeProfit=str(round(take_profit_price, 5)),
stopLoss=str(round(stop_loss_price, 5)),
tpTriggerBy='LastPrice',
slTriggerBy='LastPrice',
reduceOnly=False
)
if resp.get('retCode') != 0:
await message.answer(f"Ошибка обновления TP/SL: {resp.get('retMsg')}",
reply_markup=inline_markup.back_to_main)
return
await message.answer(
f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}",
reply_markup=inline_markup.back_to_main)
except Exception as e:
logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True)
await message.answer("Произошла ошибка при установке TP и SL.", reply_markup=inline_markup.back_to_main)
async def cancel_all_tp_sl_orders(tg_id, symbol):
"""
Отменяет все открытые ордера TP/SL для указанного символа.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
last_response = None
try:
orders_resp = client.get_open_orders(category='linear', symbol=symbol)
orders = orders_resp.get('result', {}).get('list', [])
for order in orders:
order_id = order.get('orderId')
cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id)
last_response = cancel_resp
if cancel_resp.get('retCode') != 0:
logger.warning(f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}")
except Exception as e:
logger.error(f"Ошибка при отмене ордеров TP/SL: {e}")
return last_response
async def get_active_positions_by_symbol(tg_id, message):
"""
Показывает активные позиции пользователя по символу.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
symbol = await rq.get_symbol(tg_id)
active_positions = client.get_positions(category='linear', symbol=symbol)
positions = active_positions.get('result', {}).get('list', [])
pos = positions[0] if positions else None
if float(pos.get('size', 0)) == 0:
await message.answer("❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main)
return
text = (
f"Торговая пара: {pos.get('symbol')}\n"
f"Цена входа: {pos.get('avgPrice')}\n"
f"Движение: {pos.get('side')}\n"
f"Кредитное плечо: {pos.get('leverage')}x\n"
f"Количество: {pos.get('size')}\n"
f"Тейк-профит: {pos.get('takeProfit')}\n"
f"Стоп-лосс: {pos.get('stopLoss')}\n"
)
await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol))
async def get_active_orders_by_symbol(tg_id, message):
"""
Показывает активные лимитные ордера пользователя по символу.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
active_orders = client.get_open_orders(category='linear', symbol=symbol)
limit_orders = [
order for order in active_orders.get('result', {}).get('list', [])
if order.get('orderType') == 'Limit'
]
if not limit_orders:
await message.answer("Нет активных лимитных ордеров по данной торговой паре.",
reply_markup=inline_markup.back_to_main)
return
texts = []
for order in limit_orders:
text = (
f"Торговая пара: {order.get('symbol')}\n"
f"Тип ордера: {order.get('orderType')}\n"
f"Сторона: {order.get('side')}\n"
f"Цена: {order.get('price')}\n"
f"Количество: {order.get('qty')}\n"
f"Тейк-профит: {order.get('takeProfit')}\n"
f"Стоп-лосс: {order.get('stopLoss')}\n"
f"Кредитное плечо: {order.get('leverage')}\n"
)
texts.append(text)
await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_deal_markup(symbol))
async def close_user_trade(tg_id: int, symbol: str, message):
"""
Закрывает открытые позиции пользователя по символу рыночным ордером.
Возвращает True при успехе, False при ошибках.
"""
try:
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
limit_price = await rq.get_limit_price(tg_id)
include_fee = data_risk_stgs.get('commission_fee', 'Нет') == 'Да'
client = HTTP(api_key=api_key, api_secret=secret_key)
positions_resp = client.get_positions(category="linear", symbol=symbol)
if positions_resp.get('retCode') != 0:
return False
positions_list = positions_resp.get('result', {}).get('list', [])
if not positions_list:
return False
position = positions_list[0]
qty = abs(safe_float(position.get('size')))
side = position.get('side')
entry_price = safe_float(position.get('avgPrice'))
if qty == 0:
return False
orders = client.get_open_orders(category='linear', symbol=symbol)
cancel_resp = await cancel_all_tp_sl_orders(tg_id, symbol)
open_orders_list = orders.get('result', {}).get('list', [])
order_id = open_orders_list[0].get('orderId') if open_orders_list else None
close_side = "Sell" if side == "Buy" else "Buy"
ticker_resp = client.get_tickers(category="linear", symbol=symbol)
current_price = 0.0
if ticker_resp.get('retCode') == 0:
result = ticker_resp.get('result', {})
ticker_list = []
if isinstance(result, dict):
ticker_list = result.get('list', [])
elif isinstance(result, list):
ticker_list = result
if ticker_list:
current_price = float(ticker_list[0].get('lastPrice', 0.0))
place_resp = client.place_order(
category="linear",
symbol=symbol,
side=close_side,
orderType="Market",
qty=str(qty),
timeInForce="GTC",
reduceOnly=True
)
if place_resp.get('retCode', -1) == 0:
trade_fee = 0
try:
trades_resp = client.get_closed_pnl(category="linear", symbol=symbol)
if trades_resp.get('retCode') == 0:
trades = trades_resp.get('result', {}).get('list', [])
for trade in trades:
if trade.get('orderId') == order_id:
trade_fee += float(trade.get('execFee', 0))
except Exception as e:
logger.error(f"Ошибка при получении сделок: {e}")
trade_fee = 0
pnl = (current_price - entry_price) * qty if side == "Buy" else (entry_price - current_price) * qty
if include_fee:
pnl -= trade_fee
pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0
return True
else:
if message:
await message.answer(f"Ошибка закрытия сделки {symbol}.",
reply_markup=inline_markup.back_to_main)
return False
except Exception as e:
logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True)
if message:
await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main)
return False
async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int):
"""
Закрывает сделку пользователя после задержки delay_sec секунд.
"""
try:
await asyncio.sleep(delay_sec)
result = await close_user_trade(tg_id, symbol, message)
if result:
await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.",
reply_markup=inline_markup.back_to_main)
else:
await message.answer(f"Не удалось закрыть сделку {symbol} по таймеру.",
reply_markup=inline_markup.back_to_main)
except asyncio.CancelledError:
await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main)

View File

@@ -1,10 +1,29 @@
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP
client = HTTP() logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("balance")
async def get_balance(tg_id, message):
async def get_balance(tg_id: int, message) -> float:
"""
Асинхронно получает общий баланс пользователя на Bybit.
Процедура:
- Получает API ключ и секрет пользователя из базы данных.
- Если ключи не заданы, отправляет пользователю сообщение с предложением подключить платформу.
- Создает клиент Bybit с ключами.
- Запрашивает общий баланс по типу аккаунта UNIFIED.
- Если ответ успешен, возвращает баланс в виде float.
- При ошибках API или исключениях логирует ошибку и уведомляет пользователя.
:param tg_id: int - идентификатор пользователя Telegram
:param message: объект сообщения для отправки ответов пользователю
:return: float - общий баланс пользователя; 0 при ошибке или отсутствии ключей
"""
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
@@ -14,20 +33,20 @@ async def get_balance(tg_id, message):
) )
if api_key == 'None' or secret_key == 'None': if api_key == 'None' or secret_key == 'None':
await message.answer('⚠️ Подключите платформу для торговли') await message.answer('⚠️ Подключите платформу для торговли',
reply_markup=inline_markup.connect_bybit_api_markup)
return 0 return 0
try: try:
check_user = client.get_wallet_balance() response = client.get_wallet_balance(accountType='UNIFIED')
if response['retCode'] == 0:
if check_user: total_balance = response['result']['list'][0].get('totalWalletBalance', '0')
try: return total_balance
balance = client.get_wallet_balance(accountType='UNIFIED', coin='USDT')['result']['list'][0]['coin'][0]['walletBalance'] else:
logger.error(f"Ошибка API: {response.get('retMsg')}")
return balance await message.answer(f"⚠️ Ошибка API: {response.get('retMsg')}")
except Exception as e: return 0
await message.answer('⚠️ Ошибка при получении баланса пользователя')
return 0
except Exception as e: except Exception as e:
await message.answer('⚠️ Неверные данные API, перепроверьте их') logger.error(f"Ошибка при получении общего баланса: {e}")
await message.answer('⚠️ Ошибка при получении баланса')
return 0 return 0

View File

@@ -0,0 +1,47 @@
import asyncio
import logging.config
from pybit.unified_trading import WebSocket
from websocket import WebSocketConnectionClosedException
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("bybit_ws")
event_loop = None # Сюда нужно будет установить event loop из основного приложения
def set_event_loop(loop: asyncio.AbstractEventLoop):
global event_loop
event_loop = loop
def on_execution_callback(message, msg):
"""
Callback на событие исполнения сделки.
Безопасно запускает асинхронный обработчик из sync callback.
"""
if event_loop is not None:
from app.services.Bybit.functions.Futures import handle_execution_message # Импорт внутри, чтобы избежать циклических импортов
asyncio.run_coroutine_threadsafe(handle_execution_message(message, msg), event_loop)
else:
logger.error("Event loop не установлен, callback пропущен.")
async def start_execution_ws(api_key: str, api_secret: str, message):
"""
Запускает и поддерживает WebSocket подключение для исполнения сделок.
Реконнект при потерях соединения.
"""
reconnect_delay = 5
while True:
try:
ws = WebSocket(api_key=api_key, api_secret=api_secret, testnet=False, channel_type="private")
ws.execution_stream(lambda msg: on_execution_callback(message, msg))
while True:
await asyncio.sleep(1) # Поддержание активности
except WebSocketConnectionClosedException:
logger.warning("WebSocket закрыт, переподключение через 5 секунд...")
await asyncio.sleep(reconnect_delay)
except Exception as e:
logger.error(f"Ошибка WebSocket: {e}")
await asyncio.sleep(reconnect_delay)

View File

@@ -1,21 +0,0 @@
import app.telegram.database.requests as rq
import app.services.Bybit.functions.price_symbol as price_s
from pybit.unified_trading import HTTP
client = HTTP()
async def get_min_qty(tg_id, message):
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
SYMBOL = await rq.get_symbol(tg_id)
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
price = await price_s.get_price(tg_id, message)
min_qty = int(5 / price * 1.1)
return min_qty

View File

@@ -1,99 +1,515 @@
from aiogram import F, Router import asyncio
import logging.config
from aiogram import F, Router
from app.services.Bybit.functions import Futures, func_min_qty from app.tasks.tasks import handle_stop_close_trade, handle_start_close_trade, handle_stop_trading, handle_start_trading
from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions.Futures import (close_user_trade, open_position, set_take_profit_stop_loss, \
get_active_positions_by_symbol, get_active_orders_by_symbol,
)
from app.services.Bybit.functions.balance import get_balance from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
# FSM - Механизм состояния from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
from aiogram.fsm.state import State, StatesGroup SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("functions")
router_functions_bybit_trade = Router() router_functions_bybit_trade = Router()
class state_update_symbol(StatesGroup):
symbol = State()
@router_functions_bybit_trade.callback_query(F.data == 'clb_start_trading') @router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMContext): async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
api = await rq.get_bybit_api_key(callback.from_user.id) """
secret = await rq.get_bybit_secret_key(callback.from_user.id) Обработка нажатия кнопок запуска торговли или возврата в главное меню.
balance = await get_balance(callback.from_user.id, callback.message) Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message)
price = await get_price(user_id)
if balance: if balance:
symbol = await rq.get_symbol(callback.from_user.id) symbol = await rq.get_symbol(user_id)
text = f'''💎 Торговля на Bybit text = (
f"💎 Торговля на Bybit\n\n"
⚖️ Ваш баланс (USDT): {balance} f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
📊 Текущая торговая пара: {symbol} f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
Как начать торговлю? "Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле. "2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT). "3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
''' )
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
async def start_bybit_trade_message(message, state):
api = await rq.get_bybit_api_key(message.from_user.id) async def start_bybit_trade_message(message: Message) -> None:
secret = await rq.get_bybit_secret_key(message.from_user.id) """
Отправляет пользователю информацию о балансе, символе и текущей цене,
вместе с инструкциями по началу торговли.
"""
balance = await get_balance(message.from_user.id, message) balance = await get_balance(message.from_user.id, message)
price = await get_price(message.from_user.id)
if balance: if balance:
symbol = await rq.get_symbol(message.from_user.id) symbol = await rq.get_symbol(message.from_user.id)
text = f'''💎 Торговля на Bybit text = (
f"💎 Торговля на Bybit\n\n"
⚖️ Ваш баланс (USDT): {balance} f"⚖️ Ваш баланс (USDT): {balance}\n"
📊 Текущая торговая пара: {symbol} f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
Как начать торговлю? "Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле. "2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT). "3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
''' )
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair') @router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext): async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
"""
await state.set_state(state_update_symbol.symbol) await state.set_state(state_update_symbol.symbol)
await callback.message.answer(text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ') await callback.message.answer(
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(state_update_symbol.symbol) @router_functions_bybit_trade.message(state_update_symbol.symbol)
async def update_symbol_for_trade(message: Message, state: FSMContext): async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
await state.update_data(symbol = message.text) """
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
При успешном обновлении сохранит пару и отправит обновлённую информацию.
"""
user_input = message.text.strip().upper()
data = await state.get_data() exists = await get_valid_symbols(message.from_user.id, user_input)
if not exists:
await message.answer("Введена некорректная торговая пара или такой пары нет в списке. Попробуйте снова.")
return
await state.update_data(symbol=message.text)
await message.answer('Пара была успешно обновлена') await message.answer('Пара была успешно обновлена')
await rq.update_symbol(message.from_user.id, data['symbol']) await rq.update_symbol(message.from_user.id, user_input)
await start_bybit_trade_message(message, state) await start_bybit_trade_message(message)
await state.clear() await state.clear()
@router_functions_bybit_trade.callback_query(F.data == 'clb_open_deal')
async def make_deal_bybit (callback: CallbackQuery):
data_main_stgs = await rq.get_user_main_settings(callback.from_user.id)
trade_mode = data_main_stgs['trading_mode'] @router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type')
qty = data_main_stgs['starting_quantity'] async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
margin_mode = data_main_stgs['margin_type'] """
qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) Запрашивает у пользователя тип входа в позицию (Market или Limit).
"""
await state.set_state(state_update_entry_type.entry_type)
await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup)
await callback.answer()
if qty < qty_min:
await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:'))
async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработка выбора типа входа в позицию.
Если Limit, запрашивает цену лимитного ордера.
Если Market — обновляет настройки.
"""
order_type = callback.data.split(':')[1]
if order_type not in ['Market', 'Limit']:
await callback.answer("Ошибка выбора", show_alert=True)
return
if order_type == 'Limit':
await state.set_state(state_limit_price.price)
await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel)
await callback.answer()
return
try:
await state.update_data(entry_order_type=order_type)
await rq.update_entry_order_type(callback.from_user.id, order_type)
await callback.message.answer(f"Выбран тип входа в позицию: {order_type}",
reply_markup=inline_markup.start_trading_markup)
await callback.answer()
except Exception as e:
logger.error(f"Произошла ошибка при обновлении типа входа в позицию: {e}")
await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию",
reply_markup=inline_markup.back_to_main)
await state.clear()
@router_functions_bybit_trade.message(state_limit_price.price)
async def set_limit_price(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки.
"""
try:
price = float(message.text)
if price <= 0:
await message.answer("Цена должна быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel)
return
await state.update_data(entry_order_type='Limit', limit_price=price)
await rq.update_entry_order_type(message.from_user.id, 'Limit')
await rq.update_limit_price(message.from_user.id, price)
await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.start_trading_markup)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
async def start_trading_process(callback: CallbackQuery) -> None:
"""
Запускает торговый цикл в выбранном режиме Long/Short.
Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
затем запускает торговый цикл с задержкой или без неё.
"""
tg_id = callback.from_user.id
message = callback.message
data_main_stgs = await rq.get_user_main_settings(tg_id)
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
trading_mode = data_main_stgs.get('trading_mode')
if not api_key or not secret_key:
await message.answer("❗️ У вас не настроены API ключи для Bybit.")
await callback.answer()
return
if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']:
await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}")
await callback.answer()
return
if margin_mode not in ['Isolated', 'Cross']:
margin_mode = 'Isolated'
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions = positions_resp.get('result', {}).get('list', [])
except Exception as e:
logger.error(f"Ошибка при получении позиций: {e}")
positions = []
for pos in positions:
size = pos.get('size')
existing_margin_mode = pos.get('margin_mode')
if size and float(size) > 0 and existing_margin_mode and existing_margin_mode != margin_mode:
await callback.answer(
f"⚠️ Маржинальный режим нельзя менять при открытой позиции "
f"(текущий режим: {existing_margin_mode})", show_alert=True)
return
if trading_mode == 'Long':
side = 'Buy'
elif trading_mode == 'Short':
side = 'Sell'
else: else:
match trade_mode: await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
case 'Long': reply_markup=inline_markup.back_to_main)
await Futures.contract_long(callback.from_user.id, callback.message, margin_mode) await callback.answer()
case 'Short': return
await Futures.contract_short(callback.from_user.id, callback.message, margin_mode)
case 'Switch': await message.answer("Начинаю торговлю с использованием текущих настроек...")
await callback.message.edit_text('Режим Switch пока недоступен')
case 'Smart':
await callback.message.edit_text('Режим Smart пока недоступен') timer_data = await rq.get_user_timer(tg_id)
if isinstance(timer_data, dict):
timer_minute = timer_data.get('timer_minutes', 0)
else:
timer_minute = timer_data or 0
if timer_minute > 0:
await handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=True)
await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.",
reply_markup=inline_markup.cancel_start_markup)
await rq.update_user_timer(tg_id, minutes=0)
else:
await handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=False)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
async def show_my_trades(callback: CallbackQuery) -> None:
"""
Отображает пользователю выбор типа сделки по текущей торговой паре.
"""
await callback.answer()
try:
symbol = await rq.get_symbol(callback.from_user.id)
await callback.message.answer(f"Выберите тип сделки для пары {symbol}:",
reply_markup=inline_markup.my_deals_select_markup)
except Exception as e:
logger.error(f"Произошла ошибка при выборе типа сделки: {e}")
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deals")
async def show_my_trades_callback(callback: CallbackQuery):
"""
Показывает открытые позиции пользователя по символу.
"""
await callback.answer()
try:
await get_active_positions_by_symbol(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_open_orders")
async def show_my_orders_callback(callback: CallbackQuery) -> None:
"""
Показывает открытые позиции пользователя по символу.
"""
await callback.answer()
try:
await get_active_orders_by_symbol(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе ордера: {e}")
await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl")
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
"""
Показывает активные ордера пользователя.
"""
await callback.answer()
await state.set_state(SetTP_SL_State.waiting_for_take_profit)
await callback.message.answer("Введите значение Take Profit (в цене, например 26000.5):",
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_take_profit)
async def process_take_profit(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Take Profit и запрашивает Stop Loss.
"""
try:
tp = float(message.text.strip())
if tp <= 0:
await message.answer("Значение Take Profit должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Take Profit.",
reply_markup=inline_markup.cancel)
return
await state.update_data(take_profit=tp)
await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
async def process_stop_loss(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
"""
try:
sl = float(message.text.strip())
if sl <= 0:
await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
reply_markup=inline_markup.cancel)
return
data = await state.get_data()
tp = data.get("take_profit")
if tp is None:
await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
await state.clear()
return
tg_id = message.from_user.id
await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_timer")
async def cancel_start_callback(callback: CallbackQuery) -> None:
"""
Отменяет задачу старта торговли по таймеру, если она активна.
"""
tg_id = callback.from_user.id
await handle_stop_close_trade(tg_id)
await callback.message.answer("Торговля по таймеру отменена.", reply_markup=inline_markup.back_to_main)
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает сделку пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id
result = await close_user_trade(tg_id, symbol, message=callback.message)
if result:
await handle_stop_trading(tg_id)
logger.info(f"Сделка {symbol} успешно закрыта.")
else:
logger.error(f"Не удалось закрыть сделку {symbol}.")
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:"))
async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает диалог с пользователем для задания задержки перед закрытием сделки.
"""
symbol = callback.data.split(":")[1]
await state.update_data(symbol=symbol)
await state.set_state(CloseTradeTimerState.waiting_for_delay)
await callback.message.answer("Введите задержку в минутах до закрытия сделки (например, 60):",
reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
async def process_close_delay(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод задержки и запускает задачу закрытия сделки с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число.")
return
except ValueError:
await message.answer("Некорректный ввод. Введите число в минутах.")
return
data = await state.get_data()
symbol = data.get("symbol")
tg_id = message.from_user.id
delay = delay_minutes * 60
await handle_start_close_trade(tg_id, message, symbol, delay)
await message.answer(f"Закрытие сделки {symbol} запланировано через {delay} секунд.",
reply_markup=inline_markup.cancel_start_markup)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
async def reset_martingale(callback: CallbackQuery) -> None:
"""
Сбрасывает шаги мартингейла пользователя.
"""
tg_id = callback.from_user.id
await rq.update_martingale_step(tg_id, 0)
await callback.answer("Сброс шагов выполнен.")
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading")
async def confirm_stop_trading(callback: CallbackQuery):
"""
Предлагает пользователю выбрать вариант подтверждение остановки торговли.
"""
await callback.message.answer(
"Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup
)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_immediately")
async def stop_immediately(callback: CallbackQuery):
"""
Останавливает торговлю немедленно.
"""
tg_id = callback.from_user.id
await handle_stop_trading(tg_id, use_timer=False)
await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
"""
Запускает диалог с пользователем для задания задержки перед остановкой торговли.
"""
await state.set_state(CloseTradeTimerState.waiting_for_delay)
await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
async def process_stop_delay(message: Message, state: FSMContext):
"""
Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число минут.")
return
except ValueError:
await message.answer("Некорректный формат. Введите число в минутах.")
return
tg_id = message.from_user.id
delay_seconds = delay_minutes * 60
# Остановка задачи с таймером через заданную задержку
# Можно реализовать через запуск отдельной асинхронной задачи, которая через delay_seconds отменит торговый цикл
async def delayed_stop():
await asyncio.sleep(delay_seconds)
await handle_stop_trading(tg_id, use_timer=True)
await message.answer("Торговля по таймеру остановлена.")
asyncio.create_task(delayed_stop())
await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", reply_markup=inline_markup.back_to_main)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
"""
Отменяет текущее состояние FSM и сообщает пользователю об отмене.
"""
await state.clear()
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
await callback.answer()

View File

@@ -0,0 +1,40 @@
import logging.config
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("get_valid_symbol")
async def get_valid_symbols(user_id: int, symbol: str) -> bool:
"""
Проверяет существование торговой пары на Bybit в категории 'linear'.
Эта функция получает API-ключи пользователя из базы данных и
с помощью Bybit API проверяет наличие данного символа в списке
торговых инструментов категории 'linear'.
Args:
user_id (int): Идентификатор пользователя Telegram.
symbol (str): Торговый символ (валютная пара), например "BTCUSDT".
Returns:
bool: Возвращает True, если торговая пара существует, иначе False.
Raises:
Исключения подавляются и вызывается False, если произошла ошибка запроса к API.
"""
api_key = await rq.get_bybit_api_key(user_id)
secret_key = await rq.get_bybit_secret_key(user_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
resp = client.get_instruments_info(category='linear', symbol=symbol)
# Проверка наличия результата и непустого списка инструментов
if resp.get('retCode') == 0 and resp.get('result') and resp['result'].get('list'):
return len(resp['result']['list']) > 0
return False
except Exception as e:
logging.error(f"Ошибка при получении списка инструментов: {e}")
return False

View File

@@ -1,23 +1,52 @@
from app.services.Bybit.functions import price_symbol import math
import logging.config
from app.services.Bybit.functions.price_symbol import get_price
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP
client = HTTP() logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("min_qty")
async def get_min_qty(tg_id): def round_up_qty(value: float, step: float) -> float:
"""
Округление value вверх до ближайшего кратного step.
"""
return math.ceil(value / step) * step
async def get_min_qty(tg_id: int) -> float:
"""
Получает минимальный объем (количество) ордера для символа пользователя на Bybit,
округленное с учетом шага количества qtyStep.
:param tg_id: int - идентификатор пользователя Telegram
:return: float - минимальное количество лота для ордера
"""
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
SYMBOL = await rq.get_symbol(tg_id) symbol = await rq.get_symbol(tg_id)
client = HTTP( client = HTTP(api_key=api_key, api_secret=secret_key)
api_key=api_key,
api_secret=secret_key
)
price = await price_symbol(tg_id) price = await get_price(tg_id)
json_data = client.get_instruments_info(symbol=SYMBOL, category='linear')
min_qty = int(5 / price * 1.1) # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 1% <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 5 USDT response = client.get_instruments_info(symbol=symbol, category='linear')
return min_qty instrument = response['result'][0]
lot_size_filter = instrument.get('lotSizeFilter', {})
min_order_qty = float(lot_size_filter.get('minOrderQty', 0))
min_notional_value = float(lot_size_filter.get('minNotionalValue', 0))
qty_step = float(lot_size_filter.get('qtyStep', 1))
calculated_qty = (5 / price) * 1.1
min_qty = max(min_order_qty, calculated_qty)
min_qty_rounded = round_up_qty(min_qty, qty_step)
logger.debug(f"tg_id={tg_id}: price={price}, min_order_qty={min_order_qty}, "
f"min_notional_value={min_notional_value}, qty_step={qty_step}, "
f"calculated_qty={calculated_qty}, min_qty_rounded={min_qty_rounded}")
return min_qty_rounded

View File

@@ -1,14 +1,23 @@
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit import exceptions from pybit import exceptions
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP
client = HTTP() logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("price_symbol")
async def get_price(tg_id, message):
async def get_price(tg_id: int) -> float:
"""
Асинхронно получает текущую цену символа пользователя на Bybit.
:param tg_id: int - ID пользователя Telegram
:return: float - текущая цена символа
"""
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
SYMBOL = await rq.get_symbol(tg_id) symbol = await rq.get_symbol(tg_id)
client = HTTP( client = HTTP(
api_key=api_key, api_key=api_key,
@@ -16,9 +25,9 @@ async def get_price(tg_id, message):
) )
try: try:
price = float(client.get_tickers(category='linear', symbol=SYMBOL).get('result').get('list')[0].get('ask1Price')) price = float(
client.get_tickers(category='linear', symbol=symbol).get('result').get('list')[0].get('ask1Price'))
return price return price
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
await message.answer('Неверно указана торговая пара') logger.error(f"Ошибка при получении цены: {e}")
return 1.0 return 1.0

47
app/states/States.py Normal file
View File

@@ -0,0 +1,47 @@
from aiogram.fsm.state import State, StatesGroup
class state_update_symbol(StatesGroup):
"""FSM состояние для обновления торгового символа."""
symbol = State()
class state_update_entry_type(StatesGroup):
"""FSM состояние для обновления типа входа."""
entry_type = State()
class TradeSetup(StatesGroup):
"""FSM состояния для настройки торговли с таймером и процентом."""
waiting_for_timer = State()
waiting_for_positive_percent = State()
class state_limit_price(StatesGroup):
"""FSM состояние для установки лимита."""
price = State()
class CloseTradeTimerState(StatesGroup):
"""FSM состояние ожидания задержки перед закрытием сделки."""
waiting_for_delay = State()
class SetTP_SL_State(StatesGroup):
"""FSM состояние для установки TP и SL."""
waiting_for_take_profit = State()
waiting_for_stop_loss = State()
class update_risk_management_settings(StatesGroup):
"""FSM состояние для обновления настроек управления рисками."""
price_profit = State()
price_loss = State()
max_risk_deal = State()
commission_fee = State()
class state_reg_bybit_api(StatesGroup):
"""FSM состояние для регистрации API Bybit."""
api_key = State()
secret_key = State()

125
app/tasks/tasks.py Normal file
View File

@@ -0,0 +1,125 @@
import asyncio
import logging.config
from typing import Optional
from app.services.Bybit.functions.Futures import close_trade_after_delay, trading_cycle, open_position
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("tasks")
active_start_tasks = {}
active_close_tasks = {}
active_start_tasks_timer = {}
lock_start_tasks = asyncio.Lock()
lock_close_tasks = asyncio.Lock()
def start_trading_cycle(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full') -> None:
"""
Запускает асинхронную задачу торгового цикла для пользователя с указанным tg_id.
"""
task = asyncio.create_task(open_position(tg_id, message, side, margin_mode, tpsl_mode))
active_start_tasks[tg_id] = task
def stop_trading_cycle(tg_id) -> None:
"""
Останавливает (отменяет) задачу торгового цикла для пользователя с указанным tg_id.
"""
task: Optional[asyncio.Task] = active_start_tasks.pop(tg_id, None)
if task:
task.cancel()
def start_trading_for_timer(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full') -> None:
# Старт с задержкой (trading_cycle)
task = asyncio.create_task(trading_cycle(tg_id, message))
active_start_tasks_timer[tg_id] = task
def stop_trading_for_timer(tg_id) -> None:
task: Optional[asyncio.Task] = active_start_tasks_timer.pop(tg_id, None)
if task:
task.cancel()
def start_close_trade_task(tg_id, message, symbol, delay_sec) -> None:
"""
Запускает асинхронную задачу автоматического закрытия сделки после задержки.
"""
task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay_sec))
active_close_tasks[tg_id] = task
def stop_close_trade_task(tg_id) -> None:
"""
Останавливает (отменяет) задачу автоматического закрытия сделки для пользователя.
"""
task: Optional[asyncio.Task] = active_close_tasks.pop(tg_id, None)
if task:
task.cancel()
async def handle_start_trading(tg_id: int, message, side: str, margin_mode: str, tpsl_mode='Full', use_timer=False):
"""
Запускает торговый цикл. Если уже есть запущенная задача, отменяет её.
"""
async with lock_start_tasks:
if use_timer:
old_task = active_start_tasks_timer.get(tg_id)
if old_task and not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
logger.info(f"Старая задача торговли с таймером для пользователя {tg_id} отменена")
start_trading_for_timer(tg_id, message, side, margin_mode, tpsl_mode)
logger.info(f"Новая задача торговли с таймером запущена для пользователя {tg_id}")
else:
old_task = active_start_tasks.get(tg_id)
if old_task and not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
logger.info(f"Старая задача торговли для пользователя {tg_id} отменена")
start_trading_cycle(tg_id, message, side, margin_mode, tpsl_mode)
logger.info(f"Новая задача торговли запущена для пользователя {tg_id}")
async def handle_stop_trading(tg_id: int, use_timer=False):
"""
Останавливает торговую задачу пользователя, если она активна.
"""
async with lock_start_tasks:
if use_timer:
stop_trading_for_timer(tg_id)
logger.info(f"Задача торговли с таймером остановлена для пользователя {tg_id}")
else:
stop_trading_cycle(tg_id)
logger.info(f"Задача торговли остановлена для пользователя {tg_id}")
async def handle_start_close_trade(tg_id: int, message, symbol: str, delay_sec: int):
"""
Запускает задачу закрытия сделки с задержкой. Отменяет старую задачу, если есть.
"""
async with lock_close_tasks:
old_task = active_close_tasks.get(tg_id)
if old_task and not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
logger.info(f"Старая задача закрытия сделки пользователя {tg_id} отменена")
start_close_trade_task(tg_id, message, symbol, delay_sec)
logger.info(f"Задача закрытия сделки для {symbol} запущена с задержкой {delay_sec}s для пользователя {tg_id}")
async def handle_stop_close_trade(tg_id: int):
"""
Отменяет задачу закрытия сделки пользователя, если она есть.
"""
async with lock_close_tasks:
stop_close_trade_task(tg_id)
logger.info(f"Задача закрытия сделки отменена для пользователя {tg_id}")

View File

@@ -1,4 +1,5 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
start_markup = InlineKeyboardMarkup(inline_keyboard=[ start_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")] [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")]
@@ -20,7 +21,7 @@ special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')],
back_btn_profile back_btn_profile
]) ])
connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[
@@ -28,12 +29,42 @@ connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[
]) ])
trading_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')],
[InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')],
[InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')],
[InlineKeyboardButton(text="Совершить сделку", callback_data='clb_open_deal')] [InlineKeyboardButton(text="Начать торговлю", callback_data='clb_update_entry_type')],
[InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')],
]) ])
back_btn_list_settings = [InlineKeyboardButton(text="Назад", callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[
back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад", callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек [InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
[InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")],
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
])
cancel = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Отменить", callback_data="clb_cancel")]
])
entry_order_type_markup = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"),
InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"),
],
]
)
back_btn_list_settings = [InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек
back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек
back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')]
back_to_main = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
])
main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),
@@ -43,23 +74,27 @@ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')], InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')],
[InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'),
InlineKeyboardButton(text='Максимльное кол-во ставок', callback_data='clb_change_maximum_quantity')], InlineKeyboardButton(text='Сбросить шаги Мартингейла', callback_data='clb_change_martingale_reset')],
[InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')],
back_btn_list_settings back_btn_list_settings,
back_btn_to_main
]) ])
risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'), [InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'),
InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')], InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')],
[InlineKeyboardButton(text='Иакс. риск на сделку', callback_data='clb_change_max_risk_deal')], [InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')],
[InlineKeyboardButton(text='Учитывать комиссию биржи (Да/Нет)', callback_data='commission_fee')],
back_btn_list_settings back_btn_list_settings,
back_btn_to_main
]) ])
condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'), [InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'),
InlineKeyboardButton(text='Фильтр времени', callback_data='clb_change_filter_time')], InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')],
[InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')], InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')],
@@ -69,7 +104,8 @@ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')], [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')],
back_btn_list_settings back_btn_list_settings,
back_btn_to_main
]) ])
additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
@@ -78,35 +114,83 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')], [InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')],
back_btn_list_settings back_btn_list_settings,
back_btn_to_main
]) ])
trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"), [InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"),
InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short")], InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short")],
[InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch"), [InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch"),
InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")],
back_btn_list_settings back_btn_list_settings,
back_btn_to_main
]) ])
margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[ margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"), [InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"),
InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")], InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")],
back_btn_list_settings back_btn_list_settings,
back_btn_to_main
]) ])
trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_ruchnoy"), InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")], [InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_ruchnoy"),
InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")],
[InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")] [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")]
]) ])
buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Да', callback_data="clb_yes"), InlineKeyboardButton(text='Нет', callback_data="clb_yes")] [InlineKeyboardButton(text='Да', callback_data="clb_yes"),
InlineKeyboardButton(text='Нет', callback_data="clb_no")],
]) ])
buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Включить', callback_data="clb_on"), InlineKeyboardButton(text='Выключить', callback_data="clb_off")] [InlineKeyboardButton(text='Включить', callback_data="clb_on"),
InlineKeyboardButton(text='Выключить', callback_data="clb_off")]
]) ])
my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"),
InlineKeyboardButton(text='Открытые ордера', callback_data="clb_open_orders")],
back_btn_to_main
])
def create_trades_inline_keyboard(trades):
builder = InlineKeyboardBuilder()
for trade in trades:
symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol
builder.button(text=symbol, callback_data=f"show_deal_{symbol}")
builder.adjust(2)
return builder.as_markup()
def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")],
[InlineKeyboardButton(text="Закрыть по таймеру", callback_data=f"close_deal_by_timer:{symbol}")],
[InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl")],
back_btn_to_main
])
timer_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
back_btn_to_main
])
cancel_start_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Отменить таймер", callback_data="clb_stop_timer")]
])
stop_choice_markup = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Остановить сразу", callback_data="stop_immediately"),
InlineKeyboardButton(text="Остановить по таймеру", callback_data="stop_with_timer"),
]
]
)

View File

@@ -3,4 +3,4 @@
base_buttons_markup = ReplyKeyboardMarkup(keyboard=[ base_buttons_markup = ReplyKeyboardMarkup(keyboard=[
[KeyboardButton(text="👤 Профиль")], [KeyboardButton(text="👤 Профиль")],
# [KeyboardButton(text="Настройки")] # [KeyboardButton(text="Настройки")]
], resize_keyboard=True) ], resize_keyboard=True, one_time_keyboard=False)

View File

@@ -1,27 +1,55 @@
import logging from datetime import datetime
logger = logging.getLogger(__name__) import logging.config
from sqlalchemy.sql.sqltypes import DateTime
from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from logger_helper.logger_helper import LOGGING_CONFIG
from sqlalchemy import select, insert from sqlalchemy import select, insert
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("models")
engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3') engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3')
async_session = async_sessionmaker(engine) async_session = async_sessionmaker(engine)
class Base(AsyncAttrs, DeclarativeBase): class Base(AsyncAttrs, DeclarativeBase):
"""Базовый класс для declarative моделей SQLAlchemy с поддержкой async."""
pass pass
class User_Telegram_Id(Base): class User_Telegram_Id(Base):
"""
Модель таблицы user_telegram_id.
Хранит идентификаторы Telegram пользователей.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Уникальный идентификатор пользователя Telegram.
"""
__tablename__ = 'user_telegram_id' __tablename__ = 'user_telegram_id'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(BigInteger) tg_id = mapped_column(BigInteger)
class User_Bybit_API(Base): class User_Bybit_API(Base):
"""
Модель таблицы user_bybit_api.
Хранит API ключи и секреты Bybit для каждого Telegram пользователя.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Внешний ключ на Telegram пользователя (user_telegram_id.tg_id).
api_key (str): API ключ Bybit (уникальный для пользователя).
secret_key (str): Секретный ключ Bybit (уникальный для пользователя).
"""
__tablename__ = 'user_bybit_api' __tablename__ = 'user_bybit_api'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -31,7 +59,26 @@ class User_Bybit_API(Base):
api_key = mapped_column(String(18), default='None') api_key = mapped_column(String(18), default='None')
secret_key = mapped_column(String(36), default='None') secret_key = mapped_column(String(36), default='None')
class User_Symbol(Base): class User_Symbol(Base):
"""
Модель таблицы user_main_settings.
Хранит основные настройки торговли для пользователя.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Внешний ключ на Telegram пользователя.
trading_mode (str): Режим торговли (ForeignKey на trading_modes.mode).
margin_type (str): Тип маржи (ForeignKey на margin_types.type).
size_leverage (int): Кредитное плечо.
starting_quantity (int): Начальный объём позиции.
martingale_factor (int): Коэффициент мартингейла.
martingale_step (int): Текущий шаг мартингейла.
maximal_quantity (int): Максимальное количество шагов мартингейла.
entry_order_type (str): Тип входа (Market или Limit).
limit_order_price (str, optional): Цена лимитного ордера, если используется.
"""
__tablename__ = 'user_symbols' __tablename__ = 'user_symbols'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -40,28 +87,69 @@ class User_Symbol(Base):
symbol = mapped_column(String(18), default='PENGUUSDT') symbol = mapped_column(String(18), default='PENGUUSDT')
class Trading_Mode(Base): class Trading_Mode(Base):
"""
Справочник доступных режимов торговли.
Атрибуты:
id (int): Первичный ключ.
mode (str): Уникальный режим (например, 'Long', 'Short').
"""
__tablename__ = 'trading_modes' __tablename__ = 'trading_modes'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
mode = mapped_column(String(10), unique=True) mode = mapped_column(String(10), unique=True)
class Margin_type(Base): class Margin_type(Base):
"""
Справочник типов маржинальной торговли.
Атрибуты:
id (int): Первичный ключ.
type (str): Тип маржи (например, 'Isolated', 'Cross').
"""
__tablename__ = 'margin_types' __tablename__ = 'margin_types'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
type = mapped_column(String(15), unique=True) type = mapped_column(String(15), unique=True)
class Trigger(Base): class Trigger(Base):
"""
Справочник видов триггеров для сделок.
Атрибуты:
id (int): Первичный ключ.
trigger (str): Название триггера (например, 'Ручной', 'Автоматический').
"""
__tablename__ = 'triggers' __tablename__ = 'triggers'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
trigger = mapped_column(String(15), unique=True) trigger = mapped_column(String(15), unique=True)
class User_Main_Settings(Base): class User_Main_Settings(Base):
"""
Основные настройки пользователя для торговли.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
trading_mode (str): Режим торговли, FK на trading_modes.mode.
margin_type (str): Тип маржи, FK на margin_types.type.
size_leverage (int): Кредитное плечо.
starting_quantity (int): Начальный объем позиции.
martingale_factor (int): Коэффициент мартингейла.
martingale_step (int): Текущий шаг мартингейла.
maximal_quantity (int): Максимальное число шагов мартингейла.
entry_order_type (str): Тип ордера входа (Market/Limit).
limit_order_price (Optional[str]): Цена лимитного ордера, если есть.
"""
__tablename__ = 'user_main_settings' __tablename__ = 'user_main_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -73,9 +161,24 @@ class User_Main_Settings(Base):
size_leverage = mapped_column(Integer(), default=1) size_leverage = mapped_column(Integer(), default=1)
starting_quantity = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1)
martingale_factor = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1)
martingale_step = mapped_column(Integer(), default=1)
maximal_quantity = mapped_column(Integer(), default=10) maximal_quantity = mapped_column(Integer(), default=10)
entry_order_type = mapped_column(String(10), default='Market')
limit_order_price = mapped_column(String(20), nullable=True)
class User_Risk_Management_Settings(Base): class User_Risk_Management_Settings(Base):
"""
Настройки управления рисками пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
price_profit (int): Процент прибыли для трейда.
price_loss (int): Процент убытка для трейда.
max_risk_deal (int): Максимально допустимый риск по сделке в процентах.
commission_fee (str): Учитывать ли комиссию в расчетах ("Да"/"Нет").
"""
__tablename__ = 'user_risk_management_settings' __tablename__ = 'user_risk_management_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -85,8 +188,24 @@ class User_Risk_Management_Settings(Base):
price_profit = mapped_column(Integer(), default=1) price_profit = mapped_column(Integer(), default=1)
price_loss = mapped_column(Integer(), default=1) price_loss = mapped_column(Integer(), default=1)
max_risk_deal = mapped_column(Integer(), default=100) max_risk_deal = mapped_column(Integer(), default=100)
commission_fee = mapped_column(String(), default="Да")
class User_Condition_Settings(Base): class User_Condition_Settings(Base):
"""
Дополнительные пользовательские условия для торговли.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
trigger (str): Тип триггера, FK на triggers.trigger.
filter_time (str): Временной фильтр.
filter_volatility (bool): Фильтр по волатильности.
external_cues (bool): Внешние сигналы.
tradingview_cues (bool): Сигналы TradingView.
webhook (str): URL webhook.
ai_analytics (bool): Использование AI для аналитики.
"""
__tablename__ = 'user_condition_settings' __tablename__ = 'user_condition_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -101,7 +220,18 @@ class User_Condition_Settings(Base):
webhook = mapped_column(String(40), default='') webhook = mapped_column(String(40), default='')
ai_analytics = mapped_column(Boolean, default=False) ai_analytics = mapped_column(Boolean, default=False)
class User_Additional_Settings(Base): class User_Additional_Settings(Base):
"""
Прочие дополнительные настройки пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
pattern_save (bool): Сохранять ли шаблоны.
autostart (bool): Автоматический запуск.
notifications (bool): Получение уведомлений.
"""
__tablename__ = 'user_additional_settings' __tablename__ = 'user_additional_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -112,7 +242,55 @@ class User_Additional_Settings(Base):
autostart = mapped_column(Boolean, default=False) autostart = mapped_column(Boolean, default=False)
notifications = mapped_column(Boolean, default=False) notifications = mapped_column(Boolean, default=False)
class USER_DEALS(Base):
"""
Таблица сделок пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
symbol (str): Торговая пара.
side (str): Направление сделки (Buy/Sell).
open_price (int): Цена открытия.
positive_percent (int): Процент доходности.
"""
__tablename__ = 'user_deals'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"))
symbol = mapped_column(String(18), default='PENGUUSDT')
side = mapped_column(String(10), nullable=False)
open_price = mapped_column(Integer(), nullable=False)
positive_percent = mapped_column(Integer(), nullable=False)
class UserTimer(Base):
"""
Таймер пользователя для отсроченного запуска сделок.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
timer_minutes (int): Количество минут таймера.
timer_start (datetime): Время начала таймера.
timer_end (Optional[datetime]): Время окончания таймера (если установлено).
"""
__tablename__ = 'user_timers'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"))
timer_minutes = mapped_column(Integer, nullable=False, default=0)
timer_start = mapped_column(DateTime, default=datetime.utcnow)
timer_end = mapped_column(DateTime, nullable=True)
async def async_main(): async def async_main():
"""
Асинхронное создание всех таблиц и заполнение справочников начальными данными.
"""
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)

View File

@@ -1,61 +1,92 @@
import logging import logging.config
logger = logging.getLogger(__name__) from logger_helper.logger_helper import LOGGING_CONFIG
from datetime import datetime, timedelta
from typing import Any
from app.telegram.database.models import async_session from app.telegram.database.models import (
from app.telegram.database.models import User_Telegram_Id as UTi async_session,
from app.telegram.database.models import User_Main_Settings as UMS User_Telegram_Id as UTi,
from app.telegram.database.models import User_Bybit_API as UBA User_Main_Settings as UMS,
from app.telegram.database.models import User_Symbol User_Bybit_API as UBA,
from app.telegram.database.models import User_Risk_Management_Settings as URMS User_Symbol,
from app.telegram.database.models import User_Condition_Settings as UCS User_Risk_Management_Settings as URMS,
from app.telegram.database.models import User_Additional_Settings as UAS User_Condition_Settings as UCS,
from app.telegram.database.models import Trading_Mode User_Additional_Settings as UAS,
from app.telegram.database.models import Margin_type Trading_Mode,
from app.telegram.database.models import Trigger Margin_type,
Trigger,
USER_DEALS,
UserTimer,
)
import app.telegram.functions.functions as func # functions from sqlalchemy import select, update
from sqlalchemy import select, delete, update logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("requests")
# SET_DB
async def save_tg_id_new_user(tg_id): # --- Функции сохранения в БД ---
async def save_tg_id_new_user(tg_id) -> None:
"""
Сохраняет Telegram ID нового пользователя в базу, если такого ещё нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
if not user: if not user:
session.add(UTi(tg_id=tg_id)) session.add(UTi(tg_id=tg_id))
logger.info("Новый пользователь был добавлен в бд") logger.info("Новый пользователь был добавлен в бд %s", tg_id)
await session.commit() await session.commit()
async def set_new_user_bybit_api(tg_id):
async def set_new_user_bybit_api(tg_id) -> None:
"""
Создаёт запись API пользователя Bybit, если её ещё нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id)) user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id))
if not user: if not user:
session.add(UBA( session.add(UBA(tg_id=tg_id))
tg_id=tg_id,
))
logger.info(f"Bybit был успешно подключен")
await session.commit() await session.commit()
async def set_new_user_symbol(tg_id):
async def set_new_user_symbol(tg_id) -> None:
"""
Создаёт запись торгового символа пользователя, если её нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id)) user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id))
if not user: if not user:
session.add(User_Symbol( session.add(User_Symbol(tg_id=tg_id))
tg_id=tg_id
))
logger.info(f"Symbol был успешно добавлен") logger.info(f"Symbol был успешно добавлен %s", tg_id)
await session.commit() await session.commit()
async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None: async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None:
"""
Создаёт основные настройки пользователя по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
trading_mode (str): Режим торговли.
margin_type (str): Тип маржи.
"""
async with async_session() as session: async with async_session() as session:
settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
@@ -66,11 +97,18 @@ async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -
margin_type=margin_type, margin_type=margin_type,
)) ))
logger.info("Основные настройки нового пользователя были заполнены") logger.info("Основные настройки нового пользователя были заполнены%s", tg_id)
await session.commit() await session.commit()
async def set_new_user_default_risk_management_settings(tg_id) -> None: async def set_new_user_default_risk_management_settings(tg_id) -> None:
"""
Создаёт настройки риск-менеджмента по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session: async with async_session() as session:
settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
@@ -79,11 +117,19 @@ async def set_new_user_default_risk_management_settings(tg_id) -> None:
tg_id=tg_id tg_id=tg_id
)) ))
logger.info("Риск-Менеджмент настройки нового пользователя были заполнены") logger.info("Риск-Менеджмент настройки нового пользователя были заполнены %s", tg_id)
await session.commit() await session.commit()
async def set_new_user_default_condition_settings(tg_id, trigger) -> None: async def set_new_user_default_condition_settings(tg_id, trigger) -> None:
"""
Создаёт условные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
trigger (Any): Значение триггера по умолчанию.
"""
async with async_session() as session: async with async_session() as session:
settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id)) settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id))
@@ -93,11 +139,18 @@ async def set_new_user_default_condition_settings(tg_id, trigger) -> None:
trigger=trigger trigger=trigger
)) ))
logger.info("Условные настройки нового пользователя были заполнены") logger.info("Условные настройки нового пользователя были заполнены %s", tg_id)
await session.commit() await session.commit()
async def set_new_user_default_additional_settings(tg_id) -> None: async def set_new_user_default_additional_settings(tg_id) -> None:
"""
Создаёт дополнительные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session: async with async_session() as session:
settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id)) settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id))
@@ -106,52 +159,152 @@ async def set_new_user_default_additional_settings(tg_id) -> None:
tg_id=tg_id, tg_id=tg_id,
)) ))
logger.info("Дополнительные настройки нового пользователя были заполнены") logger.info("Дополнительные настройки нового пользователя были заполнены %s", tg_id)
await session.commit() await session.commit()
# GET_DB
# --- Функции получения данных из БД ---
async def check_user(tg_id): async def check_user(tg_id):
"""
Проверяет наличие пользователя в базе.
Args:
tg_id (int): Telegram ID пользователя.
Returns:
Optional[UTi]: Пользователь или None.
"""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
return user return user
async def get_bybit_api_key(tg_id): async def get_bybit_api_key(tg_id):
"""Получить API ключ Bybit пользователя."""
async with async_session() as session: async with async_session() as session:
api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id)) api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id))
return api_key return api_key
async def get_bybit_secret_key(tg_id): async def get_bybit_secret_key(tg_id):
"""Получить секретный ключ Bybit пользователя."""
async with async_session() as session: async with async_session() as session:
secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id)) secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id))
return secret_key return secret_key
async def get_symbol(tg_id): async def get_symbol(tg_id):
"""Получить символ пользователя."""
async with async_session() as session: async with async_session() as session:
symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id)) symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id))
return symbol return symbol
async def get_user_trades(tg_id):
"""Получить сделки пользователя."""
async with async_session() as session:
query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id)
result = await session.execute(query)
trades = result.all()
return trades
async def get_entry_order_type(tg_id: object) -> str | None | Any:
"""Получить тип входного ордера пользователя."""
async with async_session() as session:
order_type = await session.scalar(
select(UMS.entry_order_type).where(UMS.tg_id == tg_id)
)
# Если в базе не установлен тип — возвращаем значение по умолчанию
return order_type or 'Market'
# --- Функции обновления данных ---
async def update_user_trades(tg_id, **kwargs):
"""Обновить сделки пользователя."""
async with async_session() as session:
query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs)
await session.execute(query)
await session.commit()
async def update_symbol(tg_id: int, symbol: str) -> None:
"""Обновить торговый символ пользователя."""
async with async_session() as session:
await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol))
await session.commit()
async def update_api_key(tg_id: int, api: str) -> None:
"""Обновить API ключ пользователя."""
async with async_session() as session:
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api))
await session.commit()
async def update_secret_key(tg_id: int, api: str) -> None:
"""Обновить секретный ключ пользователя."""
async with async_session() as session:
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api))
await session.commit()
# --- Более мелкие обновления и запросы по настройкам ---
async def update_trade_mode_user(tg_id, trading_mode) -> None:
"""Обновить режим торговли пользователя."""
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode))
if mode:
logger.info("Изменён торговый режим для пользователя %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode))
await session.commit()
async def delete_user_trade(tg_id: int, symbol: str):
"""Удалить сделку пользователя."""
async with async_session() as session:
await session.execute(
USER_DEALS.__table__.delete().where(
(USER_DEALS.tg_id == tg_id) & (USER_DEALS.symbol == symbol)
)
)
await session.commit()
async def get_for_registration_trading_mode(): async def get_for_registration_trading_mode():
"""Получить режим торговли по умолчанию."""
async with async_session() as session: async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1)) mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1))
return mode return mode
async def get_for_registration_margin_type(): async def get_for_registration_margin_type():
"""Получить тип маржи по умолчанию."""
async with async_session() as session: async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1)) type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1))
return type return type
async def get_for_registration_trigger(): async def get_for_registration_trigger():
"""Получить триггер по умолчанию."""
async with async_session() as session: async with async_session() as session:
trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1)) trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1))
return trigger return trigger
async def get_user_main_settings(tg_id): async def get_user_main_settings(tg_id):
"""Получить основные настройки пользователя."""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
if user: if user:
logger.info("Получение основных настроек пользователя") logger.info("Получение основных настроек пользователя %s", tg_id)
trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id)) trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id))
margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id)) margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id))
@@ -159,6 +312,9 @@ async def get_user_main_settings(tg_id):
starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id)) starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id))
martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id)) martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id))
maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id)) maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id))
entry_order_type = await session.scalar(select(UMS.entry_order_type).where(UMS.tg_id == tg_id))
limit_order_price = await session.scalar(select(UMS.limit_order_price).where(UMS.tg_id == tg_id))
martingale_step = await session.scalar(select(UMS.martingale_step).where(UMS.tg_id == tg_id))
data = { data = {
'trading_mode': trading_mode, 'trading_mode': trading_mode,
@@ -166,110 +322,228 @@ async def get_user_main_settings(tg_id):
'size_leverage': size_leverage, 'size_leverage': size_leverage,
'starting_quantity': starting_quantity, 'starting_quantity': starting_quantity,
'martingale_factor': martingale_factor, 'martingale_factor': martingale_factor,
'maximal_quantity': maximal_quantity 'maximal_quantity': maximal_quantity,
'entry_order_type': entry_order_type,
'limit_order_price': limit_order_price,
'martingale_step': martingale_step,
} }
return data return data
async def get_user_risk_management_settings(tg_id): async def get_user_risk_management_settings(tg_id):
"""Получить риск-менеджмента настройки пользователя."""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
if user: if user:
logger.info("Получение риск-менеджмента настроек пользователя") logger.info("Получение риск-менеджмента настроек пользователя %s", tg_id)
price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id)) price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id))
price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id)) price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id))
max_risk_deal = await session.scalar(select(URMS.max_risk_deal).where(URMS.tg_id == tg_id)) max_risk_deal = await session.scalar(select(URMS.max_risk_deal).where(URMS.tg_id == tg_id))
commission_fee = await session.scalar(select(URMS.commission_fee).where(URMS.tg_id == tg_id))
data = { data = {
'price_profit': price_profit, 'price_profit': price_profit,
'price_loss': price_loss, 'price_loss': price_loss,
'max_risk_deal': max_risk_deal 'max_risk_deal': max_risk_deal,
'commission_fee': commission_fee,
} }
return data return data
#UPDATE_SYMBOL
async def update_symbol(tg_id, symbol) -> None:
async with async_session() as session:
await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol = symbol))
await session.commit()
async def update_api_key(tg_id, api):
async with async_session() as session:
api_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key = api))
await session.commit()
async def update_secret_key(tg_id, api):
async with async_session() as session:
secret_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key = api))
await session.commit()
# UPDATE_MAIN_SETTINGS_DB
async def update_trade_mode_user(tg_id, trading_mode) -> None:
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode))
if mode:
logger.info("Изменен трейд мод")
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode = mode))
await session.commit()
async def update_margin_type(tg_id, margin_type) -> None: async def update_margin_type(tg_id, margin_type) -> None:
"""Обновить тип маржи пользователя."""
async with async_session() as session: async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type)) type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type))
if type: if type:
logger.info("Изменен тип маржи") logger.info("Изменен тип маржи %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type = type)) await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type=type))
await session.commit() await session.commit()
async def update_size_leverange(tg_id, num): async def update_size_leverange(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage = num)) await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage=num))
await session.commit() await session.commit()
async def update_starting_quantity(tg_id, num): async def update_starting_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity = num)) await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity=num))
await session.commit() await session.commit()
async def update_martingale_factor(tg_id, num): async def update_martingale_factor(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor = num)) await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor=num))
await session.commit() await session.commit()
async def update_maximal_quantity(tg_id, num): async def update_maximal_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity = num)) await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity=num))
await session.commit() await session.commit()
# UPDATE_RISK_MANAGEMENT_SETTINGS_DB
# ОБНОВЛЕНИЕ НАСТРОЕК РИСК-МЕНЕДЖМЕНТА
async def update_price_profit(tg_id, num): async def update_price_profit(tg_id, num):
"""Обновить цену тейк-профита (прибыль) пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit = num)) await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit=num))
await session.commit() await session.commit()
async def update_price_loss(tg_id, num): async def update_price_loss(tg_id, num):
"""Обновить цену тейк-лосса (убыток) пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss = num)) await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss=num))
await session.commit() await session.commit()
async def update_max_risk_deal(tg_id, num): async def update_max_risk_deal(tg_id, num):
"""Обновить максимальную сумму риска пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal = num)) await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal=num))
await session.commit()
async def update_entry_order_type(tg_id, order_type):
"""Обновить тип входного ордера пользователя."""
async with async_session() as session:
await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(entry_order_type=order_type)
)
await session.commit()
async def get_limit_price(tg_id):
"""Получить лимитную цену пользователя как float, либо None."""
async with async_session() as session:
result = await session.execute(
select(UMS.limit_order_price)
.where(UMS.tg_id == tg_id)
)
price = result.scalar_one_or_none()
if price:
try:
return float(price)
except ValueError:
return None
return None
async def update_limit_price(tg_id, price):
"""Обновить лимитную цену пользователя."""
async with async_session() as session:
await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(limit_order_price=str(price))
)
await session.commit()
async def update_commission_fee(tg_id, num):
"""Обновить комиссию пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num))
await session.commit()
async def get_user_timer(tg_id):
"""Получить данные о таймере пользователя."""
async with async_session() as session:
result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
user_timer = result.scalars().first()
if not user_timer:
logging.info(f"No timer found for user {tg_id}")
return None
timer_minutes = user_timer.timer_minutes
timer_start = user_timer.timer_start
timer_end = user_timer.timer_end
logging.info(f"Timer data for tg_id={tg_id}: "
f"timer_minutes={timer_minutes}, "
f"timer_start={timer_start}, "
f"timer_end={timer_end}")
remaining = None
if timer_end:
remaining = max(0, int((timer_end - datetime.utcnow()).total_seconds() // 60))
return {
"timer_minutes": timer_minutes,
"timer_start": timer_start,
"timer_end": timer_end,
"remaining_minutes": remaining
}
async def update_user_timer(tg_id, minutes: int):
"""Обновить данные о таймере пользователя."""
async with async_session() as session:
try:
timer_start = None
timer_end = None
if minutes > 0:
timer_start = datetime.utcnow()
timer_end = timer_start + timedelta(minutes=minutes)
result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
user_timer = result.scalars().first()
if user_timer:
user_timer.timer_minutes = minutes
user_timer.timer_start = timer_start
user_timer.timer_end = timer_end
else:
user_timer = UserTimer(
tg_id=tg_id,
timer_minutes=minutes,
timer_start=timer_start,
timer_end=timer_end
)
session.add(user_timer)
await session.commit()
except Exception as e:
logging.error(f"Ошибка обновления таймера пользователя {tg_id}: {e}")
async def get_martingale_step(tg_id):
"""Получить шаг мартингейла пользователя."""
async with async_session() as session:
result = await session.execute(select(UMS).where(UMS.tg_id == tg_id))
user_settings = result.scalars().first()
return user_settings.martingale_step
async def update_martingale_step(tg_id, step):
"""Обновить шаг мартингейла пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step))
await session.commit() await session.commit()

View File

@@ -1,6 +1,30 @@
import app.telegram.Keyboards.inline_keyboards as inline_markup import asyncio
import logging.config
import app.telegram.Keyboards.inline_keyboards as inline_markup
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.fsm.state import State, StatesGroup
from app.services.Bybit.functions.Futures import trading_cycle
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("condition_settings")
condition_settings_router = Router()
class condition_settings(StatesGroup):
trigger = State()
timer = State()
volatilty = State()
volume = State()
integration = State()
use_tv_signal = State()
async def reg_new_user_default_condition_settings(id, message): async def reg_new_user_default_condition_settings(id, message):
tg_id = id tg_id = id
@@ -9,11 +33,12 @@ async def reg_new_user_default_condition_settings(id, message):
await rq.set_new_user_default_condition_settings(tg_id, trigger) await rq.set_new_user_default_condition_settings(tg_id, trigger)
async def main_settings_message(id, message, state): async def main_settings_message(id, message, state):
text = """ <b>Условия запуска</b> text = """ <b>Условия запуска</b>
<b>- Триггер:</b> Ручной запуск / Сигнал TradingView / Полностью автоматический <b>- Триггер:</b> Ручной запуск / Сигнал TradingView / Полностью автоматический
<b>- Фильтр времени: </b> диапазон по дням недели и времени суток <b>- Таймер: </b> установить таймер / остановить таймер
<b>- Фильтр волатильности / объёма: </b> включить/отключить <b>- Фильтр волатильности / объёма: </b> включить/отключить
<b>- Интеграции и внешние сигналы: </b> <b>- Интеграции и внешние сигналы: </b>
<b>- Использовать сигналы TradingView:</b> да / нет <b>- Использовать сигналы TradingView:</b> да / нет
@@ -22,6 +47,7 @@ async def main_settings_message(id, message, state):
""" """
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup)
async def trigger_message(message, state): async def trigger_message(message, state):
text = '''Триггер text = '''Триггер
@@ -29,13 +55,45 @@ async def trigger_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup)
async def filter_time_message(message, state):
text = '''Фильтр времени
??? async def timer_message(id, message: Message, state: FSMContext):
''' await state.set_state(condition_settings.timer)
timer_info = await rq.get_user_timer(id)
if timer_info is None:
await message.answer("Таймер не установлен.", reply_markup=inline_markup.timer_markup)
return
await message.answer(
f"Таймер: {timer_info['timer_minutes']} мин\n",
reply_markup=inline_markup.timer_markup
)
@condition_settings_router.callback_query(F.data == "clb_set_timer")
async def set_timer_callback(callback: CallbackQuery, state: FSMContext):
await state.set_state(condition_settings.timer) # состояние для ввода времени
await callback.message.answer("Введите время работы в минутах (например, 60):")
await callback.answer()
@condition_settings_router.message(condition_settings.timer)
async def process_timer_input(message: Message, state: FSMContext):
try:
minutes = int(message.text)
if minutes <= 0:
await message.reply("Введите число больше нуля.")
return
await rq.update_user_timer(message.from_user.id, minutes)
await message.answer(f"Таймер установлен на {minutes} минут.\nНажмите кнопку 'Начать торговлю' для запуска.",
reply_markup=inline_markup.start_trading_markup)
await state.clear()
except ValueError:
await message.reply("Пожалуйста, введите корректное число.")
await message.answer(text=text)
async def filter_volatility_message(message, state): async def filter_volatility_message(message, state):
text = '''Фильтр волатильности text = '''Фильтр волатильности
@@ -44,6 +102,7 @@ async def filter_volatility_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)
async def external_cues_message(message, state): async def external_cues_message(message, state):
text = '''<b>Внешние сигналы</b> text = '''<b>Внешние сигналы</b>
@@ -51,6 +110,7 @@ async def external_cues_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=None)
async def trading_cues_message(message, state): async def trading_cues_message(message, state):
text = '''<b>Использование сигналов</b> text = '''<b>Использование сигналов</b>
@@ -58,16 +118,16 @@ async def trading_cues_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
async def webhook_message(message, state): async def webhook_message(message, state):
text = '''Скиньте ссылку на <b>webhook</b> (если есть trading view): ''' text = '''Скиньте ссылку на <b>webhook</b> (если есть trading view): '''
await message.answer(text=text, parse_mode='html') await message.answer(text=text, parse_mode='html')
async def ai_analytics_message(message, state): async def ai_analytics_message(message, state):
text = '''<b>ИИ - Аналитика</b> text = '''<b>ИИ - Аналитика</b>
Описание... ''' Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)

View File

@@ -10,10 +10,9 @@ async def start_message(message):
username = message.from_user.first_name username = message.from_user.first_name
else: else:
username = f'{message.from_user.first_name} {message.from_user.last_name}' username = f'{message.from_user.first_name} {message.from_user.last_name}'
await message.answer(f""" Привет <b>{username}</b>! 👋 await message.answer(f""" Привет <b>{username}</b>! 👋""", parse_mode='html', reply_markup=reply_markup.base_buttons_markup)
await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.",
Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений. parse_mode='html', reply_markup=inline_markup.start_markup)
""", parse_mode='html', reply_markup=inline_markup.start_markup)
async def profile_message(username, message): async def profile_message(username, message):
await message.answer(f""" <b>@{username}</b> await message.answer(f""" <b>@{username}</b>

View File

@@ -38,7 +38,8 @@ async def main_settings_message(id, message, state):
<b>- Размер кредитного плеча:</b> х{data['size_leverage']} <b>- Размер кредитного плеча:</b> х{data['size_leverage']}
<b>- Начальная ставка:</b> {data['starting_quantity']} <b>- Начальная ставка:</b> {data['starting_quantity']}
<b>- Коэффициент мартингейла:</b> {data['martingale_factor']} <b>- Коэффициент мартингейла:</b> {data['martingale_factor']}
<b>- Максимальное количесиво ставок в серии:</b> {data['maximal_quantity']} <b>- Количество ставок в серии:</b> {data['martingale_step']}
<b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup) """, parse_mode='html', reply_markup=inline_markup.main_settings_markup)
async def trading_mode_message(message, state): async def trading_mode_message(message, state):
@@ -189,7 +190,7 @@ async def state_margin_type(callback: CallbackQuery, state):
async def starting_quantity_message (message, state): async def starting_quantity_message (message, state):
await state.set_state(update_main_settings.starting_quantity) await state.set_state(update_main_settings.starting_quantity)
await message.edit_text("Введите <b>началаьную ставку:</b>", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) await message.edit_text("Введите <b>начальную ставку:</b>", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.starting_quantity) @router_main_settings.message(update_main_settings.starting_quantity)
async def state_starting_quantity(message: Message, state): async def state_starting_quantity(message: Message, state):

View File

@@ -1,46 +1,44 @@
from aiogram import Router from aiogram import Router
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
# FSM - Механизм состояния from app.states.States import update_risk_management_settings
from aiogram.fsm.state import State, StatesGroup
router_risk_management_settings = Router() router_risk_management_settings = Router()
class update_risk_management_settings(StatesGroup):
price_profit = State()
price_loss = State()
max_risk_deal = State()
async def reg_new_user_default_risk_management_settings(id, message): async def reg_new_user_default_risk_management_settings(id, message):
tg_id = id tg_id = id
await rq.set_new_user_default_risk_management_settings(tg_id) await rq.set_new_user_default_risk_management_settings(tg_id)
async def main_settings_message(id, message, state):
async def main_settings_message(id, message):
data = await rq.get_user_risk_management_settings(id) data = await rq.get_user_risk_management_settings(id)
text = f"""<b>Риск менеджмент</b>, text = f"""<b>Риск менеджмент</b>,
<b>- Процент изменения цены для фиксации прибыли:</b> {data['price_profit']}% <b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}%
<b>- Процент изменения цены для фиксации убытков:</b> {data['price_loss']}% <b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}%
<b>- Максимальный риск на сделку (в % от баланса):</b> {data['max_risk_deal']}% <b>- Максимальный риск на сделку (в % от баланса):</b> {data.get('max_risk_deal', 0)}%
""" <b>- Комиссия биржи для расчета прибыли:</b> {data.get('commission_fee', "Да")}
"""
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup)
async def price_profit_message(message, state): async def price_profit_message(message, state):
await state.set_state(update_risk_management_settings.price_profit) await state.set_state(update_risk_management_settings.price_profit)
text = 'Введите число изменения цены для фиксации прибыли: ' text = 'Введите число изменения цены для фиксации прибыли: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=None)
@router_risk_management_settings.message(update_risk_management_settings.price_profit) @router_risk_management_settings.message(update_risk_management_settings.price_profit)
async def state_price_profit(message: Message, state): async def state_price_profit(message: Message, state):
await state.update_data(price_profit = message.text) await state.update_data(price_profit=message.text)
data = await state.get_data() data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id) data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
@@ -49,50 +47,76 @@ async def state_price_profit(message: Message, state):
await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%") await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%")
await rq.update_price_profit(message.from_user.id, data['price_profit']) await rq.update_price_profit(message.from_user.id, data['price_profit'])
await main_settings_message(message.from_user.id, message, state) await main_settings_message(message.from_user.id, message)
await state.clear() await state.clear()
else: else:
await message.answer(f'⛔️ Ошибка: ваше значение ({data['price_profit']}%) или выше лимита (100) или вы вводите неверные символы') await message.answer(
f'⛔️ Ошибка: ваше значение ({data['price_profit']}%) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)
await main_settings_message(message.from_user.id, message, state)
async def price_loss_message(message, state): async def price_loss_message(message, state):
await state.set_state(update_risk_management_settings.price_loss) await state.set_state(update_risk_management_settings.price_loss)
text = 'Введите число изменения цены для фиксации убытков: ' text = 'Введите число изменения цены для фиксации убытков: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=None)
@router_risk_management_settings.message(update_risk_management_settings.price_loss) @router_risk_management_settings.message(update_risk_management_settings.price_loss)
async def state_price_loss(message: Message, state): async def state_price_loss(message: Message, state):
await state.update_data(price_loss = message.text) await state.update_data(price_loss=message.text)
data = await state.get_data() data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id) data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
if data['price_loss'].isdigit() and int(data['price_loss']) <= 100: if data['price_loss'].isdigit() and int(data['price_loss']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['price_loss']}% → {data['price_loss']}%") new_price_loss = int(data['price_loss'])
old_price_loss = int(data_settings.get('price_loss', 0))
await rq.update_price_loss(message.from_user.id, data['price_loss']) current_price_profit = data_settings.get('price_profit')
await main_settings_message(message.from_user.id, message, state) # Пробуем перевести price_profit в число, если это возможно
try:
current_price_profit_num = int(current_price_profit)
except Exception:
current_price_profit_num = 0
# Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом
should_update_profit = (current_price_profit_num == 0) or (current_price_profit_num == abs(old_price_loss))
# Обновляем стоп-лосс
await rq.update_price_loss(message.from_user.id, new_price_loss)
# Если нужно, меняем тейк-профит
if should_update_profit:
new_price_profit = abs(new_price_loss)
await rq.update_price_profit(message.from_user.id, new_price_profit)
await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%\n"
f"Тейк-профит автоматически установлен в: {new_price_profit}%")
else:
await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%")
await main_settings_message(message.from_user.id, message)
await state.clear() await state.clear()
else: else:
await message.answer(f'⛔️ Ошибка: ваше значение ({data['price_loss']}%) или выше лимита (100) или вы вводите неверные символы') await message.answer(
f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы')
await main_settings_message(message.from_user.id, message)
await main_settings_message(message.from_user.id, message, state)
async def max_risk_deal_message(message, state): async def max_risk_deal_message(message, state):
await state.set_state(update_risk_management_settings.max_risk_deal) await state.set_state(update_risk_management_settings.max_risk_deal)
text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=None)
@router_risk_management_settings.message(update_risk_management_settings.max_risk_deal) @router_risk_management_settings.message(update_risk_management_settings.max_risk_deal)
async def state_max_risk_deal(message: Message, state): async def state_max_risk_deal(message: Message, state):
await state.update_data(max_risk_deal = message.text) await state.update_data(max_risk_deal=message.text)
data = await state.get_data() data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id) data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
@@ -101,10 +125,27 @@ async def state_max_risk_deal(message: Message, state):
await message.answer(f"✅ Изменено: {data_settings['max_risk_deal']}% → {data['max_risk_deal']}%") await message.answer(f"✅ Изменено: {data_settings['max_risk_deal']}% → {data['max_risk_deal']}%")
await rq.update_max_risk_deal(message.from_user.id, data['max_risk_deal']) await rq.update_max_risk_deal(message.from_user.id, data['max_risk_deal'])
await main_settings_message(message.from_user.id, message, state) await main_settings_message(message.from_user.id, message)
await state.clear() await state.clear()
else: else:
await message.answer(f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы') await message.answer(
f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message, state) await main_settings_message(message.from_user.id, message)
async def commission_fee_message(message, state):
await state.set_state(update_risk_management_settings.commission_fee)
await message.answer(text="Хотите учитывать комиссию биржи:", parse_mode='html',
reply_markup=inline_markup.buttons_yes_no_markup)
@router_risk_management_settings.callback_query(lambda c: c.data in ["clb_yes", "clb_no"])
async def process_commission_fee_callback(callback: CallbackQuery, state):
val = "Да" if callback.data == "clb_yes" else "Нет"
await rq.update_commission_fee(callback.from_user.id, val)
await callback.message.answer(f"✅ Изменено: {val}")
await callback.answer()
await main_settings_message(callback.from_user.id, callback.message)
await state.clear()

View File

@@ -1,53 +1,90 @@
import logging import logging.config
import asyncio
from aiogram import F, Router from aiogram import F, Router
from aiogram.filters import CommandStart, Command from aiogram.filters import CommandStart
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
import app.telegram.functions.functions as func # functions import app.telegram.functions.functions as func
import app.telegram.functions.main_settings.settings as func_main_settings import app.telegram.functions.main_settings.settings as func_main_settings
import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings
import app.telegram.functions.condition_settings.settings as func_condition_settings import app.telegram.functions.condition_settings.settings as func_condition_settings
import app.telegram.functions.additional_settings.settings as func_additional_settings import app.telegram.functions.additional_settings.settings as func_additional_settings
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup import app.telegram.Keyboards.reply_keyboards as reply_markup
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("handlers")
router = Router() router = Router()
@router.message(CommandStart()) @router.message(CommandStart())
async def start_message(message: Message): async def start_message(message: Message) -> None:
"""
Обработчик команды /start.
Запускает WebSocket для пользователя и инициализирует нового пользователя в БД.
Args:
message (Message): Входящее сообщение с командой /start.
"""
from BybitBot_API import run_ws_for_user
tg_id = message.from_user.id
asyncio.create_task(run_ws_for_user(tg_id, message))
logger.info(f"Получение event loop")
await rq.set_new_user_bybit_api(message.from_user.id) await rq.set_new_user_bybit_api(message.from_user.id)
await func.start_message(message) await func.start_message(message)
@router.message(F.text == "👤 Профиль") @router.message(F.text == "👤 Профиль")
async def profile_message(message: Message): async def profile_message(message: Message) -> None:
"""
Обработчик кнопки 'Профиль'.
Проверяет существование пользователя и отображает профиль.
Args:
message (Message): Сообщение с текстом кнопки.
"""
user = await rq.check_user(message.from_user.id) user = await rq.check_user(message.from_user.id)
if user: if user:
await func.profile_message(message.from_user.username, message) await func.profile_message(message.from_user.username, message)
@router.message(F.text == "Настройки") @router.message(F.text == "Настройки")
async def settings_msg(message: Message): async def settings_msg(message: Message) -> None:
"""
Обработчик кнопки 'Настройки'.
Проверяет пользователя и отображает меню настроек.
Args:
message (Message): Сообщение с текстом кнопки.
"""
user = await rq.check_user(message.from_user.id) user = await rq.check_user(message.from_user.id)
if user: if user:
await func.settings_message(message) await func.settings_message(message)
@router.callback_query(F.data == "clb_start_chatbot_message") @router.callback_query(F.data == "clb_start_chatbot_message")
async def clb_profile_msg (callback: CallbackQuery): async def clb_profile_msg(callback: CallbackQuery) -> None:
"""
Обработчик колбэка 'clb_start_chatbot_message'.
Если пользователь есть в БД — показывает профиль,
иначе регистрирует нового пользователя и инициализирует настройки.
Args:
callback (CallbackQuery): Полученный колбэк.
"""
user = await rq.check_user(callback.from_user.id) user = await rq.check_user(callback.from_user.id)
username = '' first_name = callback.from_user.first_name or ""
last_name = callback.from_user.last_name or ""
if callback.from_user.first_name == None: username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь"
username = callback.from_user.last_name
elif callback.from_user.last_name == None:
username = callback.from_user.first_name
else:
username = f'{callback.from_user.first_name} {callback.from_user.last_name}'
if user: if user:
await func.profile_message(callback.from_user.username, callback.message) await func.profile_message(callback.from_user.username, callback.message)
@@ -55,7 +92,8 @@ async def clb_profile_msg (callback: CallbackQuery):
await rq.save_tg_id_new_user(callback.from_user.id) await rq.save_tg_id_new_user(callback.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message) await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id, callback.message) await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id,
callback.message)
await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id, callback.message) await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id, callback.message)
await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message) await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message)
@@ -65,53 +103,107 @@ async def clb_profile_msg (callback: CallbackQuery):
await callback.answer() await callback.answer()
# Настройки торговли
@router.callback_query(F.data == "clb_settings_message") @router.callback_query(F.data == "clb_settings_message")
async def clb_settings_msg (callback: CallbackQuery): async def clb_settings_msg(callback: CallbackQuery) -> None:
"""
Показать главное меню настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func.settings_message(callback.message) await func.settings_message(callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_back_to_special_settings_message") @router.callback_query(F.data == "clb_back_to_special_settings_message")
async def clb_back_to_settings_msg(callback: CallbackQuery): async def clb_back_to_settings_msg(callback: CallbackQuery) -> None:
"""
Вернуть пользователя к меню специальных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func.settings_message(callback.message) await func.settings_message(callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_main_settings") @router.callback_query(F.data == "clb_change_main_settings")
async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext): async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Открыть меню изменения главных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_risk_management_settings") @router.callback_query(F.data == "clb_change_risk_management_settings")
async def clb_change_risk_management_message(callback: CallbackQuery, state: FSMContext): async def clb_change_risk_management_message(callback: CallbackQuery) -> None:
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message, state) """
Открыть меню изменения настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_condition_settings") @router.callback_query(F.data == "clb_change_condition_settings")
async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext): async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Открыть меню изменения настроек условий.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state) await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_additional_settings") @router.callback_query(F.data == "clb_change_additional_settings")
async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext): async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Открыть меню изменения дополнительных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state) await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state)
await callback.answer() await callback.answer()
# Конкретные настройки каталогов
# Конкретные настройки каталогов
list_main_settings = ['clb_change_trading_mode', list_main_settings = ['clb_change_trading_mode',
'clb_change_margin_type', 'clb_change_margin_type',
'clb_change_size_leverage', 'clb_change_size_leverage',
'clb_change_starting_quantity', 'clb_change_starting_quantity',
'clb_change_martingale_factor', 'clb_change_martingale_factor',
'clb_change_maximum_quantity' 'clb_change_maximum_quantity'
] ]
@router.callback_query(F.data.in_(list_main_settings)) @router.callback_query(F.data.in_(list_main_settings))
async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext): async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик колбэков изменения главных настроек с dispatch через match-case.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer() await callback.answer()
try: try:
@@ -129,15 +221,25 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext):
case 'clb_change_maximum_quantity': case 'clb_change_maximum_quantity':
await func_main_settings.maximum_quantity_message(callback.message, state) await func_main_settings.maximum_quantity_message(callback.message, state)
except Exception as e: except Exception as e:
logging.error(f"Error callback in main_settings match-case: {e}") logger.error(f"Error callback in main_settings match-case: {e}")
list_risk_management_settings = ['clb_change_price_profit', list_risk_management_settings = ['clb_change_price_profit',
'clb_change_price_loss', 'clb_change_price_loss',
'clb_change_max_risk_deal', 'clb_change_max_risk_deal',
] 'commission_fee',
]
@router.callback_query(F.data.in_(list_risk_management_settings)) @router.callback_query(F.data.in_(list_risk_management_settings))
async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext): async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик изменений настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer() await callback.answer()
try: try:
@@ -148,28 +250,39 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo
await func_rmanagement_settings.price_loss_message(callback.message, state) await func_rmanagement_settings.price_loss_message(callback.message, state)
case 'clb_change_max_risk_deal': case 'clb_change_max_risk_deal':
await func_rmanagement_settings.max_risk_deal_message(callback.message, state) await func_rmanagement_settings.max_risk_deal_message(callback.message, state)
case 'commission_fee':
await func_rmanagement_settings.commission_fee_message(callback.message, state)
except Exception as e: except Exception as e:
logging.error(f"Error callback in risk_management match-case: {e}") logger.error(f"Error callback in risk_management match-case: {e}")
list_condition_settings = ['clb_change_trigger', list_condition_settings = ['clb_change_trigger',
'clb_change_filter_time', 'clb_change_timer',
'clb_change_filter_volatility', 'clb_change_filter_volatility',
'clb_change_external_cues', 'clb_change_external_cues',
'clb_change_tradingview_cues', 'clb_change_tradingview_cues',
'clb_change_webhook', 'clb_change_webhook',
'clb_change_ai_analytics' 'clb_change_ai_analytics'
] ]
@router.callback_query(F.data.in_(list_condition_settings)) @router.callback_query(F.data.in_(list_condition_settings))
async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext): async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик изменений настроек условий трейдинга.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer() await callback.answer()
try: try:
match callback.data: match callback.data:
case 'clb_change_trigger': case 'clb_change_trigger':
await func_condition_settings.trigger_message(callback.message, state) await func_condition_settings.trigger_message(callback.message, state)
case 'clb_change_filter_time': case 'clb_change_timer':
await func_condition_settings.filter_time_message(callback.message, state) await func_condition_settings.timer_message(callback.from_user.id, callback.message, state)
case 'clb_change_filter_volatility': case 'clb_change_filter_volatility':
await func_condition_settings.filter_volatility_message(callback.message, state) await func_condition_settings.filter_volatility_message(callback.message, state)
case 'clb_change_external_cues': case 'clb_change_external_cues':
@@ -181,15 +294,24 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext)
case 'clb_change_ai_analytics': case 'clb_change_ai_analytics':
await func_condition_settings.ai_analytics_message(callback.message, state) await func_condition_settings.ai_analytics_message(callback.message, state)
except Exception as e: except Exception as e:
logging.error(f"Error callback in main_settings match-case: {e}") logger.error(f"Error callback in main_settings match-case: {e}")
list_additional_settings = ['clb_change_save_pattern', list_additional_settings = ['clb_change_save_pattern',
'clb_change_auto_start', 'clb_change_auto_start',
'clb_change_notifications', 'clb_change_notifications',
] ]
@router.callback_query(F.data.in_(list_additional_settings)) @router.callback_query(F.data.in_(list_additional_settings))
async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext): async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик дополнительных настроек бота.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer() await callback.answer()
try: try:
@@ -201,4 +323,4 @@ async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext
case 'clb_change_notifications': case 'clb_change_notifications':
await func_additional_settings.notifications_message(callback.message, state) await func_additional_settings.notifications_message(callback.message, state)
except Exception as e: except Exception as e:
logging.error(f"Error callback in additional_settings match-case: {e}") logger.error(f"Error callback in additional_settings match-case: {e}")

View File

@@ -1,8 +0,0 @@
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

View File

@@ -1,6 +1,9 @@
from dotenv import load_dotenv from dotenv import load_dotenv, find_dotenv
import os import os
load_dotenv('.env') env_file = find_dotenv()
load_dotenv(env_file)
TOKEN_TG_BOT = os.getenv('TOKEN_TELEGRAM_BOT') TOKEN_TG_BOT_1 = os.getenv('TOKEN_TELEGRAM_BOT_1')
TOKEN_TG_BOT_2 = os.getenv('TOKEN_TELEGRAM_BOT_2')
TOKEN_TG_BOT_3 = os.getenv('TOKEN_TELEGRAM_BOT_3')

BIN
db.sqlite3 Normal file

Binary file not shown.

View File

@@ -0,0 +1,104 @@
import os
current_directory = os.path.dirname(os.path.abspath(__file__))
log_directory = os.path.join(current_directory, 'loggers')
os.makedirs(log_directory, exist_ok=True)
log_filename = os.path.join(log_directory, 'app.log')
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S", # Формат даты
},
},
"handlers": {
"timed_rotating_file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": log_filename,
"when": "midnight", # Время ротации (каждую полночь)
"interval": 1, # Интервал в днях
"backupCount": 7, # Количество сохраняемых архивов (0 - не сохранять)
"formatter": "default",
"encoding": "utf-8",
},
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
},
},
"loggers": {
"main": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"add_bybit_api": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"balance": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"functions": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"futures": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"get_valid_symbol": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"min_qty": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"price_symbol": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"requests": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"handlers": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"conditions_settings": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"models": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"bybit_ws": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"tasks": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
},
}

View File

@@ -0,0 +1,35 @@
2025-08-23 12:57:26 - main - INFO - Bot is off
2025-08-23 13:04:01 - main - INFO - Bot is off
2025-08-23 13:25:04 - main - INFO - Bot is off
2025-08-23 13:26:24 - main - INFO - Bot is off
2025-08-23 13:28:36 - main - INFO - Bot is off
2025-08-23 13:29:29 - main - INFO - Bot is off
2025-08-23 13:30:48 - main - INFO - Bot is off
2025-08-23 13:31:43 - main - INFO - Bot is off
2025-08-23 13:33:10 - main - INFO - Bot is off
2025-08-23 13:34:59 - main - INFO - Bot is off
2025-08-23 13:36:15 - main - INFO - Bot is off
2025-08-23 13:49:17 - main - INFO - Bot is off
2025-08-23 13:50:22 - main - INFO - Bot is on
2025-08-23 13:51:30 - main - INFO - Bot is off
2025-08-23 13:51:37 - main - INFO - Bot is on
2025-08-23 13:52:12 - main - INFO - Bot is off
2025-08-23 13:57:48 - main - INFO - Bot is on
2025-08-23 14:05:36 - main - INFO - Bot is off
2025-08-23 14:05:43 - main - INFO - Bot is on
2025-08-23 14:06:03 - main - INFO - Bot is off
2025-08-23 14:06:46 - main - INFO - Bot is on
2025-08-23 14:07:04 - requests - INFO - Bybit был успешно подключен
2025-08-23 14:07:43 - requests - INFO - Новый пользователь был добавлен в бд
2025-08-23 14:07:43 - requests - INFO - Основные настройки нового пользователя были заполнены
2025-08-23 14:07:43 - requests - INFO - Риск-Менеджмент настройки нового пользователя были заполнены
2025-08-23 14:07:43 - requests - INFO - Условные настройки нового пользователя были заполнены
2025-08-23 14:07:43 - requests - INFO - Дополнительные настройки нового пользователя были заполнены
2025-08-23 14:23:31 - main - INFO - Bot is off
2025-08-23 14:23:39 - main - INFO - Bot is on
2025-08-23 14:28:13 - main - INFO - Bot is off
2025-08-23 14:28:19 - main - INFO - Bot is on
2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
2025-08-23 14:29:12 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
2025-08-23 14:29:34 - main - INFO - Bot is off

30
requirements.txt Normal file
View File

@@ -0,0 +1,30 @@
aiofiles==24.1.0
aiogram==3.22.0
aiohappyeyeballs==2.6.1
aiohttp==3.12.15
aiosignal==1.4.0
aiosqlite==0.21.0
annotated-types==0.7.0
attrs==25.3.0
certifi==2025.8.3
charset-normalizer==3.4.3
dotenv==0.9.9
frozenlist==1.7.0
greenlet==3.2.4
idna==3.10
magic-filter==1.0.12
multidict==6.6.4
nest-asyncio==1.6.0
propcache==0.3.2
pybit==5.11.0
pycryptodome==3.23.0
pydantic==2.11.7
pydantic_core==2.33.2
python-dotenv==1.1.1
requests==2.32.5
SQLAlchemy==2.0.43
typing-inspection==0.4.1
typing_extensions==4.14.1
urllib3==2.5.0
websocket-client==1.8.0
yarl==1.20.1