diff --git a/.requirements/requirements.txt b/.requirements/requirements.txt index eafd27403..963025f4b 100644 --- a/.requirements/requirements.txt +++ b/.requirements/requirements.txt @@ -13,7 +13,7 @@ asyncz==0.4.0 ; python_version >= "3.10" and python_version < "4.0" attrs==23.1.0 ; python_version >= "3.10" and python_version < "4.0" beautifulsoup4==4.12.2 ; python_version >= "3.10" and python_version < "4.0" bitarray==2.8.0 ; python_version >= "3.10" and python_version < "4" -ccxt==4.0.36 ; python_version >= "3.10" and python_version < "4.0" +ccxt==4.0.39 ; python_version >= "3.10" and python_version < "4.0" certifi==2023.7.22 ; python_version >= "3.10" and python_version < "4.0" cffi==1.15.1 ; python_version >= "3.10" and python_version < "4.0" charset-normalizer==3.2.0 ; python_version >= "3.10" and python_version < "4.0" @@ -21,9 +21,9 @@ click==8.1.6 ; python_version >= "3.10" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows") cryptography==41.0.2 ; python_version >= "3.10" and python_version < "4.0" cytoolz==0.12.2 ; python_version >= "3.10" and python_version < "4" and implementation_name == "cpython" -dxsp==4.1.2 ; python_version >= "3.10" and python_version < "4.0" +dxsp==4.2.1 ; python_version >= "3.10" and python_version < "4.0" dynaconf==3.2.0 ; python_version >= "3.10" and python_version < "4.0" -emoji==2.6.0 ; python_version >= "3.10" and python_version < "4.0" +emoji==2.7.0 ; python_version >= "3.10" and python_version < "4.0" eth-abi==4.1.0 ; python_version >= "3.10" and python_version < "4" eth-account==0.9.0 ; python_version >= "3.10" and python_version < "4" eth-hash==0.5.2 ; python_version >= "3.10" and python_version < "4" @@ -35,7 +35,7 @@ eth-typing==3.4.0 ; python_version >= "3.10" and python_version < "4" eth-utils==2.2.0 ; python_version >= "3.10" and python_version < "4" exceptiongroup==1.1.2 ; python_version >= "3.10" and python_version < "3.11" fastapi==0.100.0 ; python_version >= "3.10" and python_version < "4.0" -findmyorder==1.6.1 ; python_version >= "3.10" and python_version < "4.0" +findmyorder==1.7.0 ; python_version >= "3.10" and python_version < "4.0" frozendict==2.3.8 ; python_version >= "3.10" and python_version < "4.0" frozenlist==1.4.0 ; python_version >= "3.10" and python_version < "4.0" future==0.18.3 ; python_version >= "3.10" and python_version < "4.0" @@ -47,7 +47,7 @@ html5lib==1.1 ; python_version >= "3.10" and python_version < "4.0" httpcore==0.17.3 ; python_version >= "3.10" and python_version < "4.0" httpx==0.24.1 ; python_version >= "3.10" and python_version < "4.0" hyperframe==6.0.1 ; python_version >= "3.10" and python_version < "4.0" -iamlistening==2.1.0 ; python_version >= "3.10" and python_version < "4.0" +iamlistening==3.0.1 ; python_version >= "3.10" and python_version < "4.0" idna==3.4 ; python_version >= "3.10" and python_version < "4.0" jsonschema-specifications==2023.7.1 ; python_version >= "3.10" and python_version < "4.0" jsonschema==4.18.4 ; python_version >= "3.10" and python_version < "4.0" @@ -96,13 +96,13 @@ rocketchat-api==1.30.0 ; python_version >= "3.10" and python_version < "4.0" rpds-py==0.9.2 ; python_version >= "3.10" and python_version < "4.0" rsa==4.9 ; python_version >= "3.10" and python_version < "4" setuptools==68.0.0 ; python_version >= "3.10" and python_version < "4.0" -simplematrixbotlib==2.9.0 ; python_version >= "3.10" and python_version < "4.0" +simplematrixbotlib==2.9.1 ; python_version >= "3.10" and python_version < "4.0" six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0" soupsieve==2.4.1 ; python_version >= "3.10" and python_version < "4.0" starlette==0.27.0 ; python_version >= "3.10" and python_version < "4.0" -talkytrend==1.11.0 ; python_version >= "3.10" and python_version < "4.0" -telethon==1.29.1 ; python_version >= "3.10" and python_version < "4.0" +talkytrend==1.12.1 ; python_version >= "3.10" and python_version < "4.0" +telethon==1.29.2 ; python_version >= "3.10" and python_version < "4.0" toml==0.10.2 ; python_version >= "3.10" and python_version < "4.0" toolz==0.12.0 ; python_version >= "3.10" and python_version < "4" and (implementation_name == "pypy" or implementation_name == "cpython") tradingview-ta==3.3.0 ; python_version >= "3.10" and python_version < "4.0" diff --git a/docs/index.md b/docs/index.md index 3bed098a5..6b1100df6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,300 +22,6 @@ Easily deploy via Docker on self-hosted platform or Paas.
screenshot -
-Get started - -
    -
  1. Create your channel/room and your platform bot -
  2. -
  3. Get your -
  4. -
  5. Create your config file settings.toml or use env variables
  6. - -
    -settings example - - - - - -
    - -
  7. Deploy via: -
      -
    • docker - docker pull mraniki/tt:latest or docker pull ghcr.io/mraniki/tt:latest
    • -
    • locally - git clone https://github.com/mraniki/tt:main && pip install -r requirements.txt
    • -
  8. -
  9. Start your container or if deployed locally use python3 bot.py to start
  10. -
  11. Try it now
  12. - -
- -
- -
- -
-Config - -
-env vars list -

- - - - -
- -
- -
- -
-Plugins - -

Talky

-
- - - - - - - -
- -
-
- -
-
- -
- Connect CEX and DEX exchanges
- across multi messaging platforms
- with plugin support. -
-
- -

FindMyOrder

- - - - - - - -
-
-
-
-
-
-
-
- Find My order,
- a parsing package to find trading order -
- -
How to use it
-
-
-      from findmyorder import FindMyOrder
-         fmo = FindMyOrder()
-         msg_order = "buy EURUSD sl=1000 tp=1000 q=1 comment=FOMC"
-         order = await fmo.get_order(msg_order)
-         #{'action': 'BUY', 'instrument': 'EURUSD', 'stop_loss': '1000', 'take_profit': '1000', 'quantity': '2', 'order_type': None, 'leverage_type': None, 'comment': None, 'timestamp': datetime.datetime(2023, 5, 3, 12, 10, 28, 731282, tzinfo=datetime.timezone.utc)}
-
-
- -
Example
- -https://github.com/mraniki/findmyorder/blob/6211e686abf1321ab3d5a8ac33068f854b0e9336/examples/example.py#L1-L90 - -

DXSP

-
- - - - - - - - -
-
-
-
-
-
-
-
-Swap made easy
-Trade on any blockchains with uniswap based router or 0x protocol. -
- -
How to use it
-
-
-   from dxsp import DexSwap
-    dex = DexSwap()
-    #BUY 10 USDT to SWAP with BITCOIN
-    demo_tx = await dex.get_swap('USDT','wBTC',10)
-    print("demo_tx ", demo_tx)
-
-
- -
Example
- -https://github.com/mraniki/dxsp/blob/f76fd035eddadc4de2a8509a7c26250c187b0658/examples/example.py#L1-L68 - - -

IamListening

-
- - - - - - - -
-
-
-
-
-
-
-
- A python package to listen to messaging platforms,
- such as discord, telegram and matrix -
- -
How to use it
-
-
-      from iamlistening import Listener
-        listener = Listener()
-        task = asyncio.create_task(listener.run_forever())
-        while True:
-          try:
-              msg = await listener.get_latest_message()
-              if msg:
-                  print(f"Frasier👂: {msg}"
-          except Exception as error:
-              print(error)
-        await task
-
-
- -
Example
- -https://github.com/mraniki/iamlistening/blob/52db0f4c194fe9edd12f48199737b4e1c73f7194/examples/example.py#L1-L54 - -

TalkyTrend

-
- - - - - - - -
-
-
-
-
-
-
-
-Retrieve asset trend and economic data.
-Trading view connectivity with signal scanner
-News connectivity
- FOMC reminder
-
- -
How to use it
-
-
-   from talkytrend import TalkyTrend
-   talky = TalkyTrend()
-    result = await talky.check_signal()
-    #  BUY
-    result = await talky.fetch_key_events()
-    print(result)
-    #  Title:  FDA advisers say new Alzheimer’s drug lecanemab slows cognitive decline
-    # Description:  Panel’s opinion could pave way for full regulatory approval next month for treatment of disease that affects 6.5m Americans
-    monitor = await talky.scanner() #ongoing monitoring
-    # New signal for BTCUSD (4h): STRONG_SELL
-    # Key event: {'title': 'OPEC-JMMC Meetings', 'country': 'ALL', 'date': '2023-06-04T06:15:00-04:00', 'impact': 'High', 'forecast': '', 'previous': ''}\
-    # Key news: FDA advisers say new Alzheimer’s drug lecanemab slows cognitive decline
-
-
- -
Example
-https://github.com/mraniki/talkytrend/blob/af472db335afec4e6a643077f7483d030e8511ac/examples/example.py#L1-L50 - -



diff --git a/pyproject.toml b/pyproject.toml index 0535c2b09..68b2f733b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,8 +50,8 @@ ping3 = "^4.0.4" ccxt = "^4.0.0" dxsp = "^4.1.0" findmyorder = "^1.6.1" -iamlistening = "^2.0.0" -talkytrend = "^1.10.0" +iamlistening = "^3.0.1" +talkytrend = "^1.12.1" [tool.poetry.group.dev.dependencies] @@ -85,6 +85,7 @@ pytest = "^7.0" pytest-cov = "^4.1" pytest-asyncio = "^0.21.0" pytest-mock = "^3.11.1" +pytest-loguru = "^0.2.0" [tool.poetry.group.docs] optional = true diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index dde2b62b7..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest -from _pytest.logging import LogCaptureFixture -from loguru import logger - - -@pytest.fixture -def caplog(caplog: LogCaptureFixture): - handler_id = logger.add( - caplog.handler, - format="{message}", - level=0, - filter=lambda record: record["level"].no >= caplog.handler.level, - enqueue=False, # Set to 'True' if your test is spawning child processes. - ) - yield caplog - logger.remove(handler_id) \ No newline at end of file diff --git a/tests/test_talkytrend_plugin.py b/tests/test_talkytrend_plugin.py index 975752e46..cdc579b22 100644 --- a/tests/test_talkytrend_plugin.py +++ b/tests/test_talkytrend_plugin.py @@ -52,14 +52,14 @@ async def test_plugin_tv(plugin): @pytest.mark.asyncio async def test_plugin_trend(plugin): """Test notification """ - plugin.check_signal = AsyncMock() + plugin.fetch_signal = AsyncMock() await plugin.handle_message(f"{settings.bot_prefix}{settings.bot_command_trend}") - plugin.check_signal.assert_awaited_once + plugin.fetch_signal.assert_awaited_once @pytest.mark.asyncio async def test_plugin_news(plugin): """Test notification """ - plugin.fetch_key_feed = AsyncMock() + plugin.fetch_feed = AsyncMock() await plugin.handle_message(f"{settings.bot_prefix}{settings.bot_command_news}") - plugin.fetch_key_feed.assert_awaited_once \ No newline at end of file + plugin.fetch_feed.assert_awaited_once \ No newline at end of file diff --git a/tests/test_unit.py b/tests/test_unit.py index d760b1940..86adda9ce 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -11,19 +11,26 @@ from tt.bot import app from tt.config import settings from tt.plugins.plugin_manager import BasePlugin, PluginManager -from tt.utils import send_notification, start_listener, start_plugins +from tt.utils import run_bot, send_notification, start_bot, start_plugins -#start_bot, run_bot @pytest.fixture(scope="session", autouse=True) def set_test_settings(): settings.configure(FORCE_ENV_FOR_DYNACONF="testing") +@pytest.fixture(name="message") +def message_test(): + return "Test message" + @pytest.fixture(name="listener_obj") def listener_test(): return Listener() +@pytest.fixture(name="plugin_manager_obj") +def pluginmngr_test(): + return PluginManager() + @pytest.fixture def message(): return "Test message" @@ -65,13 +72,6 @@ async def test_send_notification(caplog): assert "json://localhost/" in caplog.text -@pytest.mark.asyncio -async def test_start_listener(): - listener, task = await start_listener(max_iterations=1) - assert isinstance(listener, Listener) - assert isinstance(task, asyncio.Task) - - @pytest.mark.asyncio async def test_start_plugins(): plugin_manager = AsyncMock(spec=PluginManager) @@ -79,6 +79,49 @@ async def test_start_plugins(): plugin_manager.load_plugins.assert_called_once() +@pytest.mark.asyncio +async def test_start_bot(listener_obj, plugin_manager_obj): + start = AsyncMock() + task = asyncio.create_task(start_bot(listener_obj, plugin_manager_obj)) + task.cancel() + with pytest.raises(asyncio.CancelledError): + start.assert_awaited + await task + + +@pytest.mark.asyncio +async def test_run_bot(caplog): + start_bot = AsyncMock() + task = asyncio.create_task(run_bot()) + start_bot.assert_awaited + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + +# @pytest.mark.asyncio +# async def test_get_latest_message(message): +# listener = Listener() +# await listener.start() +# assert listener is not None +# assert settings.VALUE == "On Testing" +# await listener.handler.handle_message(message) +# assert await listener.handler.get_latest_message() == message + + +# @pytest.mark.asyncio +# async def test_listener_handler(): +# listener_test = Listener() +# print(listener_test) +# assert listener_test is not None +# assert isinstance(listener_test, Listener) +# await listener_test.start() +# await listener_test.handler.handle_message("hello") +# msg = await listener_test.handler.get_latest_message() +# print(msg) +# assert msg == "hello" + + @pytest.mark.asyncio async def test_baseplugins(): plugin = BasePlugin diff --git a/tt/config.py b/tt/config.py index fb9bbc0ec..487775318 100644 --- a/tt/config.py +++ b/tt/config.py @@ -12,7 +12,7 @@ from asyncz.schedulers.asyncio import AsyncIOScheduler from dynaconf import Dynaconf -from loguru import logger as log +from loguru import logger as loguru_logger ######################################## ### ⚙️ Settings ### @@ -36,37 +36,62 @@ default_env="default", ) - ######################################## ### 🧐 Logging ### ######################################## -# logging.basicConfig( -# format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -# level=settings.loglevel -# ) -# logger = logging.getLogger("TalkyTrader") +class InterceptHandler(logging.Handler): + def emit(self, record): + # Get corresponding Loguru level if it exists. + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + # Find caller from where originated the logged message. + frame, depth = sys._getframe(6), 6 + while frame and frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + logger.opt( + depth=depth, exception=record.exc_info).log( + level, record.getMessage()) def loguru_setup(): - log.remove() + loguru_logger.remove() # log.configure(**config) - log.add( + log_filters = { + "discord": "INFO", + "telethon": "INFO", + "web3": "INFO", + "apprise": "INFO", + "urllib3": "INFO", + } + logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) + loguru_logger.add( sink=sys.stderr, level=settings.loglevel, + filter=log_filters, ) - return log + return loguru_logger logger = loguru_setup() -if settings.loglevel == "DEBUG": - logging.getLogger("discord").setLevel(logging.ERROR) - logging.getLogger("telethon").setLevel(logging.ERROR) - logging.getLogger("urllib3").setLevel(logging.ERROR) - logging.getLogger("apprise").setLevel(logging.ERROR) - logging.getLogger("web3").setLevel(logging.ERROR) + +# logging.basicConfig( +# format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +# level=settings.loglevel +# ) +# logger = logging.getLogger("TalkyTrader") +# if settings.loglevel == "DEBUG": +# logging.getLogger("discord").setLevel(logging.ERROR) +# logging.getLogger("telethon").setLevel(logging.ERROR) +# logging.getLogger("urllib3").setLevel(logging.ERROR) +# logging.getLogger("apprise").setLevel(logging.ERROR) +# logging.getLogger("web3").setLevel(logging.ERROR) ######################################## ### ⏱️ Scheduling ### diff --git a/tt/plugins/default_plugins/dex_exchange_plugin.py b/tt/plugins/default_plugins/dex_exchange_plugin.py index f5abcb13e..9a1ce92b1 100644 --- a/tt/plugins/default_plugins/dex_exchange_plugin.py +++ b/tt/plugins/default_plugins/dex_exchange_plugin.py @@ -65,4 +65,3 @@ async def handle_message(self, msg): if command in command_mapping: function = command_mapping[command] await self.send_notification(f"{await function()}") - diff --git a/tt/plugins/default_plugins/talkytrend_plugin.py b/tt/plugins/default_plugins/talkytrend_plugin.py index 6106c7e1d..ff3826a60 100644 --- a/tt/plugins/default_plugins/talkytrend_plugin.py +++ b/tt/plugins/default_plugins/talkytrend_plugin.py @@ -18,12 +18,9 @@ def __init__(self): async def start(self): """Starts the TalkyTrend plugin""" if self.enabled: - await self.plugin_notify_schedule_task( - user_name="talky_feed", - function=self.trend.fetch_key_feed) await self.plugin_notify_cron_task( - user_name="talky_signal", - function=self.trend.check_signal) + user_name="talky_monitor", + function=self.trend.monitor) async def stop(self): """Stops the TalkyTrend plugin""" @@ -46,11 +43,10 @@ async def handle_message(self, msg): settings.bot_command_help: self.trend.get_talkytrend_help, settings.bot_command_info: self.trend.get_talkytrend_info, settings.bot_command_tv: self.trend.get_tv, - settings.bot_command_trend: self.trend.check_signal, - settings.bot_command_news: self.trend.fetch_key_feed, + settings.bot_command_trend: self.trend.fetch_signal, + settings.bot_command_news: self.trend.fetch_feed, } if command in command_mapping: function = command_mapping[command] await self.send_notification(f"{await function()}") - diff --git a/tt/plugins/plugin_manager.py b/tt/plugins/plugin_manager.py index 1f4f34e00..850e05a25 100644 --- a/tt/plugins/plugin_manager.py +++ b/tt/plugins/plugin_manager.py @@ -35,7 +35,7 @@ def load_plugin(self, module, plugin_name): and obj is not BasePlugin): plugin_instance = obj() self.plugins.append(plugin_instance) - logger.info("Plugin loaded: {}", name) + logger.debug("Plugin loaded: {}", name) async def start_all_plugins(self): """ Start all plugins """ @@ -105,7 +105,7 @@ async def plugin_notify_cron_task( user_timezone="UTC", function=None): """Handles task cron scheduling for notification - monday to Friday at 8AM, 12PM and 4PM with time being UTC based""" + monday to Friday at 8AM, 12PM and 4PM UTC based""" if function: self.scheduler.add_task( name=user_name, diff --git a/tt/talky_settings.toml b/tt/talky_settings.toml index 8c54a9e3f..9d50eb76e 100644 --- a/tt/talky_settings.toml +++ b/tt/talky_settings.toml @@ -47,8 +47,6 @@ webhook_secret = "123abc" ### APPRISE SETTINGS ### ######################################## -apprise_api_endpoint = "" -apprise_config = "" apprise_url = "json://localhost" apprise_format = "NotifyFormat.MARKDOWN" # TT_APPRISE_URL='tgram://TOKEN/CHAT_ID' @@ -192,7 +190,6 @@ talkytrend_commands = "📺 /live\n📰 /news\n📊 /trend" bot_command_tv = "live" bot_command_trend = "trend" bot_command_news = "news" -scanner_frequency = 86400 enable_signals = true assets = [ { id ="EURUSD", exchange='FX_IDC',screener="forex"}, @@ -240,15 +237,13 @@ loglevel = "DEBUG" host = "0.0.0.0" port = 8080 webhook_secret = "123abc" -apprise_api_endpoint = "" -apprise_config = "" apprise_url = "json://localhost" apprise_format = "NotifyFormat.MARKDOWN" iamlistening_enabled = true -bot_token = "" -bot_channel_id = "" -telethon_api_id = "" -telethon_api_hash = "" +bot_token = "1234556" +bot_channel_id = "1234556" +telethon_api_id = "1234556" +telethon_api_hash = "1234556" matrix_hostname = "" matrix_user = "" matrix_pass = "" @@ -329,7 +324,7 @@ talkytrend_commands = "📺 /live\n📰 /news\n📊 /trend" bot_command_tv = "live" bot_command_trend = "trend" bot_command_news = "news" -scanner_frequency = 86400 + enable_signals = true assets = [ { id ="EURUSD", exchange='FX_IDC',screener="forex"}, diff --git a/tt/utils.py b/tt/utils.py index f1de1b145..c71b9ed58 100644 --- a/tt/utils.py +++ b/tt/utils.py @@ -9,7 +9,7 @@ from apprise import Apprise, NotifyFormat from iamlistening import Listener -from tt.config import logger, settings +from tt.config import settings from tt.plugins.plugin_manager import PluginManager @@ -17,58 +17,41 @@ async def send_notification(msg): """ 💬 Notification via Apprise """ - aobj = Apprise() - if settings.apprise_api_endpoint: - aobj.add(settings.apprise_api_endpoint) - elif settings.apprise_config: - aobj.add(settings.apprise_config) - elif settings.apprise_url: - aobj.add(settings.apprise_url) + aobj = Apprise(settings.apprise_url) msg_format = settings.apprise_format or NotifyFormat.MARKDOWN await aobj.async_notify( body=msg, body_format=msg_format) -async def start_listener(max_iterations=None): +async def run_bot(max_iterations=None): """ - Start the chat listener. + 🤖 Run the chat bot & the plugins. """ - bot_listener = Listener() - task = asyncio.create_task(bot_listener.run_forever(max_iterations)) - return bot_listener, task + listener = Listener() + plugin_manager = PluginManager() + await asyncio.gather(start_bot(listener, plugin_manager)) async def start_plugins(plugin_manager): """ - Start all plugins. + 🔌 Start all plugins. """ if settings.plugin_enabled: plugin_manager.load_plugins() loop = asyncio.get_running_loop() loop.create_task(plugin_manager.start_all_plugins()) + async def start_bot(listener, plugin_manager): """ - Listen to the message in the bot channel - and dispatch to plugins + 👂 Listen to the bot and dispatch to plugins """ + await listener.start() + await start_plugins(plugin_manager) while True: - try: - msg = await listener.get_latest_message() - if msg and settings.plugin_enabled: - await plugin_manager.process_message(msg) - except Exception as error: - logger.error("👂 listener: {}", error) - - await asyncio.sleep(1) - + msg = await listener.handler.get_latest_message() + if msg and settings.plugin_enabled: + await plugin_manager.process_message(msg) -async def run_bot(max_iterations=None): - """ - Run the chat bot & the plugins. - """ - listener, task = await start_listener(max_iterations) - plugin_manager = PluginManager() - await start_plugins(plugin_manager) - await asyncio.gather(start_bot(listener, plugin_manager), task) \ No newline at end of file + await asyncio.sleep(1)