Compare commits
1 Commits
feature/is
...
fix/start-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c5eadc23b |
@@ -66,10 +66,6 @@ class Settings(BaseSettings):
|
|||||||
TELEGRAM_CHAT_ID: str | None = None
|
TELEGRAM_CHAT_ID: str | None = None
|
||||||
TELEGRAM_ENABLED: bool = True
|
TELEGRAM_ENABLED: bool = True
|
||||||
|
|
||||||
# Telegram Commands (optional)
|
|
||||||
TELEGRAM_COMMANDS_ENABLED: bool = True
|
|
||||||
TELEGRAM_POLLING_INTERVAL: float = 1.0 # seconds
|
|
||||||
|
|
||||||
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
|
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
16
src/main.py
16
src/main.py
@@ -579,25 +579,10 @@ async def run(settings: Settings) -> None:
|
|||||||
command_handler = TelegramCommandHandler(telegram)
|
command_handler = TelegramCommandHandler(telegram)
|
||||||
|
|
||||||
# Register basic commands
|
# 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:
|
async def handle_help() -> None:
|
||||||
"""Handle /help command."""
|
"""Handle /help command."""
|
||||||
message = (
|
message = (
|
||||||
"<b>📖 Available Commands</b>\n\n"
|
"<b>📖 Available Commands</b>\n\n"
|
||||||
"/start - Welcome message\n"
|
|
||||||
"/help - Show available commands\n"
|
"/help - Show available commands\n"
|
||||||
"/status - Trading status (mode, markets, P&L)\n"
|
"/status - Trading status (mode, markets, P&L)\n"
|
||||||
"/positions - Current holdings\n"
|
"/positions - Current holdings\n"
|
||||||
@@ -722,7 +707,6 @@ async def run(settings: Settings) -> None:
|
|||||||
"<b>⚠️ Error</b>\n\nFailed to retrieve positions."
|
"<b>⚠️ Error</b>\n\nFailed to retrieve positions."
|
||||||
)
|
)
|
||||||
|
|
||||||
command_handler.register_command("start", handle_start)
|
|
||||||
command_handler.register_command("help", handle_help)
|
command_handler.register_command("help", handle_help)
|
||||||
command_handler.register_command("stop", handle_stop)
|
command_handler.register_command("stop", handle_stop)
|
||||||
command_handler.register_command("resume", handle_resume)
|
command_handler.register_command("resume", handle_resume)
|
||||||
|
|||||||
@@ -200,151 +200,14 @@ telegram = TelegramClient(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bidirectional Commands
|
|
||||||
|
|
||||||
Control your trading bot remotely via Telegram commands. The bot not only sends notifications but also accepts commands for real-time control.
|
|
||||||
|
|
||||||
### Available Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `/start` | Welcome message with quick start guide |
|
|
||||||
| `/help` | List all available commands |
|
|
||||||
| `/status` | Current trading status (mode, markets, P&L, circuit breaker) |
|
|
||||||
| `/positions` | View current holdings grouped by market |
|
|
||||||
| `/stop` | Pause all trading operations |
|
|
||||||
| `/resume` | Resume trading operations |
|
|
||||||
|
|
||||||
### Command Examples
|
|
||||||
|
|
||||||
**Check Trading Status**
|
|
||||||
```
|
|
||||||
You: /status
|
|
||||||
|
|
||||||
Bot:
|
|
||||||
📊 Trading Status
|
|
||||||
|
|
||||||
Mode: PAPER
|
|
||||||
Markets: Korea, United States
|
|
||||||
Trading: Active
|
|
||||||
|
|
||||||
Current P&L: +2.50%
|
|
||||||
Circuit Breaker: -3.0%
|
|
||||||
```
|
|
||||||
|
|
||||||
**View Holdings**
|
|
||||||
```
|
|
||||||
You: /positions
|
|
||||||
|
|
||||||
Bot:
|
|
||||||
💼 Current Holdings
|
|
||||||
|
|
||||||
🇰🇷 Korea
|
|
||||||
• 005930: 10 shares @ 70,000
|
|
||||||
• 035420: 5 shares @ 200,000
|
|
||||||
|
|
||||||
🇺🇸 Overseas
|
|
||||||
• AAPL: 15 shares @ 175
|
|
||||||
• TSLA: 8 shares @ 245
|
|
||||||
|
|
||||||
Cash: ₩5,000,000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pause Trading**
|
|
||||||
```
|
|
||||||
You: /stop
|
|
||||||
|
|
||||||
Bot:
|
|
||||||
⏸️ Trading Paused
|
|
||||||
|
|
||||||
All trading operations have been suspended.
|
|
||||||
Use /resume to restart trading.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resume Trading**
|
|
||||||
```
|
|
||||||
You: /resume
|
|
||||||
|
|
||||||
Bot:
|
|
||||||
▶️ Trading Resumed
|
|
||||||
|
|
||||||
Trading operations have been restarted.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
**Chat ID Verification**
|
|
||||||
- Commands are only accepted from the configured `TELEGRAM_CHAT_ID`
|
|
||||||
- Unauthorized users receive no response
|
|
||||||
- Command attempts from wrong chat IDs are logged
|
|
||||||
|
|
||||||
**Authorization Required**
|
|
||||||
- Only the bot owner (chat ID in `.env`) can control trading
|
|
||||||
- No way for unauthorized users to discover or use commands
|
|
||||||
- All command executions are logged for audit
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
Add to your `.env` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Commands are enabled by default
|
|
||||||
TELEGRAM_COMMANDS_ENABLED=true
|
|
||||||
|
|
||||||
# Polling interval (seconds) - how often to check for commands
|
|
||||||
TELEGRAM_POLLING_INTERVAL=1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
To disable commands but keep notifications:
|
|
||||||
```bash
|
|
||||||
TELEGRAM_COMMANDS_ENABLED=false
|
|
||||||
```
|
|
||||||
|
|
||||||
### How It Works
|
|
||||||
|
|
||||||
1. **Long Polling**: Bot checks Telegram API every second for new messages
|
|
||||||
2. **Command Parsing**: Messages starting with `/` are parsed as commands
|
|
||||||
3. **Authentication**: Chat ID is verified before executing any command
|
|
||||||
4. **Execution**: Command handler is called with current bot state
|
|
||||||
5. **Response**: Result is sent back via Telegram
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- Command parsing errors → "Unknown command" response
|
|
||||||
- API failures → Graceful degradation, error logged
|
|
||||||
- Invalid state → Appropriate message (e.g., "Trading is already paused")
|
|
||||||
- Trading loop isolation → Command errors never crash trading
|
|
||||||
|
|
||||||
### Troubleshooting Commands
|
|
||||||
|
|
||||||
**Commands not responding**
|
|
||||||
1. Check `TELEGRAM_COMMANDS_ENABLED=true` in `.env`
|
|
||||||
2. Verify you started conversation with `/start`
|
|
||||||
3. Check logs for command handler errors
|
|
||||||
4. Confirm chat ID matches `.env` configuration
|
|
||||||
|
|
||||||
**Wrong chat ID**
|
|
||||||
- Commands from unauthorized chats are silently ignored
|
|
||||||
- Check logs for "unauthorized chat_id" warnings
|
|
||||||
|
|
||||||
**Delayed responses**
|
|
||||||
- Polling interval is 1 second by default
|
|
||||||
- Network latency may add delay
|
|
||||||
- Check `TELEGRAM_POLLING_INTERVAL` setting
|
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
See `telegram_client.py` for full API documentation.
|
See `telegram_client.py` for full API documentation.
|
||||||
|
|
||||||
### Notification Methods
|
Key methods:
|
||||||
- `notify_trade_execution()` - Trade alerts
|
- `notify_trade_execution()` - Trade alerts
|
||||||
- `notify_circuit_breaker()` - Emergency stops
|
- `notify_circuit_breaker()` - Emergency stops
|
||||||
- `notify_fat_finger()` - Order rejections
|
- `notify_fat_finger()` - Order rejections
|
||||||
- `notify_market_open/close()` - Session tracking
|
- `notify_market_open/close()` - Session tracking
|
||||||
- `notify_system_start/shutdown()` - Lifecycle events
|
- `notify_system_start/shutdown()` - Lifecycle events
|
||||||
- `notify_error()` - Error alerts
|
- `notify_error()` - Error alerts
|
||||||
|
|
||||||
### Command Handler
|
|
||||||
- `TelegramCommandHandler` - Bidirectional command processing
|
|
||||||
- `register_command()` - Register custom command handlers
|
|
||||||
- `start_polling()` / `stop_polling()` - Lifecycle management
|
|
||||||
|
|||||||
@@ -492,7 +492,8 @@ class TelegramCommandHandler:
|
|||||||
if not command_parts:
|
if not command_parts:
|
||||||
return
|
return
|
||||||
|
|
||||||
command_name = command_parts[0]
|
# Remove @botname suffix if present (for group chats)
|
||||||
|
command_name = command_parts[0].split("@")[0]
|
||||||
|
|
||||||
# Execute handler
|
# Execute handler
|
||||||
handler = self._commands.get(command_name)
|
handler = self._commands.get(command_name)
|
||||||
|
|||||||
@@ -230,6 +230,31 @@ class TestUpdateHandling:
|
|||||||
await handler._handle_update(update)
|
await handler._handle_update(update)
|
||||||
assert executed is False
|
assert executed is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handle_command_with_botname(self) -> None:
|
||||||
|
"""Commands with @botname suffix are handled correctly."""
|
||||||
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
|
executed = False
|
||||||
|
|
||||||
|
async def test_command() -> None:
|
||||||
|
nonlocal executed
|
||||||
|
executed = True
|
||||||
|
|
||||||
|
handler.register_command("start", test_command)
|
||||||
|
|
||||||
|
update = {
|
||||||
|
"update_id": 1,
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 456},
|
||||||
|
"text": "/start@mybot",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await handler._handle_update(update)
|
||||||
|
assert executed is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_handle_update_error_isolation(self) -> None:
|
async def test_handle_update_error_isolation(self) -> None:
|
||||||
"""Errors in handlers don't crash the system."""
|
"""Errors in handlers don't crash the system."""
|
||||||
@@ -638,51 +663,6 @@ class TestStatusCommands:
|
|||||||
class TestBasicCommands:
|
class TestBasicCommands:
|
||||||
"""Test basic command implementations."""
|
"""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
|
@pytest.mark.asyncio
|
||||||
async def test_help_command_content(self) -> None:
|
async def test_help_command_content(self) -> None:
|
||||||
"""Help command lists all available commands."""
|
"""Help command lists all available commands."""
|
||||||
@@ -698,7 +678,6 @@ class TestBasicCommands:
|
|||||||
"""Mock /help handler."""
|
"""Mock /help handler."""
|
||||||
message = (
|
message = (
|
||||||
"<b>📖 Available Commands</b>\n\n"
|
"<b>📖 Available Commands</b>\n\n"
|
||||||
"/start - Welcome message\n"
|
|
||||||
"/help - Show available commands\n"
|
"/help - Show available commands\n"
|
||||||
"/status - Trading status (mode, markets, P&L)\n"
|
"/status - Trading status (mode, markets, P&L)\n"
|
||||||
"/positions - Current holdings\n"
|
"/positions - Current holdings\n"
|
||||||
@@ -724,7 +703,6 @@ class TestBasicCommands:
|
|||||||
assert mock_post.call_count == 1
|
assert mock_post.call_count == 1
|
||||||
payload = mock_post.call_args.kwargs["json"]
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
assert "Available Commands" in payload["text"]
|
assert "Available Commands" in payload["text"]
|
||||||
assert "/start" in payload["text"]
|
|
||||||
assert "/help" in payload["text"]
|
assert "/help" in payload["text"]
|
||||||
assert "/status" in payload["text"]
|
assert "/status" in payload["text"]
|
||||||
assert "/positions" in payload["text"]
|
assert "/positions" in payload["text"]
|
||||||
|
|||||||
Reference in New Issue
Block a user