Compare commits

...

63 Commits

Author SHA1 Message Date
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 3434 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/
venv/
.venv/
requirements.txt
.idea
/.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
**Функционал:**
+ **Настройки**
+ Основные параметры *(Настроен, работает)*
+ Режим торговли Лонг/Шорт *(настроены)*, Switch/Smart *(не настроены)*
+ Тип маржи: Изолированная / Кросс *(настроено)*
+ Размер кредитного плеча: от x1 до x100 *(настроено)*
+ Начальная ставка: числовое значение *(настроено)*
+ Коэффициент мартингейла: число *(настроено)*
+ Максимальное количество ставок в серии: число *(настроено)*
+ Риск-менеджмент (Настроен, работает)
+ Процент изменения цены для фиксации прибыли (TP%): число *(настроено)*
+ Процент изменения цены для фиксации убытков (SL%): число (пример: 1%) *(настроено)*
+ Максимальный риск на сделку (в % от баланса): число (опционально) *(настроено)*
+ Условия запуска *(Не настроен)*
+ Дополнительные параметры *(Не настроен)*
+ Подключение Bybit *(настроено)*
+ Информация о правильном получении и сохранении Bybit-API keys *(настроено)*
Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
##Основные возможности
- Поддержка работы с биржей Bybit через официальный API.
- Открытие и закрытие позиций по выбранным торговым парам.
- Поддержка рыночных и лимитных ордеров.
- Установка уровней тейк-профита (TP) и стоп-лосса (SL).
- Управление кредитным плечом (leverage).
- Реализация стратегии мартингейла с настройками шага, коэффициента и лимитов.
- Контроль максимального риска на сделку по балансу пользователя.
- Обработка ошибок 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
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
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
from aiogram.types import Message, CallbackQuery
# FSM - Механизм состояния
from aiogram.fsm.state import State, StatesGroup
from app.states.States import state_reg_bybit_api
from aiogram.fsm.context import FSMContext
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("add_bybit_api")
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')
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>
<b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b>
@@ -38,17 +43,27 @@ async def info_for_bybit_api_message(callback: CallbackQuery):
await callback.answer()
@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)
text = 'Отправьте KEY_API ниже: '
await callback.message.answer(text=text)
@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):
await state.update_data(api_key = message.text)
async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext) -> None:
"""
Сохраняет API Key во временное состояние FSM,
затем запрашивает у пользователя ввод Secret Key.
"""
await state.update_data(api_key=message.text)
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)
@router_register_bybit_api.message(state_reg_bybit_api.secret_key)
async def add_secret_key(message: Message, state: FSMContext):
await state.update_data(secret_key = message.text)
async def add_secret_key(message: Message, state: FSMContext) -> None:
"""
Сохраняет Secret Key и финализирует регистрацию,
обновляет базу данных, устанавливает символ пользователя и очищает состояние.
"""
await state.update_data(secret_key=message.text)
data = await state.get_data()
@@ -68,6 +88,5 @@ async def add_secret_key(message: Message, state: FSMContext):
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
from asyncio import Handle
from annotated_types import T
import logging.config
from pybit import exceptions
from pybit.unified_trading import HTTP
from pybit.unified_trading import WebSocket
from app.services.Bybit.functions import price_symbol
from logger_helper.logger_helper import LOGGING_CONFIG
import app.services.Bybit.functions.price_symbol as price_symbol
import app.services.Bybit.functions.balance as balance_g
import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
import logging
logging.basicConfig(level=logging.DEBUG)
logging.config.dictConfig(LOGGING_CONFIG)
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'
def safe_float(val) -> float:
"""
Безопасное преобразование значения в float.
Возвращает 0.0, если значение None, пустое или некорректное.
"""
try:
if val is None or val == '':
return 0.0
return float(val)
except (ValueError, TypeError):
logger.error("Некорректное значение для преобразования в float")
return 0.0
text = f'''Позиция была успешна открыта!
Торговая пара: {symbol}
Движение: {trade_mode}
Тип-маржи: {margin_mode}
Кредитное плечо: {leverage}
Количество: {qty}
'''
await message.answer(text=text, parse_mode='html')
def format_trade_details_position(data, commission_fee):
"""
Форматирует информацию о сделке в виде строки.
"""
msg = data.get('data', [{}])[0]
async def error_max_step(message):
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок')
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))
async def error_max_risk(message):
await message.answer('Сделка не была совершена, слишком высокий риск')
if commission_fee == "Да":
if pnl >= 0:
pnl -= commission
else:
pnl -= commission
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)
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}"
)
def parse_pnl_from_msg(msg) -> float:
"""
Извлекает реализованную прибыль/убыток из сообщения.
"""
try:
return float(msg.get('realisedPnl', 0))
except Exception as e:
logger.error(f"Ошибка при извлечении реализованной прибыли: {e}")
return 0.0
async def handle_execution_message(message, msg: dict) -> None:
"""
Обработчик сообщений об исполнении сделки.
Логирует событие и проверяет условия для мартингейла и 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_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:
case 'Isolated':
margin_mode = 'ISOLATED_MARGIN'
case 'Cross':
margin_mode = 'REGULAR_MARGIN'
trade_info = format_trade_details_position(msg, commission_fee=commission_fee)
await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
liquidation_threshold = -100
try:
balance = 0
price = 0
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)
balance = await balance_g.get_balance(tg_id)
price = await price_symbol.get_price(tg_id, message)
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)
client.set_margin_mode(
setMarginMode=margin_mode # margin_type
)
elif position:
entry_price = safe_float(position.get('avgPrice'))
side = position.get('side', '')
current_price = float(position.get('markPrice', 0))
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'])
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)
# Инициализация переменных
next_quantity = starting_quantity
last_quantity = starting_quantity
realised_pnl = 0.0
current_martingale_step = 0 # Текущая ставка в серии
async def error_max_step(message) -> None:
"""
Сообщение об ошибке превышения максимального количества шагов мартингейла.
"""
logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.')
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.',
reply_markup=inline_markup.back_to_main)
next_quantity = 0
realised_pnl = 0
last_quantity = starting_quantity
async def error_max_risk(message) -> None:
"""
Сообщение об ошибке превышения риск-лимита сделки.
"""
logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.')
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.',
reply_markup=inline_markup.back_to_main)
# Пример расчёта следующего размера позиции
try:
position_info = client.get_positions(category='linear', symbol=SYMBOL)
position = position_info['result']['list'][0] # или другой нужный индекс
realised_pnl = float(position['unrealisedPnl'])
async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full'):
"""
Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска.
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:
await message.answer('Непредвиденная оишбка')
async def contract_short(tg_id, message, margin_mode):
Возвращает 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)
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)
order_type = data_main_stgs.get('entry_order_type')
match margin_mode:
case 'Isolated':
margin_mode = 'ISOLATED_MARGIN'
case 'Cross':
margin_mode = 'REGULAR_MARGIN'
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)
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
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:
balance = 0
price = 0
balance = await balance_g.get_balance(tg_id, message)
price = await price_symbol.get_price(tg_id)
balance = await balance_g.get_balance(tg_id)
price = await price_symbol.get_price(tg_id, message)
client.set_margin_mode(setMarginMode=bybit_margin_mode)
client.set_margin_mode(
setMarginMode=margin_mode # margin_type
)
martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
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_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'])
max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal'))
loss_profit = safe_float(data_risk_stgs.get('price_loss'))
take_profit = safe_float(data_risk_stgs.get('price_profit'))
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions_list = positions_resp.get('result', {}).get('list', [])
# Инициализация переменных
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
last_quantity = starting_quantity
realised_pnl = 0.0
current_martingale_step = 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'])
current_martingale = await rq.get_martingale_step(tg_id)
current_martingale_step = int(current_martingale)
if positions_list:
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("Торговля не продолжится")
if current_martingale_step > max_martingale_steps:
await error_max_step(message)
return
next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step)
else:
next_quantity = starting_quantity
current_martingale_step = 0
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:
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}")
return
r = client.place_order(
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:
min_order_value = 5.0
order_value = float(next_quantity) * float(price)
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,
side='Sell',
orderType="Market",
leverage=int(data_main_stgs['size_leverage']),
qty=next_quantity,
orderLinkId=f"deal_{SYMBOL}_{time.time()}"
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())}"
)
await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity)
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:
await message.answer('Недостаточно баланса')
logger.error(f"InvalidRequestError: {e}")
await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.',
reply_markup=inline_markup.back_to_main)
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.Keyboards.inline_keyboards as inline_markup
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
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)
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':
await message.answer('⚠️ Подключите платформу для торговли')
await message.answer('⚠️ Подключите платформу для торговли',
reply_markup=inline_markup.connect_bybit_api_markup)
return 0
try:
check_user = client.get_wallet_balance()
if check_user:
try:
balance = client.get_wallet_balance(accountType='UNIFIED', coin='USDT')['result']['list'][0]['coin'][0]['walletBalance']
return balance
except Exception as e:
await message.answer('⚠️ Ошибка при получении баланса пользователя')
response = client.get_wallet_balance(accountType='UNIFIED')
if response['retCode'] == 0:
total_balance = response['result']['list'][0].get('totalWalletBalance', '0')
return total_balance
else:
logger.error(f"Ошибка API: {response.get('retMsg')}")
await message.answer(f"⚠️ Ошибка API: {response.get('retMsg')}")
return 0
except Exception as e:
await message.answer('⚠️ Неверные данные API, перепроверьте их')
logger.error(f"Ошибка при получении общего баланса: {e}")
await message.answer('⚠️ Ошибка при получении баланса')
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,453 @@
from aiogram import F, Router
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
import app.telegram.Keyboards.inline_keyboards as inline_markup
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
# FSM - Механизм состояния
from aiogram.fsm.state import State, StatesGroup
from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
SetTP_SL_State, CloseTradeTimerState)
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()
class state_update_symbol(StatesGroup):
symbol = State()
@router_functions_bybit_trade.callback_query(F.data == 'clb_start_trading')
async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMContext):
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)
@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) -> None:
"""
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message)
price = await get_price(user_id)
if balance:
symbol = await rq.get_symbol(callback.from_user.id)
symbol = await rq.get_symbol(user_id)
text = f'''💎 Торговля на Bybit
⚖️ Ваш баланс (USDT): {balance}
📊 Текущая торговая пара: {symbol}
Как начать торговлю?
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT).
'''
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
)
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)
secret = await rq.get_bybit_secret_key(message.from_user.id)
async def start_bybit_trade_message(message: Message) -> None:
"""
Отправляет пользователю информацию о балансе, символе и текущей цене,
вместе с инструкциями по началу торговли.
"""
balance = await get_balance(message.from_user.id, message)
price = await get_price(message.from_user.id)
if balance:
symbol = await rq.get_symbol(message.from_user.id)
text = f'''💎 Торговля на Bybit
⚖️ Ваш баланс (USDT): {balance}
📊 Текущая торговая пара: {symbol}
Как начать торговлю?
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT).
'''
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {balance}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
)
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')
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 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)
async def update_symbol_for_trade(message: Message, state: FSMContext):
await state.update_data(symbol = message.text)
async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
При успешном обновлении сохранит пару и отправит обновлённую информацию.
"""
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 rq.update_symbol(message.from_user.id, data['symbol'])
await start_bybit_trade_message(message, state)
await rq.update_symbol(message.from_user.id, user_input)
await start_bybit_trade_message(message)
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']
qty = data_main_stgs['starting_quantity']
margin_mode = data_main_stgs['margin_type']
qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message)
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type')
async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запрашивает у пользователя тип входа в позицию (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:
match trade_mode:
case 'Long':
await Futures.contract_long(callback.from_user.id, callback.message, margin_mode)
case 'Short':
await Futures.contract_short(callback.from_user.id, callback.message, margin_mode)
case 'Switch':
await callback.message.edit_text('Режим Switch пока недоступен')
case 'Smart':
await callback.message.edit_text('Режим Smart пока недоступен')
await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
reply_markup=inline_markup.back_to_main)
await callback.answer()
return
await message.answer("Начинаю торговлю с использованием текущих настроек...")
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)
await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.",
reply_markup=inline_markup.cancel_start_markup)
await rq.update_user_timer(tg_id, minutes=0)
else:
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
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_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
from logger_helper.logger_helper import LOGGING_CONFIG
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)
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(
api_key=api_key,
api_secret=secret_key
)
client = HTTP(api_key=api_key, api_secret=secret_key)
price = await price_symbol(tg_id)
json_data = client.get_instruments_info(symbol=SYMBOL, category='linear')
price = await get_price(tg_id)
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 logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit import exceptions
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)
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(
api_key=api_key,
@@ -16,9 +25,9 @@ async def get_price(tg_id, message):
)
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
except exceptions.InvalidRequestError as e:
await message.answer('Неверно указана торговая пара')
logger.error(f"Ошибка при получении цены: {e}")
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()

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

@@ -0,0 +1,98 @@
import asyncio
import logging.config
from typing import Optional
from app.services.Bybit.functions.Futures import close_trade_after_delay, trading_cycle
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("tasks")
active_start_tasks = {}
active_close_tasks = {}
lock_start_tasks = asyncio.Lock()
lock_close_tasks = asyncio.Lock()
def start_trading_cycle(tg_id, message) -> None:
"""
Запускает асинхронную задачу торгового цикла для пользователя с указанным tg_id.
"""
task = asyncio.create_task(trading_cycle(tg_id, message))
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_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):
"""
Запускает торговый цикл. Если уже есть запущенная задача, отменяет её.
"""
async with lock_start_tasks:
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)
logger.info(f"Новая задача торговли запущена для пользователя {tg_id}")
async def handle_stop_trading(tg_id: int):
"""
Останавливает торговую задачу пользователя, если она активна.
"""
async with lock_start_tasks:
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.utils.keyboard import InlineKeyboardBuilder
start_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")]
@@ -28,12 +29,41 @@ connect_bybit_api_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_open_deal')]
[InlineKeyboardButton(text="Выбрать тип входа", callback_data='clb_update_entry_type')],
])
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')]]) # Клавиатура для возврата к списку каталога настроек
start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[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=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),
@@ -43,23 +73,27 @@ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')],
[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=[
[InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'),
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=[
[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_external_cues')],
@@ -69,7 +103,8 @@ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[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=[
@@ -78,7 +113,8 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[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=[
@@ -88,25 +124,63 @@ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch"),
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=[
[InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"),
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
[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")]
])
buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Да', callback_data="clb_yes"), InlineKeyboardButton(text='Нет', callback_data="clb_yes")]
buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Да', callback_data="clb_yes"),
InlineKeyboardButton(text='Нет', callback_data="clb_no")],
])
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")]
])

View File

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

View File

@@ -1,27 +1,55 @@
import logging
logger = logging.getLogger(__name__)
from datetime import datetime
import logging.config
from sqlalchemy.sql.sqltypes import DateTime
from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from logger_helper.logger_helper import LOGGING_CONFIG
from sqlalchemy import select, insert
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("models")
engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3')
async_session = async_sessionmaker(engine)
class Base(AsyncAttrs, DeclarativeBase):
"""Базовый класс для declarative моделей SQLAlchemy с поддержкой async."""
pass
class User_Telegram_Id(Base):
"""
Модель таблицы user_telegram_id.
Хранит идентификаторы Telegram пользователей.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Уникальный идентификатор пользователя Telegram.
"""
__tablename__ = 'user_telegram_id'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(BigInteger)
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'
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')
secret_key = mapped_column(String(36), default='None')
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'
id: Mapped[int] = mapped_column(primary_key=True)
@@ -40,28 +87,69 @@ class User_Symbol(Base):
symbol = mapped_column(String(18), default='PENGUUSDT')
class Trading_Mode(Base):
"""
Справочник доступных режимов торговли.
Атрибуты:
id (int): Первичный ключ.
mode (str): Уникальный режим (например, 'Long', 'Short').
"""
__tablename__ = 'trading_modes'
id: Mapped[int] = mapped_column(primary_key=True)
mode = mapped_column(String(10), unique=True)
class Margin_type(Base):
"""
Справочник типов маржинальной торговли.
Атрибуты:
id (int): Первичный ключ.
type (str): Тип маржи (например, 'Isolated', 'Cross').
"""
__tablename__ = 'margin_types'
id: Mapped[int] = mapped_column(primary_key=True)
type = mapped_column(String(15), unique=True)
class Trigger(Base):
"""
Справочник видов триггеров для сделок.
Атрибуты:
id (int): Первичный ключ.
trigger (str): Название триггера (например, 'Ручной', 'Автоматический').
"""
__tablename__ = 'triggers'
id: Mapped[int] = mapped_column(primary_key=True)
trigger = mapped_column(String(15), unique=True)
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'
id: Mapped[int] = mapped_column(primary_key=True)
@@ -73,9 +161,24 @@ class User_Main_Settings(Base):
size_leverage = mapped_column(Integer(), default=1)
starting_quantity = 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)
entry_order_type = mapped_column(String(10), default='Market')
limit_order_price = mapped_column(String(20), nullable=True)
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'
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_loss = mapped_column(Integer(), default=1)
max_risk_deal = mapped_column(Integer(), default=100)
commission_fee = mapped_column(String(), default="Да")
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'
id: Mapped[int] = mapped_column(primary_key=True)
@@ -101,7 +220,18 @@ class User_Condition_Settings(Base):
webhook = mapped_column(String(40), default='')
ai_analytics = mapped_column(Boolean, default=False)
class User_Additional_Settings(Base):
"""
Прочие дополнительные настройки пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
pattern_save (bool): Сохранять ли шаблоны.
autostart (bool): Автоматический запуск.
notifications (bool): Получение уведомлений.
"""
__tablename__ = 'user_additional_settings'
id: Mapped[int] = mapped_column(primary_key=True)
@@ -112,7 +242,55 @@ class User_Additional_Settings(Base):
autostart = 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 with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

View File

@@ -1,61 +1,92 @@
import logging
logger = logging.getLogger(__name__)
import logging.config
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 User_Telegram_Id as UTi
from app.telegram.database.models import User_Main_Settings as UMS
from app.telegram.database.models import User_Bybit_API as UBA
from app.telegram.database.models import User_Symbol
from app.telegram.database.models import User_Risk_Management_Settings as URMS
from app.telegram.database.models import User_Condition_Settings as UCS
from app.telegram.database.models import User_Additional_Settings as UAS
from app.telegram.database.models import Trading_Mode
from app.telegram.database.models import Margin_type
from app.telegram.database.models import Trigger
from app.telegram.database.models import (
async_session,
User_Telegram_Id as UTi,
User_Main_Settings as UMS,
User_Bybit_API as UBA,
User_Symbol,
User_Risk_Management_Settings as URMS,
User_Condition_Settings as UCS,
User_Additional_Settings as UAS,
Trading_Mode,
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:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
if not user:
session.add(UTi(tg_id=tg_id))
logger.info("Новый пользователь был добавлен в бд")
logger.info("Новый пользователь был добавлен в бд %s", tg_id)
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:
user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id))
if not user:
session.add(UBA(
tg_id=tg_id,
))
logger.info(f"Bybit был успешно подключен")
session.add(UBA(tg_id=tg_id))
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:
user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id))
if not user:
session.add(User_Symbol(
tg_id=tg_id
))
session.add(User_Symbol(tg_id=tg_id))
logger.info(f"Symbol был успешно добавлен")
logger.info(f"Symbol был успешно добавлен %s", tg_id)
await session.commit()
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:
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,
))
logger.info("Основные настройки нового пользователя были заполнены")
logger.info("Основные настройки нового пользователя были заполнены%s", tg_id)
await session.commit()
async def set_new_user_default_risk_management_settings(tg_id) -> None:
"""
Создаёт настройки риск-менеджмента по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
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
))
logger.info("Риск-Менеджмент настройки нового пользователя были заполнены")
logger.info("Риск-Менеджмент настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
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:
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
))
logger.info("Условные настройки нового пользователя были заполнены")
logger.info("Условные настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
async def set_new_user_default_additional_settings(tg_id) -> None:
"""
Создаёт дополнительные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
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,
))
logger.info("Дополнительные настройки нового пользователя были заполнены")
logger.info("Дополнительные настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
# GET_DB
# --- Функции получения данных из БД ---
async def check_user(tg_id):
"""
Проверяет наличие пользователя в базе.
Args:
tg_id (int): Telegram ID пользователя.
Returns:
Optional[UTi]: Пользователь или None.
"""
async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
return user
async def get_bybit_api_key(tg_id):
"""Получить API ключ Bybit пользователя."""
async with async_session() as session:
api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id))
return api_key
async def get_bybit_secret_key(tg_id):
"""Получить секретный ключ Bybit пользователя."""
async with async_session() as session:
secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id))
return secret_key
async def get_symbol(tg_id):
"""Получить символ пользователя."""
async with async_session() as session:
symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id))
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 with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1))
return mode
async def get_for_registration_margin_type():
"""Получить тип маржи по умолчанию."""
async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1))
return type
async def get_for_registration_trigger():
"""Получить триггер по умолчанию."""
async with async_session() as session:
trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1))
return trigger
async def get_user_main_settings(tg_id):
"""Получить основные настройки пользователя."""
async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
if user:
logger.info("Получение основных настроек пользователя")
logger.info("Получение основных настроек пользователя %s", 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))
@@ -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))
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))
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 = {
'trading_mode': trading_mode,
@@ -166,110 +322,228 @@ async def get_user_main_settings(tg_id):
'size_leverage': size_leverage,
'starting_quantity': starting_quantity,
'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
async def get_user_risk_management_settings(tg_id):
"""Получить риск-менеджмента настройки пользователя."""
async with async_session() as session:
user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
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_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))
commission_fee = await session.scalar(select(URMS.commission_fee).where(URMS.tg_id == tg_id))
data = {
'price_profit': price_profit,
'price_loss': price_loss,
'max_risk_deal': max_risk_deal
'max_risk_deal': max_risk_deal,
'commission_fee': commission_fee,
}
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 with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type))
if type:
logger.info("Изменен тип маржи")
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type = type))
logger.info("Изменен тип маржи %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type=type))
await session.commit()
async def update_size_leverange(tg_id, num):
"""Обновить размер левеража пользователя."""
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()
async def update_starting_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
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()
async def update_martingale_factor(tg_id, num):
"""Обновить размер левеража пользователя."""
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()
async def update_maximal_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
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()
# UPDATE_RISK_MANAGEMENT_SETTINGS_DB
# ОБНОВЛЕНИЕ НАСТРОЕК РИСК-МЕНЕДЖМЕНТА
async def update_price_profit(tg_id, num):
"""Обновить цену тейк-профита (прибыль) пользователя."""
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()
async def update_price_loss(tg_id, num):
"""Обновить цену тейк-лосса (убыток) пользователя."""
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()
async def update_max_risk_deal(tg_id, num):
"""Обновить максимальную сумму риска пользователя."""
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()

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
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):
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)
async def main_settings_message(id, message, state):
text = """ <b>Условия запуска</b>
<b>- Триггер:</b> Ручной запуск / Сигнал TradingView / Полностью автоматический
<b>- Фильтр времени: </b> диапазон по дням недели и времени суток
<b>- Таймер: </b> установить таймер / остановить таймер
<b>- Фильтр волатильности / объёма: </b> включить/отключить
<b>- Интеграции и внешние сигналы: </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)
async def trigger_message(message, state):
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)
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):
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)
async def external_cues_message(message, state):
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)
async def trading_cues_message(message, state):
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)
async def webhook_message(message, state):
text = '''Скиньте ссылку на <b>webhook</b> (если есть trading view): '''
await message.answer(text=text, parse_mode='html')
async def ai_analytics_message(message, state):
text = '''<b>ИИ - Аналитика</b>
Описание... '''
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
else:
username = f'{message.from_user.first_name} {message.from_user.last_name}'
await message.answer(f""" Привет <b>{username}</b>! 👋
Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.
""", parse_mode='html', reply_markup=inline_markup.start_markup)
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)
async def profile_message(username, message):
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['starting_quantity']}
<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)
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):
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)
async def state_starting_quantity(message: Message, state):

View File

@@ -1,36 +1,33 @@
from aiogram import Router
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
from aiogram.types import Message, CallbackQuery
# FSM - Механизм состояния
from aiogram.fsm.state import State, StatesGroup
from app.states.States import update_risk_management_settings
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):
tg_id = 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)
text = f"""<b>Риск менеджмент</b>,
<b>- Процент изменения цены для фиксации прибыли:</b> {data['price_profit']}%
<b>- Процент изменения цены для фиксации убытков:</b> {data['price_loss']}%
<b>- Максимальный риск на сделку (в % от баланса):</b> {data['max_risk_deal']}%
"""
<b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}%
<b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}%
<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)
async def price_profit_message(message, state):
await state.set_state(update_risk_management_settings.price_profit)
@@ -38,9 +35,10 @@ async def price_profit_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=None)
@router_risk_management_settings.message(update_risk_management_settings.price_profit)
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_settings = await rq.get_user_risk_management_settings(message.from_user.id)
@@ -49,13 +47,15 @@ async def state_price_profit(message: Message, state):
await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {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()
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):
await state.set_state(update_risk_management_settings.price_loss)
@@ -64,24 +64,47 @@ async def price_loss_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=None)
@router_risk_management_settings.message(update_risk_management_settings.price_loss)
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_settings = await rq.get_user_risk_management_settings(message.from_user.id)
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'])
await main_settings_message(message.from_user.id, message, state)
current_price_profit = data_settings.get('price_profit')
# Пробуем перевести 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()
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):
await state.set_state(update_risk_management_settings.max_risk_deal)
@@ -90,9 +113,10 @@ async def max_risk_deal_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=None)
@router_risk_management_settings.message(update_risk_management_settings.max_risk_deal)
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_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 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()
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.filters import CommandStart, Command
from aiogram.filters import CommandStart
from aiogram.types import Message, CallbackQuery
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.risk_management_settings.settings as func_rmanagement_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.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_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.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 func.start_message(message)
@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)
if user:
await func.profile_message(message.from_user.username, message)
@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)
if user:
await func.settings_message(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)
username = ''
if callback.from_user.first_name == None:
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}'
first_name = callback.from_user.first_name or ""
last_name = callback.from_user.last_name or ""
username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь"
if user:
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 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_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()
# Настройки торговли
@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 callback.answer()
@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 callback.answer()
@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 callback.answer()
@router.callback_query(F.data == "clb_change_risk_management_settings")
async def clb_change_risk_management_message(callback: CallbackQuery, state: FSMContext):
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message, state)
async def clb_change_risk_management_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@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 callback.answer()
@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 callback.answer()
# Конкретные настройки каталогов
# Конкретные настройки каталогов
list_main_settings = ['clb_change_trading_mode',
'clb_change_margin_type',
'clb_change_size_leverage',
'clb_change_starting_quantity',
'clb_change_martingale_factor',
'clb_change_maximum_quantity'
]
]
@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()
try:
@@ -129,15 +221,25 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext):
case 'clb_change_maximum_quantity':
await func_main_settings.maximum_quantity_message(callback.message, state)
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',
'clb_change_price_loss',
'clb_change_max_risk_deal',
]
'commission_fee',
]
@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()
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)
case 'clb_change_max_risk_deal':
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:
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',
'clb_change_filter_time',
'clb_change_timer',
'clb_change_filter_volatility',
'clb_change_external_cues',
'clb_change_tradingview_cues',
'clb_change_webhook',
'clb_change_ai_analytics'
]
]
@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()
try:
match callback.data:
case 'clb_change_trigger':
await func_condition_settings.trigger_message(callback.message, state)
case 'clb_change_filter_time':
await func_condition_settings.filter_time_message(callback.message, state)
case 'clb_change_timer':
await func_condition_settings.timer_message(callback.from_user.id, callback.message, state)
case 'clb_change_filter_volatility':
await func_condition_settings.filter_volatility_message(callback.message, state)
case 'clb_change_external_cues':
@@ -181,15 +294,24 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext)
case 'clb_change_ai_analytics':
await func_condition_settings.ai_analytics_message(callback.message, state)
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',
'clb_change_auto_start',
'clb_change_notifications',
]
]
@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()
try:
@@ -201,4 +323,4 @@ async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext
case 'clb_change_notifications':
await func_additional_settings.notifications_message(callback.message, state)
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
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