Compare commits

...

13 Commits

Author SHA1 Message Date
82d875136b deleted deprecated 2025-09-12 09:33:16 +07:00
28ead61112 Обновить README.md 2025-09-12 09:32:27 +07:00
688fc7a8ab Merge pull request 'develop' (#4) from Alex/stcs:develop into stable
Reviewed-on: #4
2025-09-12 09:28:08 +07:00
07666fa984 Merge branch 'stable' into develop 2025-09-12 09:27:52 +07:00
algizn97
b3119c6ee1 Fixed martingale step 2025-09-11 12:50:43 +05:00
algizn97
da16a267e4 Fixed 2025-09-11 12:15:19 +05:00
algizn97
babbcbd1fc The timer start function has been updated 2025-09-11 12:02:47 +05:00
algizn97
f42940f847 Added timer deletion button 2025-09-11 10:57:21 +05:00
algizn97
3ff146a1b9 Fixed 2025-09-10 20:30:51 +05:00
algizn97
93865a1b16 Fixed 2025-09-10 17:34:41 +05:00
algizn97
44f9b05001 Fixed 2025-09-10 15:28:08 +05:00
algizn97
02279d19ae Fixed 2025-09-09 11:40:51 +05:00
algizn97
cf581dc485 Fixed 2025-09-09 10:24:01 +05:00
25 changed files with 806 additions and 1168 deletions

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ venv/
.venv/ .venv/
.idea .idea
/.idea /.idea
/myenv
myenv

5
.idea/.gitignore generated vendored
View File

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

610
.idea/dbnavigator.xml generated
View File

@@ -1,610 +0,0 @@
<?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

@@ -1,44 +0,0 @@
<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

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

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?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
View File

@@ -1,8 +0,0 @@
<?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
View File

@@ -1,8 +0,0 @@
<?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
View File

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

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
import logging.config import logging.config
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
from app.services.Bybit.functions.bybit_ws import get_or_create_event_loop, set_event_loop from app.services.Bybit.functions.bybit_ws import get_or_create_event_loop, set_event_loop
from app.telegram.database.models import async_main from app.telegram.database.models import async_main
from app.telegram.handlers.handlers import router from app.telegram.handlers.handlers import router
@@ -17,8 +17,9 @@ from config import TOKEN_TG_BOT_1
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("main") logger = logging.getLogger("main")
storage = RedisStorage.from_url("redis://localhost:6379/0")
bot = Bot(token=TOKEN_TG_BOT_1) bot = Bot(token=TOKEN_TG_BOT_1)
dp = Dispatcher() dp = Dispatcher(storage=storage)
async def main() -> None: async def main() -> None:
@@ -37,7 +38,10 @@ async def main() -> None:
dp.include_router(router_register_bybit_api) dp.include_router(router_register_bybit_api)
dp.include_router(router_functions_bybit_trade) dp.include_router(router_functions_bybit_trade)
try:
await dp.start_polling(bot) await dp.start_polling(bot)
except asyncio.CancelledError:
logger.info("Bot is off")
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -42,10 +42,16 @@ git clone https://git.svoboda.works/kodorvan/stcs
pip install -r requirements.txt pip install -r requirements.txt
``` ```
3. Создайте файл .env и настройте переменные окружения. 3. Зарегистрируйте чат-робота и сгенерируйте ключ авторизации<br>
[@BotFather](https://t.me/BotFather)
4. Создайте файл .env и настройте переменные окружения
```bash
cp .env.sample .env
nvim .env
```
4. Запустите бота: 5. Запустите бота:
```bash ```bash
python BybitBot_API.py python BybitBot_API.py

View File

@@ -1,9 +1,16 @@
from aiogram import F, Router from aiogram import F, Router
import logging.config import logging.config
from app.services.Bybit.functions.functions import start_bybit_trade_message
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup import app.telegram.Keyboards.reply_keyboards as reply_markup
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.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
@@ -81,12 +88,24 @@ async def add_secret_key(message: Message, state: FSMContext) -> None:
await state.update_data(secret_key=message.text) await state.update_data(secret_key=message.text)
data = await state.get_data() data = await state.get_data()
user = await rq.check_user(message.from_user.id)
await rq.update_api_key(message.from_user.id, data['api_key']) await rq.upsert_api_keys(message.from_user.id, data['api_key'], data['secret_key'])
await rq.update_secret_key(message.from_user.id, data['secret_key'])
await rq.set_new_user_symbol(message.from_user.id) await rq.set_new_user_symbol(message.from_user.id)
await state.clear() await state.clear()
await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!', await message.answer('Данные добавлены.',
reply_markup=reply_markup.base_buttons_markup) reply_markup=reply_markup.base_buttons_markup)
if user:
await start_bybit_trade_message(message)
else:
await rq.save_tg_id_new_user(message.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id,
message)
await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
await start_bybit_trade_message(message)

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,7 @@ async def get_balance(tg_id: int, message) -> float:
api_secret=secret_key api_secret=secret_key
) )
if api_key == 'None' or secret_key == 'None': if api_key is None or secret_key is None:
await message.answer('⚠️ Подключите платформу для торговли', await message.answer('⚠️ Подключите платформу для торговли',
reply_markup=inline_markup.connect_bybit_api_message) reply_markup=inline_markup.connect_bybit_api_message)
return 0 return 0
@@ -48,5 +48,5 @@ async def get_balance(tg_id: int, message) -> float:
return 0 return 0
except Exception as e: except Exception as e:
logger.error(f"Ошибка при получении общего баланса: {e}") logger.error(f"Ошибка при получении общего баланса: {e}")
await message.answer('⚠️ Ошибка при получении баланса') await message.answer('Ошибка при подключении, повторите попытку', reply_markup=inline_markup.connect_bybit_api_message)
return 0 return 0

View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
import logging.config import logging.config
from pybit.unified_trading import WebSocket from pybit.unified_trading import WebSocket
from websocket import WebSocketConnectionClosedException from websocket import WebSocketConnectionClosedException
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
@@ -11,6 +12,30 @@ logger = logging.getLogger("bybit_ws")
event_loop = None # Сюда нужно будет установить event loop из основного приложения event_loop = None # Сюда нужно будет установить event loop из основного приложения
active_ws_tasks = {} active_ws_tasks = {}
def on_ws_error(ws, error):
logger.error(f"WebSocket internal error: {error}")
# Запланировать переподключение через event loop
if event_loop:
asyncio.run_coroutine_threadsafe(reconnect_ws(ws), event_loop)
def on_ws_close(ws, close_status_code, close_msg):
logger.warning(f"WebSocket closed: {close_status_code} - {close_msg}")
# Запланировать переподключение через event loop
if event_loop:
asyncio.run_coroutine_threadsafe(reconnect_ws(ws), event_loop)
async def reconnect_ws(ws):
logger.info("Запускаем переподключение WebSocket...")
await asyncio.sleep(5)
try:
await ws.run_forever()
except WebSocketConnectionClosedException:
logger.info("WebSocket переподключение успешно завершено.")
def get_or_create_event_loop() -> asyncio.AbstractEventLoop: def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
""" """
Возвращает текущий активный цикл событий asyncio или создает новый, если его нет. Возвращает текущий активный цикл событий asyncio или создает новый, если его нет.
@@ -46,6 +71,7 @@ def on_order_callback(message, msg):
if event_loop is not None: if event_loop is not None:
from app.services.Bybit.functions.Futures import handle_order_message from app.services.Bybit.functions.Futures import handle_order_message
asyncio.run_coroutine_threadsafe(handle_order_message(message, msg), event_loop) asyncio.run_coroutine_threadsafe(handle_order_message(message, msg), event_loop)
logger.info("Callback выполнен.")
else: else:
logger.error("Event loop не установлен, callback пропущен.") logger.error("Event loop не установлен, callback пропущен.")
@@ -54,6 +80,7 @@ def on_execution_callback(message, ws_msg):
if event_loop is not None: if event_loop is not None:
from app.services.Bybit.functions.Futures import handle_execution_message from app.services.Bybit.functions.Futures import handle_execution_message
asyncio.run_coroutine_threadsafe(handle_execution_message(message, ws_msg), event_loop) asyncio.run_coroutine_threadsafe(handle_execution_message(message, ws_msg), event_loop)
logger.info("Callback выполнен.")
else: else:
logger.error("Event loop не установлен, callback пропущен.") logger.error("Event loop не установлен, callback пропущен.")
@@ -72,8 +99,12 @@ async def start_execution_ws(api_key: str, api_secret: str, message):
continue continue
ws = WebSocket(api_key=api_key, api_secret=api_secret, testnet=False, channel_type="private") ws = WebSocket(api_key=api_key, api_secret=api_secret, testnet=False, channel_type="private")
ws.on_error = on_ws_error
ws.on_close = on_ws_close
ws.subscribe("order", lambda ws_msg: on_order_callback(message, ws_msg)) ws.subscribe("order", lambda ws_msg: on_order_callback(message, ws_msg))
ws.subscribe("execution", lambda ws_msg: on_execution_callback(message, ws_msg)) ws.subscribe("execution", lambda ws_msg: on_execution_callback(message, ws_msg))
while True: while True:
await asyncio.sleep(1) # Поддержание активности await asyncio.sleep(1) # Поддержание активности
except WebSocketConnectionClosedException: except WebSocketConnectionClosedException:

View File

@@ -2,13 +2,14 @@
import logging.config import logging.config
from aiogram import F, Router from aiogram import F, Router
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
from app.telegram.functions.main_settings.settings import main_settings_message from app.telegram.functions.main_settings.settings import main_settings_message
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \ from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \
get_active_positions_by_symbol, get_active_orders_by_symbol, get_active_positions_by_symbol, get_active_orders_by_symbol,
get_active_positions, get_active_orders, cancel_all_tp_sl_orders, get_active_positions, get_active_orders, cancel_all_tp_sl_orders,
trading_cycle, open_position, close_trade_after_delay, safe_float, open_position, close_trade_after_delay, safe_float,
) )
from app.services.Bybit.functions.balance import get_balance from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
@@ -28,6 +29,8 @@ logger = logging.getLogger("functions")
router_functions_bybit_trade = Router() router_functions_bybit_trade = Router()
user_trade_tasks = {}
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main'])) @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: async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
@@ -61,8 +64,10 @@ async def start_bybit_trade_message(message: Message) -> None:
вместе с инструкциями по началу торговли. вместе с инструкциями по началу торговли.
""" """
balance = await get_balance(message.from_user.id, message) balance = await get_balance(message.from_user.id, message)
tg_id = message.from_user.id
if balance: if balance:
await run_ws_for_user(tg_id, message)
symbol = await rq.get_symbol(message.from_user.id) symbol = await rq.get_symbol(message.from_user.id)
price = await get_price(message.from_user.id, symbol=symbol) price = await get_price(message.from_user.id, symbol=symbol)
@@ -86,6 +91,7 @@ async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMCon
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары. Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
""" """
await state.set_state(state_update_symbol.symbol) await state.set_state(state_update_symbol.symbol)
await callback.answer()
await callback.message.answer( await callback.message.answer(
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ', text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
@@ -99,7 +105,6 @@ async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
При успешном обновлении сохранит пару и отправит обновлённую информацию. При успешном обновлении сохранит пару и отправит обновлённую информацию.
""" """
user_input = message.text.strip().upper() user_input = message.text.strip().upper()
exists = await get_valid_symbols(message.from_user.id, user_input) exists = await get_valid_symbols(message.from_user.id, user_input)
if not exists: if not exists:
@@ -187,20 +192,25 @@ async def start_trading_process(callback: CallbackQuery) -> None:
Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции, Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
затем запускает торговый цикл с задержкой или без неё. затем запускает торговый цикл с задержкой или без неё.
""" """
await callback.answer()
tg_id = callback.from_user.id tg_id = callback.from_user.id
message = callback.message message = callback.message
data_main_stgs = await rq.get_user_main_settings(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id)
symbol = await rq.get_symbol(tg_id) symbol = await rq.get_symbol(tg_id)
margin_mode = data_main_stgs.get('margin_type', 'Isolated') margin_mode = data_main_stgs.get('margin_type', 'Isolated')
trading_mode = data_main_stgs.get('trading_mode') trading_mode = data_main_stgs.get('trading_mode')
switch_mode = data_main_stgs.get('switch_mode_enabled')
starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
switch_state = data_main_stgs.get("switch_state", "По направлению")
side = None if trading_mode == 'Switch':
if switch_mode == 'Включено': if switch_state == "По направлению":
switch_state = data_main_stgs.get('switch_state', 'Long') side = data_main_stgs.get("last_side")
side = 'Buy' if switch_state == 'Long' else 'Sell' else:
side = data_main_stgs.get("last_side")
if side.lower() == "buy":
side = "Sell"
else:
side = "Buy"
else: else:
if trading_mode == 'Long': if trading_mode == 'Long':
side = 'Buy' side = 'Buy'
@@ -209,7 +219,6 @@ async def start_trading_process(callback: CallbackQuery) -> None:
else: else:
await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.", await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
reply_markup=inline_markup.back_to_main) reply_markup=inline_markup.back_to_main)
await callback.answer()
return return
await message.answer("Начинаю торговлю с использованием текущих настроек...") await message.answer("Начинаю торговлю с использованием текущих настроек...")
@@ -221,14 +230,39 @@ async def start_trading_process(callback: CallbackQuery) -> None:
timer_minute = timer_data or 0 timer_minute = timer_data or 0
if timer_minute > 0: if timer_minute > 0:
await trading_cycle(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, await message.answer(f"Торговля начнётся через {timer_minute} мин.", reply_markup=inline_markup.cancel_start)
starting_quantity=starting_quantity)
await message.answer(f"Торговля начнётся через {timer_minute} мин.") async def delay_start():
try:
await asyncio.sleep(timer_minute * 60)
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
await rq.update_user_timer(tg_id, minutes=0) await rq.update_user_timer(tg_id, minutes=0)
except asyncio.exceptions.CancelledError:
logger.exception(f"Торговый цикл для пользователя {tg_id} был отменён.")
raise
task = asyncio.create_task(delay_start())
user_trade_tasks[tg_id] = task
else: else:
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity) await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel_start")
async def cancel_start_trading(callback: CallbackQuery):
tg_id = callback.from_user.id
task = user_trade_tasks.get(tg_id)
if task and not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
user_trade_tasks.pop(tg_id, None)
await rq.update_user_timer(tg_id, minutes=0)
await callback.message.answer("Запуск торговли отменён.", reply_markup=inline_markup.back_to_main)
await callback.message.edit_reply_markup(reply_markup=None)
else:
await callback.answer("Нет запланированной задачи запуска.", show_alert=True)
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals") @router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
@@ -437,7 +471,8 @@ async def process_close_delay(message: Message, state: FSMContext) -> None:
symbol = data.get("symbol") symbol = data.get("symbol")
delay = delay_minutes * 60 delay = delay_minutes * 60
await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.") await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.",
reply_markup=inline_markup.back_to_main)
await close_trade_after_delay(message.from_user.id, message, symbol, delay) await close_trade_after_delay(message.from_user.id, message, symbol, delay)
await state.clear() await state.clear()
@@ -448,7 +483,7 @@ async def reset_martingale(callback: CallbackQuery) -> None:
Сбрасывает шаги мартингейла пользователя. Сбрасывает шаги мартингейла пользователя.
""" """
tg_id = callback.from_user.id tg_id = callback.from_user.id
await rq.update_martingale_step(tg_id, 0) await rq.update_martingale_step(tg_id, 1)
await callback.answer("Сброс шагов выполнен.") await callback.answer("Сброс шагов выполнен.")
await main_settings_message(tg_id, callback.message) await main_settings_message(tg_id, callback.message)
@@ -479,11 +514,11 @@ async def stop_immediately(callback: CallbackQuery):
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer") @router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
""" """
Запускает диалог с пользователем для задания задержки перед остановкой торговли. Запускает диалог с пользователем для задания задержки до остановки торговли.
""" """
await state.set_state(CloseTradeTimerState.waiting_for_trade) await state.set_state(CloseTradeTimerState.waiting_for_trade)
await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", await callback.message.answer("Введите задержку в минутах до остановки торговли:",
reply_markup=inline_markup.cancel) reply_markup=inline_markup.cancel)
await callback.answer() await callback.answer()
@@ -505,7 +540,8 @@ async def process_stop_delay(message: Message, state: FSMContext):
tg_id = message.from_user.id tg_id = message.from_user.id
delay_seconds = delay_minutes * 60 delay_seconds = delay_minutes * 60
await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.") await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.",
reply_markup=inline_markup.back_to_main)
await asyncio.sleep(delay_seconds) await asyncio.sleep(delay_seconds)
await rq.update_trigger(tg_id, "Ручной") await rq.update_trigger(tg_id, "Ручной")
await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main) await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main)

View File

@@ -9,6 +9,10 @@ settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')] [InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')]
]) ])
cancel_start = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Отменить запуск", callback_data="clb_cancel_start")]
])
back_btn_list_settings = [InlineKeyboardButton(text="Назад", back_btn_list_settings = [InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек
back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад", back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад",
@@ -25,8 +29,8 @@ special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'), [InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'),
InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')], InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')],
[InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings'), [InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings')],
InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')], # InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')],
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')],
back_btn_to_main back_btn_to_main
]) ])
@@ -46,10 +50,8 @@ trading_markup = InlineKeyboardMarkup(inline_keyboard=[
]) ])
start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[ 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_start_chatbot_trading")],
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], [InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
]) ])
cancel = InlineKeyboardMarkup(inline_keyboard=[ cancel = InlineKeyboardMarkup(inline_keyboard=[
@@ -73,7 +75,7 @@ back_to_main = InlineKeyboardMarkup(inline_keyboard=[
main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),
InlineKeyboardButton(text='Режим свитч', callback_data='clb_change_switch_mode'), InlineKeyboardButton(text='Состояние свитча', callback_data='clb_change_switch_state'),
InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')], InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')],
[InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'), [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'),
@@ -101,14 +103,14 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'), [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'),
InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')], InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')],
#
[InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), # [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')], # InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')],
#
[InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'), # [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'),
InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')], # InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')],
#
[InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')], # [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')],
back_btn_list_settings, back_btn_list_settings,
back_btn_to_main back_btn_to_main
@@ -127,7 +129,8 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"), [InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"),
InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"), InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"),
InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], 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 back_btn_to_main
@@ -137,8 +140,7 @@ margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"), [InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"),
InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")], InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")],
back_btn_list_settings, back_btn_list_settings
back_btn_to_main
]) ])
trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
@@ -196,6 +198,7 @@ def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup:
timer_markup = InlineKeyboardMarkup(inline_keyboard=[ timer_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
[InlineKeyboardButton(text="Удалить таймер", callback_data="clb_delete_timer")],
back_btn_to_main back_btn_to_main
]) ])
@@ -208,14 +211,7 @@ stop_choice_markup = InlineKeyboardMarkup(
] ]
) )
buttons_on_off_markup_for_switch = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Включить', callback_data="clb_on_switch"),
InlineKeyboardButton(text='Выключить', callback_data="clb_off_switch")],
[InlineKeyboardButton(text="Изменить состояние", callback_data="clb_switch_state")],
back_btn_to_main
])
switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[ switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Long', callback_data="clb_long_switch"), [InlineKeyboardButton(text='По направлению', callback_data="clb_long_switch"),
InlineKeyboardButton(text='Short', callback_data="clb_short_switch")], InlineKeyboardButton(text='Против направления', callback_data="clb_short_switch")],
]) ])

View File

@@ -54,10 +54,10 @@ class User_Bybit_API(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
api_key = mapped_column(String(18), default='None') api_key = mapped_column(String(18), unique=True, nullable=True)
secret_key = mapped_column(String(36), default='None') secret_key = mapped_column(String(36), unique=True, nullable=True)
class User_Symbol(Base): class User_Symbol(Base):
@@ -65,25 +65,12 @@ class User_Symbol(Base):
Модель таблицы user_main_settings. Модель таблицы user_main_settings.
Хранит основные настройки торговли для пользователя. Хранит основные настройки торговли для пользователя.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Внешний ключ на Telegram пользователя.
trading_mode (str): Режим торговли (ForeignKey на trading_modes.mode).
margin_type (str): Тип маржи (ForeignKey на margin_types.type).
size_leverage (int): Кредитное плечо.
starting_quantity (int): Начальный объём позиции.
martingale_factor (int): Коэффициент мартингейла.
martingale_step (int): Текущий шаг мартингейла.
maximal_quantity (int): Максимальное количество шагов мартингейла.
entry_order_type (str): Тип входа (Market или Limit).
limit_order_price (str, optional): Цена лимитного ордера, если используется.
""" """
__tablename__ = 'user_symbols' __tablename__ = 'user_symbols'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT') symbol = mapped_column(String(18), default='PENGUUSDT')
@@ -94,7 +81,7 @@ class Trading_Mode(Base):
Атрибуты: Атрибуты:
id (int): Первичный ключ. id (int): Первичный ключ.
mode (str): Уникальный режим (например, 'Long', 'Short'). mode (str): Уникальный режим (например, 'Long', 'Short', 'Switch).
""" """
__tablename__ = 'trading_modes' __tablename__ = 'trading_modes'
@@ -123,7 +110,7 @@ class Trigger(Base):
Справочник триггеров для сделок. Справочник триггеров для сделок.
Атрибуты: Атрибуты:
id (int): Первичный ключ.. id (int): Первичный ключ.
""" """
__tablename__ = 'triggers' __tablename__ = 'triggers'
@@ -148,24 +135,25 @@ class User_Main_Settings(Base):
maximal_quantity (int): Максимальное число шагов мартингейла. maximal_quantity (int): Максимальное число шагов мартингейла.
entry_order_type (str): Тип ордера входа (Market/Limit). entry_order_type (str): Тип ордера входа (Market/Limit).
limit_order_price (Optional[str]): Цена лимитного ордера, если есть. limit_order_price (Optional[str]): Цена лимитного ордера, если есть.
last_side (str): Последняя сторона ордера.
""" """
__tablename__ = 'user_main_settings' __tablename__ = 'user_main_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
trading_mode = mapped_column(ForeignKey("trading_modes.mode")) trading_mode = mapped_column(ForeignKey("trading_modes.mode"))
margin_type = mapped_column(ForeignKey("margin_types.type")) margin_type = mapped_column(ForeignKey("margin_types.type"))
switch_mode_enabled = mapped_column(String(15), default="Выключен") switch_state = mapped_column(String(10), default='По направлению')
switch_state = mapped_column(String(10), default='Long')
size_leverage = mapped_column(Integer(), default=1) size_leverage = mapped_column(Integer(), default=1)
starting_quantity = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1)
martingale_factor = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1)
martingale_step = mapped_column(Integer(), default=0) martingale_step = mapped_column(Integer(), default=1)
maximal_quantity = mapped_column(Integer(), default=10) maximal_quantity = mapped_column(Integer(), default=10)
entry_order_type = mapped_column(String(10), default='Market') entry_order_type = mapped_column(String(10), default='Market')
limit_order_price = mapped_column(Numeric(18, 15), nullable=True) limit_order_price = mapped_column(Numeric(18, 15), nullable=True)
last_side = mapped_column(String(10), default='Buy')
class User_Risk_Management_Settings(Base): class User_Risk_Management_Settings(Base):
@@ -184,7 +172,7 @@ class User_Risk_Management_Settings(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
price_profit = mapped_column(Integer(), default=1) price_profit = mapped_column(Integer(), default=1)
price_loss = mapped_column(Integer(), default=1) price_loss = mapped_column(Integer(), default=1)
@@ -211,9 +199,9 @@ class User_Condition_Settings(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
trigger = mapped_column(String(15), default='Ручной') trigger = mapped_column(String(15), default='Автоматический')
filter_time = mapped_column(String(25), default='???') filter_time = mapped_column(String(25), default='???')
filter_volatility = mapped_column(Boolean, default=False) filter_volatility = mapped_column(Boolean, default=False)
external_cues = mapped_column(Boolean, default=False) external_cues = mapped_column(Boolean, default=False)
@@ -237,7 +225,7 @@ class User_Additional_Settings(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
pattern_save = mapped_column(Boolean, default=False) pattern_save = mapped_column(Boolean, default=False)
autostart = mapped_column(Boolean, default=False) autostart = mapped_column(Boolean, default=False)
@@ -260,7 +248,7 @@ class USER_DEALS(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT') symbol = mapped_column(String(18), default='PENGUUSDT')
side = mapped_column(String(10), nullable=False) side = mapped_column(String(10), nullable=False)
@@ -282,7 +270,7 @@ class UserTimer(Base):
__tablename__ = 'user_timers' __tablename__ = 'user_timers'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
timer_minutes = mapped_column(Integer, nullable=False, default=0) timer_minutes = mapped_column(Integer, nullable=False, default=0)
timer_start = mapped_column(DateTime, default=datetime.utcnow) timer_start = mapped_column(DateTime, default=datetime.utcnow)
timer_end = mapped_column(DateTime, nullable=True) timer_end = mapped_column(DateTime, nullable=True)
@@ -296,7 +284,7 @@ async def async_main():
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
# Заполнение таблиц # Заполнение таблиц
modes = ['Long', 'Short', 'Smart'] modes = ['Long', 'Short', 'Switch', 'Smart']
for mode in modes: for mode in modes:
result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode)) result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode))
if not result.first(): if not result.first():
@@ -309,3 +297,10 @@ async def async_main():
if not result.first(): if not result.first():
logger.info("Заполение таблицы типов маржи") logger.info("Заполение таблицы типов маржи")
await conn.execute(Margin_type.__table__.insert().values(type=type)) await conn.execute(Margin_type.__table__.insert().values(type=type))
last_side = ['Buy', 'Sell']
for side in last_side:
result = await conn.execute(select(User_Main_Settings).where(User_Main_Settings.last_side == side))
if not result.first():
logger.info("Заполение таблицы последнего направления")
await conn.execute(User_Main_Settings.__table__.insert().values(last_side=side))

View File

@@ -239,17 +239,21 @@ async def update_symbol(tg_id: int, symbol: str) -> None:
await session.commit() await session.commit()
async def update_api_key(tg_id: int, api: str) -> None: async def upsert_api_keys(tg_id: int, api_key: str, secret_key: str) -> None:
"""Обновить API ключ пользователя.""" """Обновить API ключ пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api)) result = await session.execute(select(UBA).where(UBA.tg_id == tg_id))
await session.commit() user = result.scalars().first()
if user:
if api_key is not None:
async def update_secret_key(tg_id: int, api: str) -> None: user.api_key = api_key
"""Обновить секретный ключ пользователя.""" if secret_key is not None:
async with async_session() as session: user.secret_key = secret_key
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api)) logger.info(f"Обновлены ключи для пользователя {tg_id}")
else:
new_user = UBA(tg_id=tg_id, api_key=api_key, secret_key=secret_key)
session.add(new_user)
logger.info(f"Добавлен новый пользователь {tg_id} с ключами")
await session.commit() await session.commit()
@@ -303,36 +307,20 @@ async def get_user_main_settings(tg_id):
"""Получить основные настройки пользователя.""" """Получить основные настройки пользователя."""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
if user: if user:
logger.info("Получение основных настроек пользователя %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))
switch_mode_enabled = await session.scalar(select(UMS.switch_mode_enabled).where(UMS.tg_id == tg_id))
switch_state = await session.scalar(select(UMS.switch_state).where(UMS.tg_id == tg_id))
size_leverage = await session.scalar(select(UMS.size_leverage).where(UMS.tg_id == tg_id))
starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id))
martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id))
maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id))
entry_order_type = await session.scalar(select(UMS.entry_order_type).where(UMS.tg_id == tg_id))
limit_order_price = await session.scalar(select(UMS.limit_order_price).where(UMS.tg_id == tg_id))
martingale_step = await session.scalar(select(UMS.martingale_step).where(UMS.tg_id == tg_id))
data = { data = {
'trading_mode': trading_mode, 'trading_mode': user.trading_mode,
'margin_type': margin_mode, 'margin_type': user.margin_type,
'switch_mode_enabled': switch_mode_enabled, 'switch_state': user.switch_state,
'switch_state': switch_state, 'size_leverage': user.size_leverage,
'size_leverage': size_leverage, 'starting_quantity': user.starting_quantity,
'starting_quantity': starting_quantity, 'martingale_factor': user.martingale_factor,
'martingale_factor': martingale_factor, 'maximal_quantity': user.maximal_quantity,
'maximal_quantity': maximal_quantity, 'entry_order_type': user.entry_order_type,
'entry_order_type': entry_order_type, 'limit_order_price': user.limit_order_price,
'limit_order_price': limit_order_price, 'martingale_step': user.martingale_step,
'martingale_step': martingale_step, 'last_side': user.last_side,
} }
return data return data
@@ -576,3 +564,22 @@ async def update_trigger(tg_id, trigger):
await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger)) await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger))
await session.commit() await session.commit()
async def set_last_series_info(tg_id: int, last_side: str):
async with async_session() as session:
async with session.begin():
# Обновляем запись
result = await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(last_side=last_side)
)
if result.rowcount == 0:
# Если запись не существует, создаём новую
new_entry = UMS(
tg_id=tg_id,
last_side=last_side,
)
session.add(new_entry)
await session.commit()

View File

@@ -29,12 +29,7 @@ async def main_settings_message(id, message):
text = f""" <b>Условия запуска</b> text = f""" <b>Условия запуска</b>
<b>- Режим торговли:</b> {trigger} <b>- Режим торговли:</b> {trigger}
<b>- Таймер: </b> установить таймер / остановить таймер <b>- Таймер: </b> установить таймер / удалить таймер
<b>- Фильтр волатильности / объёма: </b> включить/отключить
<b>- Интеграции и внешние сигналы: </b>
<b>- Использовать сигналы TradingView:</b> да / нет
<b>- Использовать AI-аналитику от ChatGPT:</b> да / не
<b>- Webhook URL для сигналов (если используется TradingView): </b>
""" """
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup)
@@ -42,7 +37,7 @@ async def main_settings_message(id, message):
async def trigger_message(id, message, state: FSMContext): async def trigger_message(id, message, state: FSMContext):
await state.set_state(condition_settings.trigger) await state.set_state(condition_settings.trigger)
text = ''' text = '''
<b>- Автоматический:</b> торговля будет продолжаться до условии остановки. <b>- Автоматический:</b> торговля будет происходить в рамках серии ставок.
<b>- Ручной:</b> торговля будет происходить только в ручном режиме. <b>- Ручной:</b> торговля будет происходить только в ручном режиме.
<em>- Выберите тип триггера:</em>''' <em>- Выберите тип триггера:</em>'''
@@ -73,7 +68,7 @@ async def timer_message(id, message: Message, state: FSMContext):
return return
await message.answer( await message.answer(
f"Таймер: {timer_info['timer_minutes']} мин\n", f"Таймер установлен на: {timer_info['timer_minutes']} мин\n",
reply_markup=inline_markup.timer_markup reply_markup=inline_markup.timer_markup
) )
@@ -81,7 +76,7 @@ async def timer_message(id, message: Message, state: FSMContext):
@condition_settings_router.callback_query(F.data == "clb_set_timer") @condition_settings_router.callback_query(F.data == "clb_set_timer")
async def set_timer_callback(callback: CallbackQuery, state: FSMContext): async def set_timer_callback(callback: CallbackQuery, state: FSMContext):
await state.set_state(condition_settings.timer) # состояние для ввода времени await state.set_state(condition_settings.timer) # состояние для ввода времени
await callback.message.answer("Введите время работы в минутах (например, 60):") await callback.message.answer("Введите время работы в минутах (например, 60):", reply_markup=inline_markup.cancel)
await callback.answer() await callback.answer()
@@ -94,14 +89,21 @@ async def process_timer_input(message: Message, state: FSMContext):
return return
await rq.update_user_timer(message.from_user.id, minutes) await rq.update_user_timer(message.from_user.id, minutes)
await message.answer(f"Таймер установлен на {minutes} минут.\nНажмите кнопку 'Начать торговлю' для запуска.", logger.info("Timer set for user %s: %s minutes", message.from_user.id, minutes)
reply_markup=inline_markup.start_trading_markup) await timer_message(message.from_user.id, message, state)
await state.clear() await state.clear()
except ValueError: except ValueError:
await message.reply("Пожалуйста, введите корректное число.") await message.reply("Пожалуйста, введите корректное число.")
@condition_settings_router.callback_query(F.data == "clb_delete_timer")
async def delete_timer_callback(callback: CallbackQuery, state: FSMContext):
await state.clear()
await rq.update_user_timer(callback.from_user.id, 0)
logger.info("Timer deleted for user %s", callback.from_user.id)
await timer_message(callback.from_user.id, callback.message, state)
await callback.answer()
async def filter_volatility_message(message, state): async def filter_volatility_message(message, state):
text = '''Фильтр волатильности text = '''Фильтр волатильности

View File

@@ -5,6 +5,9 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
from app.services.Bybit.functions.Futures import safe_float, calculate_total_budget, get_bybit_client
from app.states.States import update_main_settings from app.states.States import update_main_settings
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
@@ -24,20 +27,54 @@ async def reg_new_user_default_main_settings(id, message):
async def main_settings_message(id, message): async def main_settings_message(id, message):
try:
data = await rq.get_user_main_settings(id) data = await rq.get_user_main_settings(id)
tg_id = id
data_main_stgs = await rq.get_user_main_settings(id)
data_risk_stgs = await rq.get_user_risk_management_settings(id)
client = await get_bybit_client(tg_id)
symbol = await rq.get_symbol(tg_id)
max_martingale_steps = (data_main_stgs or {}).get('maximal_quantity', 0)
commission_fee = (data_risk_stgs or {}).get('commission_fee')
starting_quantity = safe_float((data_main_stgs or {}).get('starting_quantity'))
martingale_factor = safe_float((data_main_stgs or {}).get('martingale_factor'))
fee_info = client.get_fee_rates(category='linear', symbol=symbol)
leverage = safe_float((data_main_stgs or {}).get('size_leverage'))
price = await get_price(tg_id, symbol=symbol)
entry_price = safe_float(price)
if commission_fee == "Да":
commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
else:
commission_fee_percent = 0.0
total_budget = await calculate_total_budget(
starting_quantity=starting_quantity,
martingale_factor=martingale_factor,
max_steps=max_martingale_steps,
commission_fee_percent=commission_fee_percent,
leverage=leverage,
current_price=entry_price,
)
await message.answer(f"""<b>Основные настройки</b> await message.answer(f"""<b>Основные настройки</b>
<b>- Режим торговли:</b> {data['trading_mode']} <b>- Режим торговли:</b> {data['trading_mode']}
<b>- Режим свитч:</b> {data['switch_mode_enabled']} <b>- Состояние свитча:</b> {data['switch_state']}
<b>- Состояние свитча:</b> {data['switch_state']} <b>- Направление последней сделки:</b> {data['last_side']}
<b>- Тип маржи:</b> {data['margin_type']} <b>- Тип маржи:</b> {data['margin_type']}
<b>- Размер кредитного плеча:</b> х{data['size_leverage']} <b>- Размер кредитного плеча:</b> х{data['size_leverage']}
<b>- Начальная ставка:</b> {data['starting_quantity']} <b>- Начальная ставка:</b> {data['starting_quantity']}
<b>- Коэффициент мартингейла:</b> {data['martingale_factor']} <b>- Коэффициент мартингейла:</b> {data['martingale_factor']}
<b>- Количество ставок в серии:</b> {data['martingale_step']} <b>- Текущий шаг:</b> {data['martingale_step']}
<b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']} <b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup)
<b>- Требуемый бюджет:</b> {total_budget:.2f} USDT
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup)
except PermissionError as e:
logger.error("Authenticated endpoints require keys: %s", e)
await message.answer("Вы не авторизованы.", reply_markup=inline_markup.connect_bybit_api_message)
async def trading_mode_message(message, state): async def trading_mode_message(message, state):
@@ -49,7 +86,7 @@ async def trading_mode_message(message, state):
<b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены. <b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены.
<b>Смарт</b> — автоматизированный режим, который подбирает оптимальную стратегию в зависимости от текущих рыночных условий. <b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности.
<em>Выберите ниже для изменений:</em> <em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.trading_mode_markup) """, parse_mode='html', reply_markup=inline_markup.trading_mode_markup)
@@ -77,6 +114,13 @@ async def state_trading_mode(callback: CallbackQuery, state):
await state.clear() await state.clear()
case 'trade_mode_switch':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Switch")
await rq.update_trade_mode_user(id, 'Switch')
await main_settings_message(id, callback.message)
await state.clear()
case 'trade_mode_smart': case 'trade_mode_smart':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart") await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart")
await rq.update_trade_mode_user(id, 'Smart') await rq.update_trade_mode_user(id, 'Smart')
@@ -91,45 +135,27 @@ async def switch_mode_enabled_message(message, state):
await state.set_state(update_main_settings.switch_mode_enabled) await state.set_state(update_main_settings.switch_mode_enabled)
await message.edit_text( await message.edit_text(
"""<b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности. f"""<b> Состояние свитча</b>
<b>По направлению</b> - по направлению последней сделки предыдущей серии
<b>Против направления</b> - против направления последней сделки предыдущей серии
<em>По умолчанию при первом запуске бота, направление сделки установлено на "Buy".</em>
<em>Выберите ниже для изменений:</em>""", parse_mode='html', <em>Выберите ниже для изменений:</em>""", parse_mode='html',
reply_markup=inline_markup.buttons_on_off_markup_for_switch) reply_markup=inline_markup.switch_state_markup)
@router_main_settings.callback_query(lambda c: c.data in ["clb_on_switch", "clb_off_switch"])
async def state_switch_mode_enabled(callback: CallbackQuery, state):
await callback.answer()
tg_id = callback.from_user.id
val = "Включить" if callback.data == "clb_on_switch" else "Выключить"
if val == "Включить":
await rq.update_switch_mode_enabled(tg_id, "Включено")
await callback.answer(f"Включено")
await main_settings_message(tg_id, callback.message)
else:
await rq.update_switch_mode_enabled(tg_id, "Выключено")
await callback.answer(f"Выключено")
await main_settings_message(tg_id, callback.message)
await state.clear()
@router_main_settings.callback_query(lambda c: c.data in ["clb_switch_state"])
async def state_switch_mode_enabled(callback: CallbackQuery):
await callback.answer()
await callback.message.answer("Выберите состояние свитча:", reply_markup=inline_markup.switch_state_markup)
@router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"]) @router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"])
async def state_switch_mode_enabled(callback: CallbackQuery, state): async def state_switch_mode_enabled(callback: CallbackQuery, state):
await callback.answer() await callback.answer()
tg_id = callback.from_user.id tg_id = callback.from_user.id
val = "Long" if callback.data == "clb_long_switch" else "Short" val = "По направлению" if callback.data == "clb_long_switch" else "Против направления"
if val == "Long": if val == "По направлению":
await rq.update_switch_state(tg_id, "Long") await rq.update_switch_state(tg_id, "По направлению")
await callback.message.answer(f"Состояние свитча: {val}") await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message) await main_settings_message(tg_id, callback.message)
else: else:
await rq.update_switch_state(tg_id, "Short") await rq.update_switch_state(tg_id, "Против направления")
await callback.message.answer(f"Состояние свитча: {val}") await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message) await main_settings_message(tg_id, callback.message)
await state.clear() await state.clear()
@@ -144,23 +170,47 @@ async def size_leverage_message(message, state):
@router_main_settings.message(update_main_settings.size_leverage) @router_main_settings.message(update_main_settings.size_leverage)
async def state_size_leverage(message: Message, state): async def state_size_leverage(message: Message, state):
try:
leverage = float(message.text)
if leverage <= 0:
raise ValueError("Неверное значение")
except ValueError:
await message.answer(
"Ошибка: пожалуйста, введите положительное число для кредитного плеча."
"\nПопробуйте снова."
)
return
await state.update_data(size_leverage=message.text) await state.update_data(size_leverage=message.text)
data = await state.get_data() data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id) tg_id = message.from_user.id
symbol = await rq.get_symbol(tg_id)
leverage = data['size_leverage']
client = await get_bybit_client(tg_id)
if data['size_leverage'].isdigit() and int(data['size_leverage']) <= 100: instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
await message.answer(f"✅ Изменено: {data_settings['size_leverage']}{data['size_leverage']}") info = instruments_resp.get("result", {}).get("list", [])
await rq.update_size_leverange(message.from_user.id, data['size_leverage']) max_leverage = safe_float(info[0].get("leverageFilter", {}).get("maxLeverage", 0))
if safe_float(leverage) > max_leverage:
await message.answer(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
f"Устанавливаю максимальное.",
reply_markup=inline_markup.back_to_main,
)
logger.info(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
await rq.update_size_leverange(message.from_user.id, max_leverage)
await main_settings_message(message.from_user.id, message) await main_settings_message(message.from_user.id, message)
await state.clear() await state.clear()
else: else:
await message.answer( await message.answer(f"✅ Изменено: {leverage}")
f'⛔️ Ошибка: ваше значение ({data['size_leverage']}) или выше лимита (100) или вы вводите неверные символы') await rq.update_size_leverange(message.from_user.id, safe_float(leverage))
await main_settings_message(message.from_user.id, message) await main_settings_message(message.from_user.id, message)
await state.clear()
async def martingale_factor_message(message, state): async def martingale_factor_message(message, state):
@@ -213,13 +263,16 @@ async def margin_type_message(message, state):
@router_main_settings.callback_query(update_main_settings.margin_type) @router_main_settings.callback_query(update_main_settings.margin_type)
async def state_margin_type(callback: CallbackQuery, state): async def state_margin_type(callback: CallbackQuery, state):
callback_data = callback.data
if callback_data in ['margin_type_isolated', 'margin_type_cross']:
tg_id = callback.from_user.id tg_id = callback.from_user.id
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
data_settings = await rq.get_user_main_settings(tg_id) data_settings = await rq.get_user_main_settings(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)
try: try:
active_positions = client.get_positions(category='linear', settleCoin='USDT') active_positions = client.get_positions(category='linear', settleCoin="USDT")
positions = active_positions.get('result', {}).get('list', []) positions = active_positions.get('result', {}).get('list', [])
except Exception as e: except Exception as e:
@@ -230,13 +283,14 @@ async def state_margin_type(callback: CallbackQuery, state):
size = pos.get('size') size = pos.get('size')
if float(size) > 0: if float(size) > 0:
await callback.answer( await callback.answer(
"⚠️ Маржинальный режим нельзя менять при открытой позиции", "⚠️ Маржинальный режим нельзя менять при открытой позиции"
show_alert=True
) )
return return
try: try:
match callback.data: match callback.data:
case 'margin_type_isolated': case 'margin_type_isolated':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated") await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated")
await rq.update_margin_type(tg_id, 'Isolated') await rq.update_margin_type(tg_id, 'Isolated')
@@ -244,6 +298,7 @@ async def state_margin_type(callback: CallbackQuery, state):
await state.clear() await state.clear()
case 'margin_type_cross': case 'margin_type_cross':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross") await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross")
await rq.update_margin_type(tg_id, 'Cross') await rq.update_margin_type(tg_id, 'Cross')
@@ -252,6 +307,11 @@ async def state_margin_type(callback: CallbackQuery, state):
await state.clear() await state.clear()
except Exception as e: except Exception as e:
logger.error(f"error: {e}") logger.error(f"error: {e}")
else:
await callback.answer()
await main_settings_message(callback.from_user.id, callback.message)
await state.clear()
async def starting_quantity_message(message, state): async def starting_quantity_message(message, state):

View File

@@ -1,7 +1,7 @@
import logging.config import logging.config
from aiogram import F, Router from aiogram import F, Router
from aiogram.filters import CommandStart from aiogram.filters import CommandStart, Command
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -23,7 +23,7 @@ logger = logging.getLogger("handlers")
router = Router() router = Router()
@router.message(Command("start"))
@router.message(CommandStart()) @router.message(CommandStart())
async def start_message(message: Message) -> None: async def start_message(message: Message) -> None:
""" """
@@ -37,6 +37,7 @@ async def start_message(message: Message) -> None:
await func.start_message(message) await func.start_message(message)
@router.message(Command("profile"))
@router.message(F.text == "👤 Профиль") @router.message(F.text == "👤 Профиль")
async def profile_message(message: Message) -> None: async def profile_message(message: Message) -> None:
""" """
@@ -52,6 +53,12 @@ async def profile_message(message: Message) -> None:
if user and balance: if user and balance:
await run_ws_for_user(tg_id, message) await run_ws_for_user(tg_id, message)
await func.profile_message(message.from_user.username, message) await func.profile_message(message.from_user.username, message)
else:
await rq.save_tg_id_new_user(message.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id, message)
await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
@router.callback_query(F.data == "clb_start_chatbot_message") @router.callback_query(F.data == "clb_start_chatbot_message")
@@ -64,6 +71,8 @@ async def clb_profile_msg(callback: CallbackQuery) -> None:
Args: Args:
callback (CallbackQuery): Полученный колбэк. callback (CallbackQuery): Полученный колбэк.
""" """
tg_id = callback.from_user.id
message = callback.message
user = await rq.check_user(callback.from_user.id) user = await rq.check_user(callback.from_user.id)
balance = await get_balance(callback.from_user.id, callback.message) balance = await get_balance(callback.from_user.id, callback.message)
first_name = callback.from_user.first_name or "" first_name = callback.from_user.first_name or ""
@@ -71,6 +80,7 @@ async def clb_profile_msg(callback: CallbackQuery) -> None:
username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь" username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь"
if user and balance: if user and balance:
await run_ws_for_user(tg_id, message)
await func.profile_message(callback.from_user.username, callback.message) await func.profile_message(callback.from_user.username, callback.message)
else: else:
await rq.save_tg_id_new_user(callback.from_user.id) await rq.save_tg_id_new_user(callback.from_user.id)
@@ -164,7 +174,7 @@ async def clb_change_additional_message(callback: CallbackQuery) -> None:
# Конкретные настройки каталогов # Конкретные настройки каталогов
list_main_settings = ['clb_change_trading_mode', list_main_settings = ['clb_change_trading_mode',
'clb_change_switch_mode', 'clb_change_switch_state',
'clb_change_margin_type', 'clb_change_margin_type',
'clb_change_size_leverage', 'clb_change_size_leverage',
'clb_change_starting_quantity', 'clb_change_starting_quantity',
@@ -188,7 +198,7 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> N
match callback.data: match callback.data:
case 'clb_change_trading_mode': case 'clb_change_trading_mode':
await func_main_settings.trading_mode_message(callback.message, state) await func_main_settings.trading_mode_message(callback.message, state)
case 'clb_change_switch_mode': case 'clb_change_switch_state':
await func_main_settings.switch_mode_enabled_message(callback.message, state) await func_main_settings.switch_mode_enabled_message(callback.message, state)
case 'clb_change_margin_type': case 'clb_change_margin_type':
await func_main_settings.margin_type_message(callback.message, state) await func_main_settings.margin_type_message(callback.message, state)

Binary file not shown.

View File

@@ -80,7 +80,7 @@ LOGGING_CONFIG = {
"level": "DEBUG", "level": "DEBUG",
"propagate": False, "propagate": False,
}, },
"conditions_settings": { "condition_settings": {
"handlers": ["console", "timed_rotating_file"], "handlers": ["console", "timed_rotating_file"],
"level": "DEBUG", "level": "DEBUG",
"propagate": False, "propagate": False,

View File

@@ -6,22 +6,40 @@ aiosignal==1.4.0
aiosqlite==0.21.0 aiosqlite==0.21.0
annotated-types==0.7.0 annotated-types==0.7.0
attrs==25.3.0 attrs==25.3.0
black==25.1.0
certifi==2025.8.3 certifi==2025.8.3
charset-normalizer==3.4.3 charset-normalizer==3.4.3
click==8.2.1
colorama==0.4.6
dotenv==0.9.9 dotenv==0.9.9
flake8==7.3.0
flake8-bugbear==24.12.12
flake8-pie==0.16.0
frozenlist==1.7.0 frozenlist==1.7.0
greenlet==3.2.4 greenlet==3.2.4
idna==3.10 idna==3.10
isort==6.0.1
magic-filter==1.0.12 magic-filter==1.0.12
mando==0.7.1
mccabe==0.7.0
multidict==6.6.4 multidict==6.6.4
mypy_extensions==1.1.0
nest-asyncio==1.6.0 nest-asyncio==1.6.0
packaging==25.0
pathspec==0.12.1
platformdirs==4.4.0
propcache==0.3.2 propcache==0.3.2
pybit==5.11.0 pybit==5.11.0
pycodestyle==2.14.0
pycryptodome==3.23.0 pycryptodome==3.23.0
pydantic==2.11.7 pydantic==2.11.7
pydantic_core==2.33.2 pydantic_core==2.33.2
pyflakes==3.4.0
python-dotenv==1.1.1 python-dotenv==1.1.1
radon==6.0.1
redis==6.4.0
requests==2.32.5 requests==2.32.5
six==1.17.0
SQLAlchemy==2.0.43 SQLAlchemy==2.0.43
typing-inspection==0.4.1 typing-inspection==0.4.1
typing_extensions==4.14.1 typing_extensions==4.14.1