Merge branch 'develop' into api-server-list-custom-data

This commit is contained in:
Axel-CH
2025-03-18 00:53:10 -04:00
16 changed files with 1192 additions and 203 deletions

View File

@@ -19,7 +19,7 @@ repos:
- types-requests==2.32.0.20250306
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20241206
- SQLAlchemy==2.0.38
- SQLAlchemy==2.0.39
# stages: [push]
- repo: https://github.com/pycqa/isort

View File

@@ -1,6 +1,6 @@
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/freqtrade/freqtrade/actions/)
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)

View File

@@ -613,6 +613,14 @@
"description": "Telegram topic ID - only applicable for group chats",
"type": "string"
},
"authorized_users": {
"description": "Authorized users for the bot.",
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"allow_custom_messages": {
"description": "Allow sending custom messages from the Strategy.",
"type": "boolean",
@@ -1417,8 +1425,7 @@
},
"fit_live_predictions_candles": {
"description": "Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset.",
"type": "boolean",
"default": false
"type": "integer"
},
"data_kitchen_thread_count": {
"description": "Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.).",

View File

@@ -1,6 +1,6 @@
![freqtrade](assets/freqtrade_poweredby.svg)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/freqtrade/freqtrade/actions/)
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)

View File

@@ -1,6 +1,6 @@
markdown==3.7
mkdocs==1.6.1
mkdocs-material==9.6.7
mkdocs-material==9.6.8
mdx_truly_sane_lists==1.3
pymdown-extensions==10.14.3
jinja2==3.1.6

View File

@@ -81,6 +81,19 @@ Without this, the bot will always respond to the general channel in the group if
Similar to the group-id - you can use `/tg_info` from the topic/thread to get the correct topic-id.
#### Authorized users
For groups, it can be useful to limit who can send commands to the bot.
If `"authorized_users": []` is present and empty, no user will be allowed to control the bot.
In the below example, only the user with the id "1234567" is allowed to control the bot - all other users will only be able to receive messages.
```json
"chat_id": "-1001332619709",
"topic_id": "3",
"authorized_users": ["1234567"]
```
## Control telegram noise
Freqtrade provides means to control the verbosity of your telegram bot.

View File

@@ -471,6 +471,12 @@ CONF_SCHEMA = {
"description": "Telegram topic ID - only applicable for group chats",
"type": "string",
},
"authorized_users": {
"description": "Authorized users for the bot.",
"type": "array",
"items": {"type": "string"},
"uniqueItems": True,
},
"allow_custom_messages": {
"description": "Allow sending custom messages from the Strategy.",
"type": "boolean",
@@ -1026,8 +1032,7 @@ CONF_SCHEMA = {
"Number of historical candles to use for computing target (label) "
"statistics from prediction data, instead of from the training dataset."
),
"type": "boolean",
"default": False,
"type": "integer",
},
"data_kitchen_thread_count": {
"description": (

File diff suppressed because it is too large Load Diff

View File

@@ -2441,8 +2441,8 @@ class Exchange:
return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_ts)
logger.info(
f"Failed to reuse watch {pair}, {timeframe}, {candle_ts < last_refresh_time},"
f" {candle_ts}, {last_refresh_time}, "
f"Couldn't reuse watch for {pair}, {timeframe}, falling back to REST api. "
f"{candle_ts < last_refresh_time}, {candle_ts}, {last_refresh_time}, "
f"{format_ms_time(candle_ts)}, {format_ms_time(last_refresh_time)} "
)

View File

@@ -213,7 +213,7 @@ def _get_resample_from_period(period: str) -> str:
if period == "month":
return "1ME"
if period == "year":
return "1Y"
return "1YE"
raise ValueError(f"Period {period} is not supported.")

View File

@@ -25,6 +25,7 @@ from telegram import (
InlineKeyboardButton,
InlineKeyboardMarkup,
KeyboardButton,
Message,
ReplyKeyboardMarkup,
Update,
)
@@ -96,17 +97,17 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
"""
@wraps(command_handler)
async def wrapper(self, *args, **kwargs):
async def wrapper(self, *args, **kwargs) -> None:
"""Decorator logic"""
update = kwargs.get("update") or args[0]
# Reject unauthorized messages
if update.callback_query:
cchat_id = int(update.callback_query.message.chat.id)
ctopic_id = update.callback_query.message.message_thread_id
else:
cchat_id = int(update.message.chat_id)
ctopic_id = update.message.message_thread_id
message: Message = (
update.message if update.callback_query is None else update.callback_query.message
)
cchat_id: int = int(message.chat_id)
ctopic_id: int | None = message.message_thread_id
from_user_id: str = str(update.effective_user.id if update.effective_user else "")
chat_id = int(self._config["telegram"]["chat_id"])
if cchat_id != chat_id:
@@ -118,6 +119,10 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
logger.debug(f"Rejected message from wrong channel: {cchat_id}, {ctopic_id}")
return None
authorized = self._config["telegram"].get("authorized_users", None)
if authorized is not None and from_user_id not in authorized:
logger.info(f"Unauthorized user tried to control the bot: {from_user_id}")
return None
# Rollback session to avoid getting data stored in a transaction.
Trade.rollback()
logger.debug("Executing handler: %s for chat_id: %s", command_handler.__name__, chat_id)
@@ -2155,6 +2160,9 @@ class Telegram(RPCHandler):
return
chat_id = update.message.chat_id
topic_id = update.message.message_thread_id
user_id = (
update.effective_user.id if topic_id is not None and update.effective_user else None
)
msg = f"""Freqtrade Bot Info:
```json
@@ -2162,7 +2170,8 @@ class Telegram(RPCHandler):
"enabled": true,
"token": "********",
"chat_id": "{chat_id}",
{f'"topic_id": "{topic_id}"' if topic_id else ""}
{f'"topic_id": "{topic_id}",' if topic_id else ""}
{f'//"authorized_users": ["{user_id}"]' if topic_id and user_id else ""}
}}
```
"""

View File

@@ -29,7 +29,7 @@ classifiers = [
dependencies = [
# from requirements.txt
"ccxt>=4.3.24",
"ccxt>=4.4.60",
"SQLAlchemy>=2.0.6",
"python-telegram-bot>=20.1",
"humanize>=4.0.0",

View File

@@ -7,7 +7,7 @@
-r docs/requirements-docs.txt
coveralls==4.0.1
ruff==0.9.10
ruff==0.11.0
mypy==1.15.0
pre-commit==4.1.0
pytest==8.3.5

View File

@@ -5,4 +5,4 @@
scipy==1.15.2
scikit-learn==1.6.1
ft-scikit-optimize==0.9.2
filelock==3.17.0
filelock==3.18.0

View File

@@ -4,11 +4,11 @@ bottleneck==1.4.2
numexpr==2.10.2
pandas-ta==0.3.14b
ccxt==4.4.65
ccxt==4.4.68
cryptography==44.0.2
aiohttp==3.9.5
SQLAlchemy==2.0.38
python-telegram-bot==21.11.1
SQLAlchemy==2.0.39
python-telegram-bot==22.0
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.12.1

View File

@@ -6,7 +6,7 @@ import asyncio
import logging
import re
import threading
from datetime import datetime, timedelta, timezone
from datetime import timedelta
from functools import reduce
from random import choice, randint
from string import ascii_uppercase
@@ -16,7 +16,7 @@ import pytest
import time_machine
from pandas import DataFrame
from sqlalchemy import select
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
from telegram import Chat, Message, ReplyKeyboardMarkup, Update, User
from telegram.error import BadRequest, NetworkError, TelegramError
from freqtrade import __version__
@@ -67,7 +67,12 @@ def default_conf(default_conf) -> dict:
@pytest.fixture
def update():
message = Message(0, datetime.now(timezone.utc), Chat(1235, 0))
message = Message(
0,
dt_now(),
Chat(1235, 0),
from_user=User(5432, "test", is_bot=False),
)
_update = Update(0, message=message)
return _update
@@ -232,8 +237,12 @@ async def test_authorized_only(default_conf, mocker, caplog, update) -> None:
async def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
patch_exchange(mocker)
caplog.set_level(logging.DEBUG)
chat = Chat(0xDEADBEEF, 0)
message = Message(randint(1, 100), datetime.now(timezone.utc), chat)
message = Message(
randint(1, 100),
dt_now(),
Chat(0xDEADBEEF, 0),
from_user=User(5432, "test", is_bot=False),
)
update = Update(randint(1, 100), message=message)
default_conf["telegram"]["enabled"] = False
@@ -249,6 +258,42 @@ async def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> Non
assert not log_has("Exception occurred within Telegram module", caplog)
async def test_authorized_users(default_conf, mocker, caplog, update) -> None:
patch_exchange(mocker)
caplog.set_level(logging.DEBUG)
default_conf["telegram"]["enabled"] = False
default_conf["telegram"]["authorized_users"] = ["5432"]
bot = FreqtradeBot(default_conf)
rpc = RPC(bot)
dummy = DummyCls(rpc, default_conf)
await dummy.dummy_handler(update=update, context=MagicMock())
assert dummy.state["called"] is True
assert log_has("Executing handler: dummy_handler for chat_id: 1235", caplog)
caplog.clear()
# Test empty case
default_conf["telegram"]["authorized_users"] = []
dummy1 = DummyCls(rpc, default_conf)
await dummy1.dummy_handler(update=update, context=MagicMock())
assert dummy1.state["called"] is False
assert log_has_re(r"Unauthorized user tried to .*5432", caplog)
caplog.clear()
# Test wrong user
default_conf["telegram"]["authorized_users"] = ["1234"]
dummy1 = DummyCls(rpc, default_conf)
await dummy1.dummy_handler(update=update, context=MagicMock())
assert dummy1.state["called"] is False
assert log_has_re(r"Unauthorized user tried to .*5432", caplog)
caplog.clear()
# Test reverse case again
default_conf["telegram"]["authorized_users"] = ["5432"]
dummy1 = DummyCls(rpc, default_conf)
await dummy1.dummy_handler(update=update, context=MagicMock())
assert dummy1.state["called"] is True
assert not log_has_re(r"Unauthorized user tried to .*5432", caplog)
async def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None:
patch_exchange(mocker)
@@ -638,7 +683,7 @@ async def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time
assert msg_mock.call_count == 1
assert "Daily Profit over the last 2 days</b>:" in msg_mock.call_args_list[0][0][0]
assert "Day " in msg_mock.call_args_list[0][0][0]
assert str(datetime.now(timezone.utc).date()) in msg_mock.call_args_list[0][0][0]
assert str(dt_now().date()) in msg_mock.call_args_list[0][0][0]
assert " 6.83 USDT" in msg_mock.call_args_list[0][0][0]
assert " 7.51 USD" in msg_mock.call_args_list[0][0][0]
assert "(2)" in msg_mock.call_args_list[0][0][0]
@@ -651,11 +696,8 @@ async def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time
await telegram._daily(update=update, context=context)
assert msg_mock.call_count == 1
assert "Daily Profit over the last 7 days</b>:" in msg_mock.call_args_list[0][0][0]
assert str(datetime.now(timezone.utc).date()) in msg_mock.call_args_list[0][0][0]
assert (
str((datetime.now(timezone.utc) - timedelta(days=5)).date())
in msg_mock.call_args_list[0][0][0]
)
assert str(dt_now().date()) in msg_mock.call_args_list[0][0][0]
assert str((dt_now() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0]
assert " 6.83 USDT" in msg_mock.call_args_list[0][0][0]
assert " 7.51 USD" in msg_mock.call_args_list[0][0][0]
assert "(2)" in msg_mock.call_args_list[0][0][0]
@@ -725,7 +767,7 @@ async def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, tim
in msg_mock.call_args_list[0][0][0]
)
assert "Monday " in msg_mock.call_args_list[0][0][0]
today = datetime.now(timezone.utc).date()
today = dt_now().date()
first_iso_day_of_current_week = today - timedelta(days=today.weekday())
assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0]
assert " 2.74 USDT" in msg_mock.call_args_list[0][0][0]
@@ -793,7 +835,7 @@ async def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, ti
assert msg_mock.call_count == 1
assert "Monthly Profit over the last 2 months</b>:" in msg_mock.call_args_list[0][0][0]
assert "Month " in msg_mock.call_args_list[0][0][0]
today = datetime.now(timezone.utc).date()
today = dt_now().date()
current_month = f"{today.year}-{today.month:02} "
assert current_month in msg_mock.call_args_list[0][0][0]
assert " 2.74 USDT" in msg_mock.call_args_list[0][0][0]
@@ -898,7 +940,7 @@ async def test_telegram_profit_handle(
trade.orders.append(oobj)
trade.update_trade(oobj)
trade.close_date = datetime.now(timezone.utc)
trade.close_date = dt_now()
trade.is_open = False
Trade.commit()