Compare commits
4 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70701bf73a | ||
| 20dbd94892 | |||
|
|
48a99962e3 | ||
| ee66ecc305 |
81
src/main.py
81
src/main.py
@@ -29,7 +29,7 @@ from src.db import init_db, log_trade
|
|||||||
from src.logging.decision_logger import DecisionLogger
|
from src.logging.decision_logger import DecisionLogger
|
||||||
from src.logging_config import setup_logging
|
from src.logging_config import setup_logging
|
||||||
from src.markets.schedule import MarketInfo, get_next_market_open, get_open_markets
|
from src.markets.schedule import MarketInfo, get_next_market_open, get_open_markets
|
||||||
from src.notifications.telegram_client import TelegramClient
|
from src.notifications.telegram_client import TelegramClient, TelegramCommandHandler
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -575,6 +575,69 @@ async def run(settings: Settings) -> None:
|
|||||||
enabled=settings.TELEGRAM_ENABLED,
|
enabled=settings.TELEGRAM_ENABLED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Initialize Telegram command handler
|
||||||
|
command_handler = TelegramCommandHandler(telegram)
|
||||||
|
|
||||||
|
# Register basic commands
|
||||||
|
async def handle_start() -> None:
|
||||||
|
"""Handle /start command."""
|
||||||
|
message = (
|
||||||
|
"<b>🤖 The Ouroboros Trading Bot</b>\n\n"
|
||||||
|
"AI-powered global stock trading agent with real-time notifications.\n\n"
|
||||||
|
"<b>Available commands:</b>\n"
|
||||||
|
"/help - Show this help message\n"
|
||||||
|
"/status - Current trading status\n"
|
||||||
|
"/positions - View holdings\n"
|
||||||
|
"/stop - Pause trading\n"
|
||||||
|
"/resume - Resume trading"
|
||||||
|
)
|
||||||
|
await telegram.send_message(message)
|
||||||
|
|
||||||
|
async def handle_help() -> None:
|
||||||
|
"""Handle /help command."""
|
||||||
|
message = (
|
||||||
|
"<b>📖 Available Commands</b>\n\n"
|
||||||
|
"/start - Welcome message\n"
|
||||||
|
"/help - Show available commands\n"
|
||||||
|
"/status - Trading status (mode, markets, P&L)\n"
|
||||||
|
"/positions - Current holdings\n"
|
||||||
|
"/stop - Pause trading\n"
|
||||||
|
"/resume - Resume trading"
|
||||||
|
)
|
||||||
|
await telegram.send_message(message)
|
||||||
|
|
||||||
|
async def handle_stop() -> None:
|
||||||
|
"""Handle /stop command - pause trading."""
|
||||||
|
if not pause_trading.is_set():
|
||||||
|
await telegram.send_message("⏸️ Trading is already paused")
|
||||||
|
return
|
||||||
|
|
||||||
|
pause_trading.clear()
|
||||||
|
logger.info("Trading paused via Telegram command")
|
||||||
|
await telegram.send_message(
|
||||||
|
"<b>⏸️ Trading Paused</b>\n\n"
|
||||||
|
"All trading operations have been suspended.\n"
|
||||||
|
"Use /resume to restart trading."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle_resume() -> None:
|
||||||
|
"""Handle /resume command - resume trading."""
|
||||||
|
if pause_trading.is_set():
|
||||||
|
await telegram.send_message("▶️ Trading is already active")
|
||||||
|
return
|
||||||
|
|
||||||
|
pause_trading.set()
|
||||||
|
logger.info("Trading resumed via Telegram command")
|
||||||
|
await telegram.send_message(
|
||||||
|
"<b>▶️ Trading Resumed</b>\n\n"
|
||||||
|
"Trading operations have been restarted."
|
||||||
|
)
|
||||||
|
|
||||||
|
command_handler.register_command("start", handle_start)
|
||||||
|
command_handler.register_command("help", handle_help)
|
||||||
|
command_handler.register_command("stop", handle_stop)
|
||||||
|
command_handler.register_command("resume", handle_resume)
|
||||||
|
|
||||||
# Initialize volatility hunter
|
# Initialize volatility hunter
|
||||||
volatility_analyzer = VolatilityAnalyzer(min_volume_surge=2.0, min_price_change=1.0)
|
volatility_analyzer = VolatilityAnalyzer(min_volume_surge=2.0, min_price_change=1.0)
|
||||||
market_scanner = MarketScanner(
|
market_scanner = MarketScanner(
|
||||||
@@ -602,7 +665,10 @@ async def run(settings: Settings) -> None:
|
|||||||
# Track market open/close state for notifications
|
# Track market open/close state for notifications
|
||||||
_market_states: dict[str, bool] = {} # market_code -> is_open
|
_market_states: dict[str, bool] = {} # market_code -> is_open
|
||||||
|
|
||||||
|
# Trading control events
|
||||||
shutdown = asyncio.Event()
|
shutdown = asyncio.Event()
|
||||||
|
pause_trading = asyncio.Event()
|
||||||
|
pause_trading.set() # Default: trading enabled
|
||||||
|
|
||||||
def _signal_handler() -> None:
|
def _signal_handler() -> None:
|
||||||
logger.info("Shutdown signal received")
|
logger.info("Shutdown signal received")
|
||||||
@@ -621,6 +687,12 @@ async def run(settings: Settings) -> None:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("System startup notification failed: %s", exc)
|
logger.warning("System startup notification failed: %s", exc)
|
||||||
|
|
||||||
|
# Start command handler
|
||||||
|
try:
|
||||||
|
await command_handler.start_polling()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to start command handler: %s", exc)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Branch based on trading mode
|
# Branch based on trading mode
|
||||||
if settings.TRADE_MODE == "daily":
|
if settings.TRADE_MODE == "daily":
|
||||||
@@ -634,6 +706,9 @@ async def run(settings: Settings) -> None:
|
|||||||
session_interval = settings.SESSION_INTERVAL_HOURS * 3600 # Convert to seconds
|
session_interval = settings.SESSION_INTERVAL_HOURS * 3600 # Convert to seconds
|
||||||
|
|
||||||
while not shutdown.is_set():
|
while not shutdown.is_set():
|
||||||
|
# Wait for trading to be unpaused
|
||||||
|
await pause_trading.wait()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await run_daily_session(
|
await run_daily_session(
|
||||||
broker,
|
broker,
|
||||||
@@ -666,6 +741,9 @@ async def run(settings: Settings) -> None:
|
|||||||
logger.info("Realtime trading mode: 60s interval per stock")
|
logger.info("Realtime trading mode: 60s interval per stock")
|
||||||
|
|
||||||
while not shutdown.is_set():
|
while not shutdown.is_set():
|
||||||
|
# Wait for trading to be unpaused
|
||||||
|
await pause_trading.wait()
|
||||||
|
|
||||||
# Get currently open markets
|
# Get currently open markets
|
||||||
open_markets = get_open_markets(settings.enabled_market_list)
|
open_markets = get_open_markets(settings.enabled_market_list)
|
||||||
|
|
||||||
@@ -831,6 +909,7 @@ async def run(settings: Settings) -> None:
|
|||||||
pass # Normal — timeout means it's time for next cycle
|
pass # Normal — timeout means it's time for next cycle
|
||||||
finally:
|
finally:
|
||||||
# Clean up resources
|
# Clean up resources
|
||||||
|
await command_handler.stop_polling()
|
||||||
await broker.close()
|
await broker.close()
|
||||||
await telegram.close()
|
await telegram.close()
|
||||||
db_conn.close()
|
db_conn.close()
|
||||||
|
|||||||
@@ -253,6 +253,292 @@ class TestUpdateHandling:
|
|||||||
await handler._handle_update(update)
|
await handler._handle_update(update)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTradingControlCommands:
|
||||||
|
"""Test trading control commands."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stop_command_pauses_trading(self) -> None:
|
||||||
|
"""Stop command clears pause event."""
|
||||||
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
|
# Create mock pause event
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
pause_event = asyncio.Event()
|
||||||
|
pause_event.set() # Initially active
|
||||||
|
|
||||||
|
mock_resp = AsyncMock()
|
||||||
|
mock_resp.status = 200
|
||||||
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||||
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
async def mock_stop() -> None:
|
||||||
|
"""Mock /stop handler."""
|
||||||
|
if not pause_event.is_set():
|
||||||
|
await client.send_message("⏸️ Trading is already paused")
|
||||||
|
return
|
||||||
|
|
||||||
|
pause_event.clear()
|
||||||
|
await client.send_message(
|
||||||
|
"<b>⏸️ Trading Paused</b>\n\n"
|
||||||
|
"All trading operations have been suspended.\n"
|
||||||
|
"Use /resume to restart trading."
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.register_command("stop", mock_stop)
|
||||||
|
|
||||||
|
with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
|
||||||
|
update = {
|
||||||
|
"update_id": 1,
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 456},
|
||||||
|
"text": "/stop",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler._handle_update(update)
|
||||||
|
|
||||||
|
# Verify pause event was cleared
|
||||||
|
assert not pause_event.is_set()
|
||||||
|
|
||||||
|
# Verify message was sent
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
|
assert "Trading Paused" in payload["text"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resume_command_resumes_trading(self) -> None:
|
||||||
|
"""Resume command sets pause event."""
|
||||||
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
|
# Create mock pause event (initially paused)
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
pause_event = asyncio.Event()
|
||||||
|
pause_event.clear() # Initially paused
|
||||||
|
|
||||||
|
mock_resp = AsyncMock()
|
||||||
|
mock_resp.status = 200
|
||||||
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||||
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
async def mock_resume() -> None:
|
||||||
|
"""Mock /resume handler."""
|
||||||
|
if pause_event.is_set():
|
||||||
|
await client.send_message("▶️ Trading is already active")
|
||||||
|
return
|
||||||
|
|
||||||
|
pause_event.set()
|
||||||
|
await client.send_message(
|
||||||
|
"<b>▶️ Trading Resumed</b>\n\n"
|
||||||
|
"Trading operations have been restarted."
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.register_command("resume", mock_resume)
|
||||||
|
|
||||||
|
with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
|
||||||
|
update = {
|
||||||
|
"update_id": 1,
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 456},
|
||||||
|
"text": "/resume",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler._handle_update(update)
|
||||||
|
|
||||||
|
# Verify pause event was set
|
||||||
|
assert pause_event.is_set()
|
||||||
|
|
||||||
|
# Verify message was sent
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
|
assert "Trading Resumed" in payload["text"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stop_when_already_paused(self) -> None:
|
||||||
|
"""Stop command when already paused sends appropriate message."""
|
||||||
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
|
# Create mock pause event (already paused)
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
pause_event = asyncio.Event()
|
||||||
|
pause_event.clear()
|
||||||
|
|
||||||
|
mock_resp = AsyncMock()
|
||||||
|
mock_resp.status = 200
|
||||||
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||||
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
async def mock_stop() -> None:
|
||||||
|
"""Mock /stop handler."""
|
||||||
|
if not pause_event.is_set():
|
||||||
|
await client.send_message("⏸️ Trading is already paused")
|
||||||
|
return
|
||||||
|
|
||||||
|
pause_event.clear()
|
||||||
|
|
||||||
|
handler.register_command("stop", mock_stop)
|
||||||
|
|
||||||
|
with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
|
||||||
|
update = {
|
||||||
|
"update_id": 1,
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 456},
|
||||||
|
"text": "/stop",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler._handle_update(update)
|
||||||
|
|
||||||
|
# Verify message was sent
|
||||||
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
|
assert "already paused" in payload["text"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resume_when_already_active(self) -> None:
|
||||||
|
"""Resume command when already active sends appropriate message."""
|
||||||
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
|
# Create mock pause event (already active)
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
pause_event = asyncio.Event()
|
||||||
|
pause_event.set()
|
||||||
|
|
||||||
|
mock_resp = AsyncMock()
|
||||||
|
mock_resp.status = 200
|
||||||
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||||
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
async def mock_resume() -> None:
|
||||||
|
"""Mock /resume handler."""
|
||||||
|
if pause_event.is_set():
|
||||||
|
await client.send_message("▶️ Trading is already active")
|
||||||
|
return
|
||||||
|
|
||||||
|
pause_event.set()
|
||||||
|
|
||||||
|
handler.register_command("resume", mock_resume)
|
||||||
|
|
||||||
|
with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
|
||||||
|
update = {
|
||||||
|
"update_id": 1,
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 456},
|
||||||
|
"text": "/resume",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler._handle_update(update)
|
||||||
|
|
||||||
|
# Verify message was sent
|
||||||
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
|
assert "already active" in payload["text"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasicCommands:
|
||||||
|
"""Test basic command implementations."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_command_content(self) -> None:
|
||||||
|
"""Start command contains welcome message and command list."""
|
||||||
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
|
mock_resp = AsyncMock()
|
||||||
|
mock_resp.status = 200
|
||||||
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||||
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
async def mock_start() -> None:
|
||||||
|
"""Mock /start handler."""
|
||||||
|
message = (
|
||||||
|
"<b>🤖 The Ouroboros Trading Bot</b>\n\n"
|
||||||
|
"AI-powered global stock trading agent with real-time notifications.\n\n"
|
||||||
|
"<b>Available commands:</b>\n"
|
||||||
|
"/help - Show this help message\n"
|
||||||
|
"/status - Current trading status\n"
|
||||||
|
"/positions - View holdings\n"
|
||||||
|
"/stop - Pause trading\n"
|
||||||
|
"/resume - Resume trading"
|
||||||
|
)
|
||||||
|
await client.send_message(message)
|
||||||
|
|
||||||
|
handler.register_command("start", mock_start)
|
||||||
|
|
||||||
|
with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
|
||||||
|
update = {
|
||||||
|
"update_id": 1,
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 456},
|
||||||
|
"text": "/start",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler._handle_update(update)
|
||||||
|
|
||||||
|
# Verify message was sent
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
|
assert "Ouroboros Trading Bot" in payload["text"]
|
||||||
|
assert "/help" in payload["text"]
|
||||||
|
assert "/status" in payload["text"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_help_command_content(self) -> None:
|
||||||
|
"""Help command lists all available commands."""
|
||||||
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
|
mock_resp = AsyncMock()
|
||||||
|
mock_resp.status = 200
|
||||||
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||||
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
|
||||||
|
async def mock_help() -> None:
|
||||||
|
"""Mock /help handler."""
|
||||||
|
message = (
|
||||||
|
"<b>📖 Available Commands</b>\n\n"
|
||||||
|
"/start - Welcome message\n"
|
||||||
|
"/help - Show available commands\n"
|
||||||
|
"/status - Trading status (mode, markets, P&L)\n"
|
||||||
|
"/positions - Current holdings\n"
|
||||||
|
"/stop - Pause trading\n"
|
||||||
|
"/resume - Resume trading"
|
||||||
|
)
|
||||||
|
await client.send_message(message)
|
||||||
|
|
||||||
|
handler.register_command("help", mock_help)
|
||||||
|
|
||||||
|
with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
|
||||||
|
update = {
|
||||||
|
"update_id": 1,
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 456},
|
||||||
|
"text": "/help",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler._handle_update(update)
|
||||||
|
|
||||||
|
# Verify message was sent
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
|
assert "Available Commands" in payload["text"]
|
||||||
|
assert "/start" in payload["text"]
|
||||||
|
assert "/help" in payload["text"]
|
||||||
|
assert "/status" in payload["text"]
|
||||||
|
assert "/positions" in payload["text"]
|
||||||
|
assert "/stop" in payload["text"]
|
||||||
|
assert "/resume" in payload["text"]
|
||||||
|
|
||||||
|
|
||||||
class TestGetUpdates:
|
class TestGetUpdates:
|
||||||
"""Test getUpdates API interaction."""
|
"""Test getUpdates API interaction."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user