diff --git a/.dockerignore b/.dockerignore
index 50e16ff..6275699 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,35 +1,20 @@
node_modules
__pycache__
jsconfig.json
-.eslintrc.js
+^\.*
*.md
-.vscode
-.devcontainer
-.ini
-.git
-.github
-.gitignore
-.github/
docs/
ecosystem.config.js
-.http
mongo_data
mongo_data.old
mongo_db
kafka_data
-.DS_Store
-.env.local
-.env.prod
-.env.development.local
-.env.test.local
/.env
.vscode/settings.json
-.venv/
-.DS_Store
*.pyc
api/main/config/*.cfg
-.mypy_cache
db.dump
-.pytest_cache
tests/
*.md
+!.dockerignore
+!.gitignore
diff --git a/algorithms/coinrule.py b/algorithms/coinrule.py
index abb4b06..976b8be 100644
--- a/algorithms/coinrule.py
+++ b/algorithms/coinrule.py
@@ -1,8 +1,8 @@
import os
-from shared.utils import round_numbers
from models.signals import SignalsConsumer
from shared.enums import KafkaTopics
+from shared.utils import round_numbers
def fast_and_slow_macd(
@@ -20,13 +20,7 @@ def fast_and_slow_macd(
bb_high, bb_mid, bb_low = self.bb_spreads()
# If volatility is too low, dynamic trailling will close too early with bb_spreads
- if (
- macd > macd_signal
- and ma_7 > ma_25
- and bb_high < 1
- and bb_high > 0.001
- and trend
- ):
+ if macd > macd_signal and ma_7 > ma_25 and bb_high < 1 and bb_high > 0.001:
msg = f"""
- [{os.getenv('ENV')}] {algo} #algorithm #{symbol}
- Current price: {close_price}
diff --git a/algorithms/ma_candlestick.py b/algorithms/ma_candlestick.py
index f58ddb1..5cd4d17 100644
--- a/algorithms/ma_candlestick.py
+++ b/algorithms/ma_candlestick.py
@@ -1,7 +1,8 @@
import os
+
+from models.signals import SignalsConsumer
from shared.enums import KafkaTopics
from shared.utils import round_numbers
-from models.signals import SignalsConsumer
# Algorithms based on Bollinguer bands
diff --git a/algorithms/price_changes.py b/algorithms/price_changes.py
index 0eb8cea..9868efa 100644
--- a/algorithms/price_changes.py
+++ b/algorithms/price_changes.py
@@ -1,54 +1,47 @@
import os
-from shared.utils import round_numbers
from models.signals import SignalsConsumer
from shared.enums import KafkaTopics
+
def price_rise_15(
self,
close_price,
symbol,
- run_autotrade,
prev_price,
p_value,
- r_value,
- btc_correlation
- volatility
+ btc_correlation,
):
"""
Price increase/decrease algorithm
https://www.binance.com/en/support/faq/understanding-top-movers-statuses-on-binance-spot-trading-18c97e8ab67a4e1b824edd590cae9f16
"""
-
+
algo = "price_rise_15_rally_pullback"
price_diff = (float(close_price) - float(prev_price)) / close_price
- volatility = round_numbers(volatility)
bb_high, bb_mid, bb_low = self.bb_spreads()
trend = self.define_strategy()
if 0.07 <= price_diff < 0.11:
first_line = "Price increase over 7%"
- elif -0.07 <= price_diff < -0.11 :
+ elif -0.07 <= price_diff < -0.11:
first_line = f"{algo} #algorithm over 7%"
else:
return
-
- msg = (f"""
+ msg = f"""
- [{os.getenv('ENV')}] {first_line} #{symbol}
- Current price: {close_price}
-- Log volatility (log SD): {volatility}
- P-value: {p_value}
- Pearson correlation with BTC: {btc_correlation["close_price"]}
- https://www.binance.com/en/trade/{symbol}
- Dashboard trade
-""")
-
+"""
+
value = SignalsConsumer(
- spread=volatility,
current_price=close_price,
msg=msg,
symbol=symbol,
@@ -58,9 +51,13 @@ def price_rise_15(
"bb_high": bb_high,
"bb_mid": bb_mid,
"bb_low": bb_low,
- }
+ },
)
- self.producer.send(KafkaTopics.signals.value, value=value.model_dump_json()).add_callback(self.base_producer.on_send_success).add_errback(self.base_producer.on_send_error)
+ self.producer.send(
+ KafkaTopics.signals.value, value=value.model_dump_json()
+ ).add_callback(self.base_producer.on_send_success).add_errback(
+ self.base_producer.on_send_error
+ )
return
diff --git a/algorithms/rally.py b/algorithms/rally.py
index 40bc17b..2ae8bae 100644
--- a/algorithms/rally.py
+++ b/algorithms/rally.py
@@ -1,6 +1,7 @@
import os
-from api.streaming.models import SignalsConsumer
+
from shared.enums import KafkaTopics
+from models.signals import SignalsConsumer
def rally_or_pullback(
@@ -19,6 +20,8 @@ def rally_or_pullback(
"""
Rally algorithm
+ Key difference with other algorithms is the trend is determined not by market
+ but day or minute percentage change
https://www.binance.com/en/support/faq/understanding-top-movers-statuses-on-binance-spot-trading-18c97e8ab67a4e1b824edd590cae9f16
"""
data = self.get_24_ticker(symbol)
@@ -39,62 +42,51 @@ def rally_or_pullback(
if day_diff <= 0.08 and minute_diff >= 0.05:
algo_type = "Rally"
- # trend = "uptrend"
if day_diff_pb >= 0.08 and minute_diff_pb <= 0.05:
algo_type = "Pullback"
- # trend = "downtrend"
if not algo_type:
return
- trend = self.define_strategy()
- if not trend:
- return
-
- bb_high, bb_mid, bb_low = self.bb_spreads()
-
- msg = f"""
- - [{os.getenv('ENV')}] {algo_type} #algorithm #{symbol}
- - Current price: {close_price}
- - Log volatility (log SD): {volatility}
- - Bollinguer bands spread: {(bb_high - bb_low) / bb_high }
- - Reversal? {"Yes" if self.market_domination_reversal else "No"}
- - https://www.binance.com/en/trade/{symbol}
- - Dashboard trade
- """
-
- if algo_type == "Pullback":
- algo = f"rally_{algo_type}"
-
- if (
- float(close_price) > float(open_price)
- and volatility > 0.09
- # and close_price < ma_25[len(ma_25) - 1]
- # and close_price < ma_25[len(ma_25) - 2]
- # and close_price < ma_25[len(ma_25) - 3]
- and close_price < ma_100[len(ma_100) - 1]
- and close_price < ma_100[len(ma_100) - 2]
- and close_price < ma_100[len(ma_100) - 3]
- ):
- value = SignalsConsumer(
- spread=None,
- current_price=close_price,
- msg=msg,
- symbol=symbol,
- algo=algo,
- trend=trend,
- bb_spreads={
- "bb_high": bb_high,
- "bb_mid": bb_mid,
- "bb_low": bb_low,
- },
- )
-
- self.producer.send(
- KafkaTopics.signals.value, value=value.model_dump_json()
- ).add_callback(self.base_producer.on_send_success).add_errback(
- self.base_producer.on_send_error
- )
+ if (
+ close_price < ma_25[len(ma_25) - 1]
+ and close_price < ma_25[len(ma_25) - 2]
+ and close_price < ma_25[len(ma_25) - 3]
+ and close_price < ma_100[len(ma_100) - 1]
+ and close_price < ma_100[len(ma_100) - 2]
+ and close_price < ma_100[len(ma_100) - 3]
+ ):
+ bb_high, bb_mid, bb_low = self.bb_spreads()
+
+ msg = f"""
+ - [{os.getenv('ENV')}] {algo_type} #algorithm #{symbol}
+ - Current price: {close_price}
+ - Log volatility (log SD): {volatility}
+ - Bollinguer bands spread: {(bb_high - bb_low) / bb_high }
+ - Reversal? {"Yes" if self.market_domination_reversal else "No"}
+ - https://www.binance.com/en/trade/{symbol}
+ - Dashboard trade
+ """
+
+ value = SignalsConsumer(
+ spread=None,
+ current_price=close_price,
+ msg=msg,
+ symbol=symbol,
+ algo=algo_type,
+ trend=volatility,
+ bb_spreads={
+ "bb_high": bb_high,
+ "bb_mid": bb_mid,
+ "bb_low": bb_low,
+ },
+ )
+
+ self.producer.send(
+ KafkaTopics.signals.value, value=value.model_dump_json()
+ ).add_callback(self.base_producer.on_send_success).add_errback(
+ self.base_producer.on_send_error
+ )
return
diff --git a/algorithms/timeseries_gpt.py b/algorithms/timeseries_gpt.py
index e5fc1b9..57bedab 100644
--- a/algorithms/timeseries_gpt.py
+++ b/algorithms/timeseries_gpt.py
@@ -1,10 +1,5 @@
-import json
import os
-import pandas as pd
-from shared.enums import KafkaTopics
-from shared.utils import round_numbers
-from models.signals import SignalsConsumer
-from shared.enums import KafkaTopics
+
from nixtla import NixtlaClient
nixtla_client = NixtlaClient(os.environ.get("NIXTLA_API_KEY"))
diff --git a/algorithms/top_gainer_drop.py b/algorithms/top_gainer_drop.py
index addfac7..063b663 100644
--- a/algorithms/top_gainer_drop.py
+++ b/algorithms/top_gainer_drop.py
@@ -1,18 +1,18 @@
import os
-
-from shared.enums import KafkaTopics
+from models.signals import SignalsConsumer
+from shared.enums import KafkaTopics, TrendEnum
def top_gainers_drop(
- self,
+ cls,
close_price,
open_price,
ma_7,
- ma_100,
ma_25,
- symbol,
- lowest_price,
- slope,
+ ma_100,
+ ma_7_prev,
+ ma_25_prev,
+ ma_100_prev,
volatility,
):
"""
@@ -21,34 +21,39 @@ def top_gainers_drop(
so create margin_short bot
"""
- if float(close_price) < float(open_price) and symbol in self.top_coins_gainers:
+ if float(close_price) < float(open_price) and cls.symbol in cls.top_coins_gainers:
algo = "top_gainers_drop"
- trend = self.define_strategy(self)
+ trend = cls.define_strategy()
if not trend:
- return
+ trend = TrendEnum.down_trend
+
+ bb_high, bb_mid, bb_low = cls.bb_spreads()
msg = f"""
-- [{os.getenv('ENV')}] Top gainers's drop #{algo} algorithm #{symbol}
-- Current price: {close_price}
-- Log volatility (log SD): {volatility}
-- Slope: {slope}
-- https://www.binance.com/en/trade/{symbol}
-- Dashboard trade
-"""
- value = {
- "msg": msg,
- "symbol": symbol,
- "algo": algo,
- "spread": volatility,
- "current_price": close_price,
- "trend": trend,
- }
-
- self.producer.send(
+ - [{os.getenv('ENV')}] Top gainers's drop #{algo} algorithm #{cls.symbol}
+ - Current price: {close_price}
+ - Log volatility (log SD): {volatility}
+ - Bollinguer bands spread: {(bb_high - bb_low) / bb_high }
+ - Reversal? {"Yes" if cls.market_domination_reversal else "No"}
+ - https://www.binance.com/en/trade/{cls.symbol}
+ - Dashboard trade
+ """
+
+ value = SignalsConsumer(
+ spread=volatility,
+ current_price=close_price,
+ msg=msg,
+ symbol=cls.symbol,
+ algo=algo,
+ trend=trend,
+ bb_spreads=None,
+ )
+
+ cls.producer.send(
KafkaTopics.signals.value, value=value.model_dump_json()
- ).add_callback(self.base_producer.on_send_success).add_errback(
- self.base_producer.on_send_error
+ ).add_callback(cls.base_producer.on_send_success).add_errback(
+ cls.base_producer.on_send_error
)
return
diff --git a/algorithms/whale_alert_signals.py b/algorithms/whale_alert_signals.py
deleted file mode 100644
index 4b628da..0000000
--- a/algorithms/whale_alert_signals.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import logging
-import os
-import time
-
-# from telegram_bot import TelegramBot # For formatted dictionary printing
-# from whalealert.whalealert import WhaleAlert
-
-
-# class WhaleAlertSignals:
-# """
-# Receive Whale alerts from telegram channel
-# """
-# def __init__(self):
-# self.api_key = os.getenv("WHALER_KEY")
-# self.whale_alert = WhaleAlert()
-# self.telegram_bot = TelegramBot()
-# self.transaction_count_limit = 2
-# self.exclude_list = ["GUSD","USDC", "USDT", "BUSD", "BTC", "ETH"]
-
-# def get_last_transaction(self):
-# start_time = int(time.time() - 600)
-# success, transactions, status = self.whale_alert.get_transactions(start_time, api_key=self.api_key, limit=self.transaction_count_limit)
-# if success:
-# if (transactions[0]["amount_usd"] != transactions[1]["amount_usd"]) and transactions[1]["symbol"] not in self.exclude_list:
-# return transactions[1]
-# else:
-# return None
-# else:
-# return status
-
-# def run_bot(self) -> None:
-# """Run the bot."""
-# logging.info("Running Whale alert signals...")
-# transaction = self.get_last_transaction()
-# if transaction:
-# from_owner = "#" + transaction["from"]["owner"] if transaction["from"]["owner"] == "unknown" else transaction["to"]["owner"]
-# to_owner = "#" + transaction["to"]["owner"] if transaction["to"]["owner"] == "unknown" else transaction["to"]["owner"]
-
-# msg = f'[{os.getenv("ENV")}] #Whale alert: {transaction["transaction_type"]} of #{transaction["symbol"]} ({transaction["amount_usd"]} USD) from {from_owner} wallet to {to_owner}\n- https://www.binance.com/en/trade/{transaction["symbol"]}_USDT \n- Dashboard trade http://terminal.binbot.in/bots/new/{transaction["symbol"]}USDT'
-# self.telegram_bot.send_msg(msg)
-
-# pass
diff --git a/consumer.py b/consumer.py
index 9f5ac0a..f5a6415 100644
--- a/consumer.py
+++ b/consumer.py
@@ -1,13 +1,22 @@
-import json
-import os
import asyncio
+import json
import logging
+import os
+
from aiokafka import AIOKafkaConsumer
+from aiokafka.errors import RequestTimedOutError, UnknownMemberIdError
+
from consumers.autotrade_consumer import AutotradeConsumer
-from shared.enums import KafkaTopics
-from consumers.telegram_consumer import TelegramConsumer
from consumers.klines_provider import KlinesProvider
-from aiokafka.errors import UnknownMemberIdError, RequestTimedOutError
+from consumers.telegram_consumer import TelegramConsumer
+from shared.enums import KafkaTopics
+
+logging.basicConfig(
+ level=logging.INFO,
+ filename=None,
+ format="%(asctime)s.%(msecs)03d UTC %(levelname)s %(name)s: %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+)
async def data_process_pipe():
@@ -62,8 +71,8 @@ async def data_analytics_pipe():
at_consumer.load_data_on_start()
if message.topic == KafkaTopics.signals.value:
- at_consumer.process_autotrade_restrictions(message.value)
await telegram_consumer.send_msg(message.value)
+ at_consumer.process_autotrade_restrictions(message.value)
finally:
await consumer.stop()
diff --git a/consumers/autotrade_consumer.py b/consumers/autotrade_consumer.py
index 30b3bfb..f4b68b0 100644
--- a/consumers/autotrade_consumer.py
+++ b/consumers/autotrade_consumer.py
@@ -1,8 +1,9 @@
-import logging
import json
+import logging
+from datetime import datetime
+
from models.signals import SignalsConsumer
from shared.apis import BinbotApi
-from datetime import datetime
from shared.autotrade import Autotrade
@@ -24,7 +25,6 @@ def __init__(self, producer) -> None:
self.active_test_bots: list = []
self.load_data_on_start()
# Because market domination analysis 40 weight from binance endpoints
- self.top_coins_gainers: list = []
self.btc_change_perc = 0
self.volatility = 0
pass
@@ -74,6 +74,13 @@ def reached_max_active_autobots(self, db_collection_name: str) -> bool:
return False
+ def is_margin_available(self, symbol: str) -> bool:
+ """
+ Check if margin trading is allowed for a symbol
+ """
+ info = self._exchange_info(symbol)
+ return bool(info["symbols"][0]["isMarginTradingAllowed"])
+
def process_autotrade_restrictions(self, result: str):
"""
Refactored autotrade conditions.
@@ -125,9 +132,10 @@ def process_autotrade_restrictions(self, result: str):
"Reached maximum number of active bots set in controller settings"
)
else:
- autotrade = Autotrade(
- symbol, self.autotrade_settings, data.algo, "bots"
- )
- autotrade.activate_autotrade(data)
+ if self.is_margin_available(symbol):
+ autotrade = Autotrade(
+ symbol, self.autotrade_settings, data.algo, "bots"
+ )
+ autotrade.activate_autotrade(data)
return
diff --git a/consumers/klines_provider.py b/consumers/klines_provider.py
index 58b9c4a..88ebe0e 100644
--- a/consumers/klines_provider.py
+++ b/consumers/klines_provider.py
@@ -1,11 +1,13 @@
import json
import logging
+
import pandas as pd
from kafka import KafkaConsumer
-from shared.enums import BinanceKlineIntervals
+
+from database import KafkaDB
from models.klines import KlineProduceModel
from producers.technical_indicators import TechnicalIndicators
-from database import KafkaDB
+from shared.enums import BinanceKlineIntervals
# spark = SparkSession.builder.appName("Klines Statistics analyses")\
# .config("compute.ops_on_diff_frames", "true").getOrCreate()
diff --git a/consumers/telegram_consumer.py b/consumers/telegram_consumer.py
index 7748566..768e89b 100644
--- a/consumers/telegram_consumer.py
+++ b/consumers/telegram_consumer.py
@@ -1,7 +1,8 @@
import json
+import os
+
from telegram import Bot
from telegram.constants import ParseMode
-import os
class TelegramConsumer:
diff --git a/database.py b/database.py
index e2b8e68..6aceffd 100644
--- a/database.py
+++ b/database.py
@@ -1,10 +1,12 @@
import os
+from datetime import datetime
+
from dotenv import load_dotenv
from pymongo import DESCENDING, MongoClient
from pymongo.collection import Collection
+
+from models.klines import KlineModel, KlineProduceModel
from shared.enums import BinanceKlineIntervals
-from models.klines import KlineProduceModel, KlineModel
-from datetime import datetime
load_dotenv()
diff --git a/models/bot.py b/models/bot.py
index 36c6ded..af9efbc 100644
--- a/models/bot.py
+++ b/models/bot.py
@@ -1,8 +1,9 @@
from time import time
from typing import Literal
+
+from shared.enums import Status, Strategy
from bson.objectid import ObjectId
from pydantic import BaseModel, Field, field_validator
-from binquant.shared.enums import Status, Strategy
class OrderSchema(BaseModel):
@@ -104,8 +105,7 @@ class SafetyOrderSchema(BaseModel):
class BotSchema(BaseModel):
id: str = ""
pair: str
- balance_size_to_use: float = 0
- balance_to_use: str = "1"
+ fiat: str = "USDC"
base_order_size: str = "15" # Min Binance 0.0001 BNB
candlestick_interval: str = "15m"
close_condition: str = ""
@@ -175,8 +175,7 @@ class Config:
"description": "Most fields are optional. Deal field is generated internally, orders are filled up by Binance",
"example": {
"pair": "BNBUSDT",
- "balance_size_to_use": 0,
- "balance_to_use": 0,
+ "fiat": "USDC",
"base_order_size": 15,
"candlestick_interval": "15m",
"cooldown": 0,
diff --git a/models/klines.py b/models/klines.py
index 54b7ee4..a8285a6 100644
--- a/models/klines.py
+++ b/models/klines.py
@@ -1,6 +1,7 @@
-from pydantic import BaseModel, field_validator
from datetime import datetime
+from pydantic import BaseModel, field_validator
+
# from pyspark.sql.types import StructType, StructField, StringType, LongType, BooleanType, FloatType
diff --git a/models/signals.py b/models/signals.py
index f3efce2..db749d2 100644
--- a/models/signals.py
+++ b/models/signals.py
@@ -1,13 +1,19 @@
-from pydantic import BaseModel, ConfigDict, Field, field_validator
-from enum import Enum
-
-from shared.enums import Status, Strategy
+from time import time
+from bson import ObjectId
+from pydantic import BaseModel, ConfigDict, Field, field_validator
-class TrendEnum(str, Enum):
- up_trend = "uptrend"
- down_trend = "downtrend"
- neutral = None
+from shared.enums import (
+ BinanceKlineIntervals,
+ BinanceOrderModel,
+ BinbotEnums,
+ CloseConditions,
+ DealModel,
+ EnumDefinitions,
+ Status,
+ Strategy,
+ TrendEnum,
+)
class BollinguerSpread(BaseModel):
@@ -52,35 +58,103 @@ def name_must_contain_space(cls, v):
class BotPayload(BaseModel):
+ id: str | None = None
pair: str
- balance_size_to_use: float = 0
- balance_to_use: str = "USDT"
- base_order_size: str = "15" # Min Binance 0.0001 BNB
- close_condition: str = ""
+ fiat: str = "USDC"
+ base_order_size: float | int | str = 15 # Min Binance 0.0001 BNB
+ candlestick_interval: BinanceKlineIntervals = Field(
+ default=BinanceKlineIntervals.fifteen_minutes
+ )
+ close_condition: CloseConditions = Field(default=CloseConditions.dynamic_trailling)
+ # cooldown period in minutes before opening next bot with same pair
+ cooldown: int = 0
+ deal: DealModel = Field(default_factory=DealModel)
dynamic_trailling: bool = False
errors: list[str] = [] # Event logs
- mode: str = "autotrade" # Manual is triggered by the terminal dashboard, autotrade by research app
+ # to deprecate in new db
+ locked_so_funds: float | None = 0 # funds locked by Safety orders
+ mode: str = "manual"
name: str = "Default bot"
- status: Status = Status.inactive
+ orders: list[BinanceOrderModel] = [] # Internal
+ status: Status = Field(default=Status.inactive)
stop_loss: float = 0
margin_short_reversal: bool = False # If stop_loss > 0, allow for reversal
take_profit: float = 0
trailling: bool = True
trailling_deviation: float = 0
trailling_profit: float = 0 # Trailling activation (first take profit hit)
- strategy: Strategy
- cooldown: int = 0 # Cooldown in seconds
- short_buy_price: float = 0 # > 0 base_order does not execute immediately, executes short strategy when this value is hit
+ strategy: Strategy = Field(default=Strategy.long)
+ short_buy_price: float = 0
short_sell_price: float = 0 # autoswitch to short_strategy
+ # Deal and orders are internal, should never be updated by outside data
+ total_commission: float = 0
+ created_at: float = time() * 1000
+ updated_at: float = time() * 1000
+
+ model_config = {
+ "arbitrary_types_allowed": True,
+ "use_enum_values": True,
+ "json_encoders": {ObjectId: str},
+ "json_schema_extra": {
+ "description": "Most fields are optional. Deal field is generated internally, orders are filled up by Exchange",
+ "examples": [
+ {
+ "pair": "BNBUSDT",
+ "fiat": "USDC",
+ "base_order_size": 15,
+ "candlestick_interval": "15m",
+ "cooldown": 0,
+ "errors": [],
+ # Manual is triggered by the terminal dashboard, autotrade by research app,
+ "mode": "manual",
+ "name": "Default bot",
+ "orders": [],
+ "status": "inactive",
+ "stop_loss": 0,
+ "take_profit": 2.3,
+ "trailling": "true",
+ "trailling_deviation": 0.63,
+ "trailling_profit": 2.3,
+ "strategy": "long",
+ "short_buy_price": 0,
+ "short_sell_price": 0,
+ "total_commission": 0,
+ }
+ ],
+ },
+ }
+
+ @field_validator("pair", "candlestick_interval")
+ @classmethod
+ def string_not_empty(cls, v):
+ assert v != "", "Empty pair field."
+ return v
- @field_validator("pair", "base_order_size")
+ @field_validator("candlestick_interval")
@classmethod
def check_names_not_empty(cls, v):
- assert v != "", "Empty pair field."
+ if v not in EnumDefinitions.chart_intervals:
+ raise ValueError(f"{v} must be a valid candlestick interval")
return v
+ @field_validator("base_order_size", "base_order_size", mode="before")
+ @classmethod
+ def countables(cls, v):
+ if isinstance(v, str):
+ return float(v)
+ elif isinstance(v, int):
+ return float(v)
+ elif isinstance(v, float):
+ return v
+ else:
+ raise ValueError(f"{v} must be a number (float, int or string)")
+
@field_validator(
- "stop_loss", "take_profit", "trailling_deviation", "trailling_profit"
+ "stop_loss",
+ "take_profit",
+ "trailling_deviation",
+ "trailling_profit",
+ mode="before",
)
@classmethod
def check_percentage(cls, v):
@@ -89,12 +163,28 @@ def check_percentage(cls, v):
else:
raise ValueError(f"{v} must be a percentage")
+ @field_validator("mode")
+ @classmethod
+ def check_mode(cls, v: str):
+ if v not in BinbotEnums.mode:
+ raise ValueError(f'Status must be one of {", ".join(BinbotEnums.mode)}')
+ return v
+
+ @field_validator("strategy")
+ @classmethod
+ def check_strategy(cls, v: str):
+ if v not in BinbotEnums.strategy:
+ raise ValueError(f'Status must be one of {", ".join(BinbotEnums.strategy)}')
+ return v
+
@field_validator("trailling")
@classmethod
- def check_trailling(cls, v: str | bool):
+ def string_booleans(cls, v: str | bool):
if isinstance(v, str) and v.lower() == "false":
return False
- return True
+ if isinstance(v, str) and v.lower() == "true":
+ return True
+ return v
@field_validator("errors")
@classmethod
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..e0f6fc1
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,14 @@
+[mypy]
+implicit_optional = True
+ignore_missing_imports = True
+plugins = pydantic.mypy
+show_error_codes = True
+follow_imports = silent
+warn_redundant_casts = True
+warn_unused_ignores = True
+check_untyped_defs = True
+implicit_reexport = True
+explicit_package_bases = True
+disallow_untyped_calls = True
+disallow_untyped_defs = True
+allow-redefinition = True
diff --git a/producer.py b/producer.py
index 3c89713..5375dc3 100644
--- a/producer.py
+++ b/producer.py
@@ -1,5 +1,5 @@
-import logging
import asyncio
+import logging
from producers.base import BaseProducer
from producers.klines_connector import KlinesConnector
diff --git a/producers/base.py b/producers/base.py
index 9e0fa18..db61031 100644
--- a/producers/base.py
+++ b/producers/base.py
@@ -1,8 +1,9 @@
-import os
import json
import logging
+import os
from kafka import KafkaProducer
+
from database import KafkaDB
@@ -23,4 +24,4 @@ def on_send_success(self, record_metadata):
logging.info(f"Produced: {record_metadata.topic}, {record_metadata.offset}")
def on_send_error(self, excp):
- print(f"Message production failed to send: {excp}")
+ logging.error(f"Message production failed to send: {excp}")
diff --git a/producers/klines_connector.py b/producers/klines_connector.py
index c8e264c..2616a59 100644
--- a/producers/klines_connector.py
+++ b/producers/klines_connector.py
@@ -2,10 +2,11 @@
import logging
from kafka import KafkaProducer
-from shared.apis import BinbotApi
+
from producers.produce_klines import KlinesProducer
-from shared.streaming.socket_client import SpotWebsocketStreamClient
+from shared.apis import BinbotApi
from shared.exceptions import WebSocketError
+from shared.streaming.socket_client import SpotWebsocketStreamClient
class KlinesConnector(BinbotApi):
diff --git a/producers/produce_klines.py b/producers/produce_klines.py
index d87878f..8890529 100644
--- a/producers/produce_klines.py
+++ b/producers/produce_klines.py
@@ -1,6 +1,7 @@
from kafka import KafkaProducer
-from models.klines import KlineProduceModel
+
from database import KafkaDB
+from models.klines import KlineProduceModel
from shared.enums import KafkaTopics
diff --git a/producers/technical_indicators.py b/producers/technical_indicators.py
index 6eddddb..ea3c5c6 100644
--- a/producers/technical_indicators.py
+++ b/producers/technical_indicators.py
@@ -1,15 +1,20 @@
-from datetime import datetime, timedelta
import logging
-import pandas
+from datetime import datetime, timedelta
from typing import Literal
+import pandas
+
+from algorithms.coinrule import buy_low_sell_high, fast_and_slow_macd
+from algorithms.ma_candlestick import ma_candlestick_drop, ma_candlestick_jump
+
# from algorithms.timeseries_gpt import detect_anomalies
+from algorithms.rally import rally_or_pullback
+from algorithms.top_gainer_drop import top_gainers_drop
from models.signals import SignalsConsumer, TrendEnum
-from shared.enums import KafkaTopics
-from shared.apis import BinbotApi
from producers.base import BaseProducer
-from algorithms.ma_candlestick import ma_candlestick_jump, ma_candlestick_drop
-from algorithms.coinrule import buy_low_sell_high, fast_and_slow_macd
+from shared.apis import BinbotApi
+from shared.enums import KafkaTopics
+from shared.utils import round_numbers
class TechnicalIndicators(BinbotApi):
@@ -24,6 +29,7 @@ def __init__(self, df, symbol) -> None:
)
self.market_domination_reversal: bool | None = None
self.active_pairs = self.get_active_pairs()["data"]
+ self.top_coins_gainers: list[str] = []
pass
def update_active_bots_bb_spreads(self, close_price, symbol):
@@ -80,7 +86,11 @@ def bb_spreads(self) -> tuple[float, float, float]:
bb_mid = float(self.df.bb_mid[len(self.df.bb_mid) - 1])
bb_low = float(self.df.bb_lower[len(self.df.bb_lower) - 1])
- return bb_high, bb_mid, bb_low
+ return (
+ round_numbers(bb_high, 6),
+ round_numbers(bb_mid, 6),
+ round_numbers(bb_low, 6),
+ )
def define_strategy(self):
"""
@@ -102,15 +112,6 @@ def define_strategy(self):
):
trend = None
- # if self.market_domination_trend == "gainers":
- # trend = TrendEnum.up_trend.value
-
- # elif self.market_domination_trend == "losers":
- # trend = TrendEnum.down_trend.value
-
- # else:
- # trend = None
-
return trend
def calculate_slope(candlesticks):
@@ -194,8 +195,8 @@ def ma_spreads(self):
- bottom_band: diff between ma_7 and ma_25
"""
- band_1 = (abs((self.df["ma_100"] - self.df["ma_25"])) / self.df["ma_100"]) * 100
- band_2 = (abs((self.df["ma_25"] - self.df["ma_7"])) / self.df["ma_25"]) * 100
+ band_1 = (abs(self.df["ma_100"] - self.df["ma_25"]) / self.df["ma_100"]) * 100
+ band_2 = (abs(self.df["ma_25"] - self.df["ma_7"]) / self.df["ma_25"]) * 100
self.df["big_ma_spread"] = band_1
self.df["small_ma_spread"] = band_2
@@ -250,6 +251,8 @@ def market_domination(self) -> Literal["gainers", "losers", "neutral", None]:
f"Performing market domination analyses. Current trend: {self.market_domination_trend}"
)
data = self.get_market_domination_series()
+ top_gainers_day = self.get_top_gainers()["data"]
+ self.top_coins_gainers = [item["symbol"] for item in top_gainers_day]
# reverse to make latest series more important
data["gainers_count"].reverse()
data["losers_count"].reverse()
@@ -391,32 +394,31 @@ def publish(self):
)
# This function calls a lot ticker24 revise it before uncommenting
- # rally_or_pullback(
- # self,
- # close_price,
- # open_price,
- # self.symbol,
- # ma_7,
- # ma_25,
- # ma_100,
- # ma_7_prev,
- # ma_25_prev,
- # ma_100_prev,
- # volatility
- # )
+ rally_or_pullback(
+ self,
+ close_price,
+ open_price,
+ self.symbol,
+ ma_7,
+ ma_25,
+ ma_100,
+ ma_7_prev,
+ ma_25_prev,
+ ma_100_prev,
+ volatility,
+ )
- # top_gainers_drop(
- # self,
- # close_price,
- # open_price,
- # self.symbol,
- # ma_7,
- # ma_25,
- # ma_100,
- # ma_7_prev,
- # ma_25_prev,
- # ma_100_prev,
- # volatility
- # )
+ top_gainers_drop(
+ self,
+ close_price,
+ open_price,
+ ma_7,
+ ma_25,
+ ma_100,
+ ma_7_prev,
+ ma_25_prev,
+ ma_100_prev,
+ volatility,
+ )
return
diff --git a/shared/apis.py b/shared/apis.py
index 66b5049..fcc7c12 100644
--- a/shared/apis.py
+++ b/shared/apis.py
@@ -4,8 +4,10 @@
from decimal import Decimal
from random import randrange
from urllib.parse import urlencode
+
from dotenv import load_dotenv
from requests import Session, get
+
from shared.utils import handle_binance_errors
load_dotenv()
@@ -120,7 +122,7 @@ def launchpool_projects(self):
data = self.request(url=self.launchpool_url, headers={"User-Agent": "Mozilla"})
return data
- def price_precision(self, symbol):
+ def price_precision(self, symbol) -> int:
"""
Modified from price_filter_by_symbol
from /api/account/account.py
@@ -130,16 +132,16 @@ def price_precision(self, symbol):
symbols = self._exchange_info(symbol)
market = symbols["symbols"][0]
price_filter = next(
- (m for m in market["filters"] if m["filterType"] == "PRICE_FILTER")
+ m for m in market["filters"] if m["filterType"] == "PRICE_FILTER"
)
- # Once got the filter data of Binance
- # Transform into string and remove leading zeros
- # This is how the exchange accepts the prices, it will not work with scientific exponential notation e.g. 2.1-10
- price_precision = Decimal(str(price_filter["tickSize"].rstrip(".0")))
+ # Convert scientific notation to decimal and remove leading zeros
+ tick_size = float(price_filter["tickSize"])
+ tick_size_str = f"{tick_size:.8f}".rstrip("0").rstrip(".")
+ price_precision = Decimal(tick_size_str).as_tuple()
- # Finally return the correct number of decimals required
- return -(price_precision).as_tuple().exponent
+ # Finally return the correct number of decimals required as positive number
+ return abs(int(price_precision.exponent))
def min_amount_check(self, symbol, qty):
"""
@@ -152,7 +154,7 @@ def min_amount_check(self, symbol, qty):
symbols = self._exchange_info(symbol)
market = symbols["symbols"][0]
min_notional_filter = next(
- (m for m in market["filters"] if m["filterType"] == "NOTIONAL")
+ m for m in market["filters"] if m["filterType"] == "NOTIONAL"
)
min_qty = float(qty) > float(min_notional_filter["minNotional"])
return min_qty
@@ -183,6 +185,7 @@ class BinbotApi(BinanceApi):
bb_activate_bot_url = f"{bb_base_url}/bot/activate"
bb_gainers_losers = f"{bb_base_url}/account/gainers-losers"
bb_market_domination = f"{bb_base_url}/charts/market-domination"
+ bb_top_gainers = f"{bb_base_url}/charts/top-gainers"
# Trade operations
bb_buy_order_url = f"{bb_base_url}/order/buy"
@@ -353,3 +356,10 @@ def get_active_pairs(self):
def margin_trading_check(self, symbol):
data = self.request(url=f"{self.bb_margin_trading_check_url}/{symbol}")
return data
+
+ def get_top_gainers(self):
+ """
+ Top crypto/token/coin gainers of the day
+ """
+ data = self.request(url=self.bb_top_gainers)
+ return data
diff --git a/shared/autotrade.py b/shared/autotrade.py
index a0c9dff..875d283 100644
--- a/shared/autotrade.py
+++ b/shared/autotrade.py
@@ -1,13 +1,13 @@
import json
-import math
import logging
-
+import math
from datetime import datetime
+
+from models.signals import BotPayload, TrendEnum
from producers.base import BaseProducer
+from shared.apis import BinbotApi
from shared.enums import CloseConditions, KafkaTopics, Strategy
-from models.signals import BotPayload, TrendEnum
from shared.exceptions import AutotradeError
-from shared.apis import BinbotApi
from shared.utils import round_numbers, supress_notation
@@ -35,14 +35,13 @@ def __init__(
self.default_bot = BotPayload(
pair=pair,
name=f"{algorithm_name}_{current_date}",
- balance_size_to_use=str(settings["balance_size_to_use"]),
- balance_to_use=settings["balance_to_use"],
+ fiat=settings["balance_to_use"],
base_order_size=settings["base_order_size"],
+ strategy=Strategy.long,
stop_loss=settings["stop_loss"],
take_profit=settings["take_profit"],
trailling=settings["trailling"],
trailling_deviation=settings["trailling_deviation"],
- strategy=settings["strategy"],
close_condition=CloseConditions.dynamic_trailling,
)
self.db_collection_name = db_collection_name
@@ -150,13 +149,13 @@ def handle_price_drops(
(
b["free"]
for b in balances["data"]
- if b["asset"] == self.default_bot.balance_to_use
+ if b["asset"] == self.default_bot.fiat
),
None,
)
if not available_balance:
- print(f"Not enough {self.default_bot.balance_to_use} for safety orders")
+ logging.info(f"Not enough {self.default_bot.fiat} for safety orders")
return
if trend == "downtrend":
@@ -181,20 +180,11 @@ def set_paper_trading_values(self, balances, qty):
if self.pair.endswith(b["asset"]):
qty = supress_notation(b["free"], self.decimals)
if self.min_amount_check(self.pair, qty):
- # balance_size_to_use = 0.0 means "Use all balance". float(0) = 0.0
- if float(self.default_bot.balance_size_to_use) != 0.0:
- if b["free"] < float(self.default_bot.balance_size_to_use):
- # Display warning and continue with full balance
- print(
- f"Error: balance ({qty}) is less than balance_size_to_use ({float(self.default_bot['balance_size_to_use'])}). Autotrade will use all balance"
- )
- else:
- qty = float(self.default_bot.balance_size_to_use)
-
self.default_bot.base_order_size = qty
break
- rate = rate["price"]
+ ticker = self.get_24_ticker(symbol=self.pair)
+ rate = ticker["price"]
qty = supress_notation(b["free"], self.decimals)
# Round down to 6 numbers to avoid not enough funds
base_order_size = (
@@ -298,7 +288,7 @@ def activate_autotrade(self, data, **kwargs):
if "error" in bot and bot["error"] > 0:
# Failed to activate bot so:
- # (1) Add to blacklist/exclude from future autotrades
+ # (1) Add to blacklist/exclude from future autotrades
# (2) Submit error to event logs
# (3) Delete inactive bot
# this prevents cluttering UI with loads of useless bots
diff --git a/shared/enums.py b/shared/enums.py
index 1590e39..651d81b 100644
--- a/shared/enums.py
+++ b/shared/enums.py
@@ -1,4 +1,7 @@
from enum import Enum
+from time import time
+
+from pydantic import BaseModel, field_validator
class EnumDefinitions:
@@ -73,36 +76,30 @@ class OrderType(str, Enum):
take_profit_limit = "TAKE_PROFIT_LIMIT"
limit_maker = "LIMIT_MAKER"
- def __str__(self):
- return str(self.str)
-
class TimeInForce(str, Enum):
gtc = "GTC"
ioc = "IOC"
fok = "FOK"
- def __str__(self):
- return str(self.str)
-
class OrderSide(str, Enum):
buy = "BUY"
sell = "SELL"
- def __str__(self):
- return str(self.str)
+
+class TrendEnum(str, Enum):
+ up_trend = "uptrend"
+ down_trend = "downtrend"
+ neutral = None
class CloseConditions(str, Enum):
dynamic_trailling = "dynamic_trailling"
- timestamp = "timestamp" # No trailling, standard stop loss
- market_reversal = (
- "market_reversal" # binbot-research param (self.market_trend_reversal)
- )
-
- def __str__(self):
- return str(self.str)
+ # No trailling, standard stop loss
+ timestamp = "timestamp"
+ # binbot-research param (self.market_trend_reversal)
+ market_reversal = "market_reversal"
class KafkaTopics(str, Enum):
@@ -112,9 +109,6 @@ class KafkaTopics(str, Enum):
signals = "signals"
restart_streaming = "restart_streaming"
- def __str__(self):
- return str(self.str)
-
class BinanceKlineIntervals(str, Enum):
one_minute = "1m"
@@ -133,9 +127,6 @@ class BinanceKlineIntervals(str, Enum):
one_week = "1w"
one_month = "1M"
- def __str__(self):
- return str(self.value)
-
def bin_size(self):
return int(self.value[:-1])
@@ -150,3 +141,112 @@ def unit(self):
return "week"
elif self.value[-1:] == "M":
return "month"
+
+
+class DealType(str, Enum):
+ base_order = "base_order"
+ take_profit = "take_profit"
+ stop_loss = "stop_loss"
+ short_sell = "short_sell"
+ short_buy = "short_buy"
+ margin_short = "margin_short"
+ panic_close = "panic_close"
+
+
+class BinanceOrderModel(BaseModel):
+ """
+ Data model given by Binance,
+ therefore it should be strings
+ """
+
+ order_type: str
+ time_in_force: str
+ timestamp: int
+ order_id: int
+ order_side: str
+ pair: str
+ qty: float
+ status: str
+ price: float
+ deal_type: DealType
+
+ @field_validator("timestamp", "order_id", "price", "qty", "order_id")
+ @classmethod
+ def validate_str_numbers(cls, v):
+ if isinstance(v, float):
+ return v
+ elif isinstance(v, int):
+ return v
+ elif isinstance(v, str):
+ return float(v)
+ else:
+ raise ValueError(f"{v} must be a number")
+
+
+class DealModel(BaseModel):
+ """
+ Data model that is used for operations,
+ so it should all be numbers (int or float)
+ """
+
+ buy_price: float = 0
+ buy_total_qty: float = 0
+ buy_timestamp: float = time() * 1000
+ current_price: float = 0
+ sd: float = 0
+ avg_buy_price: float = 0
+ take_profit_price: float = 0
+ sell_timestamp: float = 0
+ sell_price: float = 0
+ sell_qty: float = 0
+ trailling_stop_loss_price: float = 0
+ # take_profit but for trailling, to avoid confusion, trailling_profit_price always be > trailling_stop_loss_price
+ trailling_profit_price: float = 0
+ stop_loss_price: float = 0
+ trailling_profit: float = 0
+ so_prices: float = 0
+ post_closure_current_price: float = 0
+ original_buy_price: float = 0 # historical buy_price after so trigger
+ short_sell_price: float = 0
+ short_sell_qty: float = 0
+ short_sell_timestamp: float = time() * 1000
+
+ # fields for margin trading
+ margin_short_loan_principal: float = 0
+ margin_loan_id: float = 0
+ hourly_interest_rate: float = 0
+ margin_short_sell_price: float = 0
+ margin_short_loan_interest: float = 0
+ margin_short_buy_back_price: float = 0
+ margin_short_sell_qty: float = 0
+ margin_short_buy_back_timestamp: int = 0
+ margin_short_base_order: float = 0
+ margin_short_sell_timestamp: int = 0
+ margin_short_loan_timestamp: int = 0
+
+ @field_validator(
+ "buy_price",
+ "current_price",
+ "avg_buy_price",
+ "original_buy_price",
+ "take_profit_price",
+ "sell_price",
+ "short_sell_price",
+ "trailling_stop_loss_price",
+ "trailling_profit_price",
+ "stop_loss_price",
+ "trailling_profit",
+ "margin_short_loan_principal",
+ "margin_short_sell_price",
+ "margin_short_loan_interest",
+ "margin_short_buy_back_price",
+ "margin_short_base_order",
+ "margin_short_sell_qty",
+ )
+ @classmethod
+ def check_prices(cls, v):
+ if float(v) < 0:
+ raise ValueError("Price must be a positive number")
+ elif isinstance(v, str):
+ return float(v)
+ return v
diff --git a/shared/streaming/socket_manager.py b/shared/streaming/socket_manager.py
index 4a011c8..355933e 100644
--- a/shared/streaming/socket_manager.py
+++ b/shared/streaming/socket_manager.py
@@ -1,12 +1,13 @@
import json
import logging
-import time
import threading
+import time
+
from websocket import (
ABNF,
- create_connection,
- WebSocketException,
WebSocketConnectionClosedException,
+ WebSocketException,
+ create_connection,
)
@@ -64,10 +65,10 @@ def read_data(self):
if isinstance(e, WebSocketConnectionClosedException):
self.logger.error("Lost websocket connection")
else:
- self.logger.error("Websocket exception: {}".format(e))
+ self.logger.error(f"Websocket exception: {e}")
raise e
except Exception as e:
- self.logger.error("Exception in read_data: {}".format(e))
+ self.logger.error(f"Exception in read_data: {e}")
raise e
if op_code == ABNF.OPCODE_CLOSE:
@@ -101,7 +102,7 @@ def _callback(self, callback, *args):
try:
callback(self, *args)
except Exception as e:
- self.logger.error("Error from callback {}: {}".format(callback, e))
+ self.logger.error(f"Error from callback {callback}: {e}")
if self.on_error:
self.on_error(self, e)
diff --git a/shared/utils.py b/shared/utils.py
index 9f70dd3..9773d63 100644
--- a/shared/utils.py
+++ b/shared/utils.py
@@ -1,9 +1,11 @@
import math
import re
+from datetime import datetime
from decimal import Decimal
from time import sleep
+
from requests import Response
-from datetime import datetime
+
from shared.exceptions import InvalidSymbol
@@ -28,7 +30,7 @@ def supress_trailling(value: str | float | int) -> float:
value = float(value)
# supress scientific notation
number = float(f"{value:f}")
- number = float("{0:g}".format(number))
+ number = float(f"{number:g}")
return number
@@ -75,7 +77,7 @@ def supress_notation(num: float, precision: int = 0) -> str:
if precision >= 0:
decimal_points = precision
else:
- decimal_points = Decimal(num).as_tuple().exponent * -1
+ decimal_points = int(Decimal(num).as_tuple().exponent * -1)
return f"{num:.{str(decimal_points)}f}"
@@ -88,11 +90,6 @@ def handle_binance_errors(response: Response):
"""
response.raise_for_status()
-
- if 400 <= response.status_code < 500:
- if response.status_code == 418:
- sleep(120)
-
# Calculate request weights and pause half of the way (1200/2=600)
if (
"x-mbx-used-weight-1m" in response.headers
@@ -101,6 +98,9 @@ def handle_binance_errors(response: Response):
print("Request weight limit prevention pause, waiting 1 min")
sleep(120)
+ if response.status_code == 418:
+ sleep(120)
+
content = response.json()
if "code" in content:
diff --git a/tests/__init__.py b/tests/__init__.py
index d732eff..f2b6227 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,4 +1,4 @@
-from producers.klines_connector import KlinesConnector
from producers.base import BaseProducer
+from producers.klines_connector import KlinesConnector
__all__ = ["KlinesConnector", "BaseProducer"]
diff --git a/tests/test_producer.py b/tests/test_producer.py
index 58314e7..29e02de 100644
--- a/tests/test_producer.py
+++ b/tests/test_producer.py
@@ -1,5 +1,6 @@
-from kafka import KafkaProducer
import pytest
+from kafka import KafkaProducer
+
from producers.base import BaseProducer
from producers.klines_connector import KlinesConnector
@@ -80,5 +81,5 @@ def test_producer_error(klines_connector):
connector.start_stream()
connector.process_kline_stream(res)
assert False
- except KeyError as e:
+ except KeyError:
assert True