Compare commits
4 Commits
fix/start-
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a098d9a6 | ||
| d2b07326ed | |||
| 10ff718045 | |||
|
|
0ca3fe9f5d |
@@ -66,6 +66,10 @@ class Settings(BaseSettings):
|
||||
TELEGRAM_CHAT_ID: str | None = None
|
||||
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"}
|
||||
|
||||
@property
|
||||
|
||||
76
src/main.py
76
src/main.py
@@ -624,11 +624,21 @@ async def run(settings: Settings) -> None:
|
||||
# Get trading status
|
||||
trading_status = "Active" if pause_trading.is_set() else "Paused"
|
||||
|
||||
# Get current P&L from risk manager
|
||||
# Calculate P&L from balance data
|
||||
try:
|
||||
balance = await broker.get_balance()
|
||||
current_pnl = risk.calculate_pnl(balance)
|
||||
pnl_str = f"{current_pnl:+.2f}%"
|
||||
output2 = balance.get("output2", [{}])
|
||||
if output2:
|
||||
total_eval = safe_float(output2[0].get("tot_evlu_amt", "0"))
|
||||
purchase_total = safe_float(output2[0].get("pchs_amt_smtl_amt", "0"))
|
||||
current_pnl = (
|
||||
((total_eval - purchase_total) / purchase_total * 100)
|
||||
if purchase_total > 0
|
||||
else 0.0
|
||||
)
|
||||
pnl_str = f"{current_pnl:+.2f}%"
|
||||
else:
|
||||
pnl_str = "N/A"
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to get P&L: %s", exc)
|
||||
pnl_str = "N/A"
|
||||
@@ -642,7 +652,7 @@ async def run(settings: Settings) -> None:
|
||||
f"<b>Markets:</b> {markets_str}\n"
|
||||
f"<b>Trading:</b> {trading_status}\n\n"
|
||||
f"<b>Current P&L:</b> {pnl_str}\n"
|
||||
f"<b>Circuit Breaker:</b> {risk.circuit_breaker_threshold:.1f}%"
|
||||
f"<b>Circuit Breaker:</b> {risk._cb_threshold:.1f}%"
|
||||
)
|
||||
await telegram.send_message(message)
|
||||
|
||||
@@ -653,52 +663,40 @@ async def run(settings: Settings) -> None:
|
||||
)
|
||||
|
||||
async def handle_positions() -> None:
|
||||
"""Handle /positions command - show current holdings."""
|
||||
"""Handle /positions command - show account summary."""
|
||||
try:
|
||||
# Get account balance
|
||||
balance = await broker.get_balance()
|
||||
output2 = balance.get("output2", [{}])
|
||||
|
||||
# Check if there are any positions
|
||||
if not balance.stocks:
|
||||
if not output2:
|
||||
await telegram.send_message(
|
||||
"<b>💼 Current Holdings</b>\n\n"
|
||||
"No positions currently held."
|
||||
"<b>💼 Account Summary</b>\n\n"
|
||||
"No balance information available."
|
||||
)
|
||||
return
|
||||
|
||||
# Group positions by market (domestic vs overseas)
|
||||
domestic_positions = []
|
||||
overseas_positions = []
|
||||
# Extract account-level data
|
||||
total_eval = safe_float(output2[0].get("tot_evlu_amt", "0"))
|
||||
total_cash = safe_float(output2[0].get("dnca_tot_amt", "0"))
|
||||
purchase_total = safe_float(output2[0].get("pchs_amt_smtl_amt", "0"))
|
||||
|
||||
for stock in balance.stocks:
|
||||
position_str = (
|
||||
f"• {stock.code}: {stock.quantity} shares @ "
|
||||
f"{stock.avg_price:,.0f}"
|
||||
)
|
||||
|
||||
# Simple heuristic: if code is 6 digits, it's domestic (Korea)
|
||||
if len(stock.code) == 6 and stock.code.isdigit():
|
||||
domestic_positions.append(position_str)
|
||||
else:
|
||||
overseas_positions.append(position_str)
|
||||
|
||||
# Build message
|
||||
message_parts = ["<b>💼 Current Holdings</b>\n"]
|
||||
|
||||
if domestic_positions:
|
||||
message_parts.append("\n🇰🇷 <b>Korea</b>")
|
||||
message_parts.extend(domestic_positions)
|
||||
|
||||
if overseas_positions:
|
||||
message_parts.append("\n🇺🇸 <b>Overseas</b>")
|
||||
message_parts.extend(overseas_positions)
|
||||
|
||||
# Add total cash
|
||||
message_parts.append(
|
||||
f"\n<b>Cash:</b> ₩{balance.total_cash:,.0f}"
|
||||
# Calculate P&L
|
||||
pnl_pct = (
|
||||
((total_eval - purchase_total) / purchase_total * 100)
|
||||
if purchase_total > 0
|
||||
else 0.0
|
||||
)
|
||||
pnl_sign = "+" if pnl_pct >= 0 else ""
|
||||
|
||||
message = "\n".join(message_parts)
|
||||
message = (
|
||||
"<b>💼 Account Summary</b>\n\n"
|
||||
f"<b>Total Evaluation:</b> ₩{total_eval:,.0f}\n"
|
||||
f"<b>Available Cash:</b> ₩{total_cash:,.0f}\n"
|
||||
f"<b>Purchase Total:</b> ₩{purchase_total:,.0f}\n"
|
||||
f"<b>P&L:</b> {pnl_sign}{pnl_pct:.2f}%\n\n"
|
||||
"<i>Note: Individual position details require API enhancement</i>"
|
||||
)
|
||||
await telegram.send_message(message)
|
||||
|
||||
except Exception as exc:
|
||||
|
||||
@@ -200,14 +200,151 @@ 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
|
||||
|
||||
See `telegram_client.py` for full API documentation.
|
||||
|
||||
Key methods:
|
||||
### Notification Methods
|
||||
- `notify_trade_execution()` - Trade alerts
|
||||
- `notify_circuit_breaker()` - Emergency stops
|
||||
- `notify_fat_finger()` - Order rejections
|
||||
- `notify_market_open/close()` - Session tracking
|
||||
- `notify_system_start/shutdown()` - Lifecycle events
|
||||
- `notify_error()` - Error alerts
|
||||
|
||||
### Command Handler
|
||||
- `TelegramCommandHandler` - Bidirectional command processing
|
||||
- `register_command()` - Register custom command handlers
|
||||
- `start_polling()` / `stop_polling()` - Lifecycle management
|
||||
|
||||
@@ -549,7 +549,7 @@ class TestStatusCommands:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_positions_command_shows_holdings(self) -> None:
|
||||
"""Positions command displays current holdings."""
|
||||
"""Positions command displays account summary."""
|
||||
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||
handler = TelegramCommandHandler(client)
|
||||
|
||||
@@ -561,12 +561,12 @@ class TestStatusCommands:
|
||||
async def mock_positions() -> None:
|
||||
"""Mock /positions handler."""
|
||||
message = (
|
||||
"<b>💼 Current Holdings</b>\n"
|
||||
"\n🇰🇷 <b>Korea</b>\n"
|
||||
"• 005930: 10 shares @ 70,000\n"
|
||||
"\n🇺🇸 <b>Overseas</b>\n"
|
||||
"• AAPL: 15 shares @ 175\n"
|
||||
"\n<b>Cash:</b> ₩5,000,000"
|
||||
"<b>💼 Account Summary</b>\n\n"
|
||||
"<b>Total Evaluation:</b> ₩10,500,000\n"
|
||||
"<b>Available Cash:</b> ₩5,000,000\n"
|
||||
"<b>Purchase Total:</b> ₩10,000,000\n"
|
||||
"<b>P&L:</b> +5.00%\n\n"
|
||||
"<i>Note: Individual position details require API enhancement</i>"
|
||||
)
|
||||
await client.send_message(message)
|
||||
|
||||
@@ -586,8 +586,9 @@ class TestStatusCommands:
|
||||
# Verify message was sent
|
||||
assert mock_post.call_count == 1
|
||||
payload = mock_post.call_args.kwargs["json"]
|
||||
assert "Current Holdings" in payload["text"]
|
||||
assert "shares" in payload["text"]
|
||||
assert "Account Summary" in payload["text"]
|
||||
assert "Total Evaluation" in payload["text"]
|
||||
assert "P&L" in payload["text"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_positions_command_empty_holdings(self) -> None:
|
||||
@@ -603,8 +604,8 @@ class TestStatusCommands:
|
||||
async def mock_positions_empty() -> None:
|
||||
"""Mock /positions handler with no positions."""
|
||||
message = (
|
||||
"<b>💼 Current Holdings</b>\n\n"
|
||||
"No positions currently held."
|
||||
"<b>💼 Account Summary</b>\n\n"
|
||||
"No balance information available."
|
||||
)
|
||||
await client.send_message(message)
|
||||
|
||||
@@ -623,7 +624,7 @@ class TestStatusCommands:
|
||||
|
||||
# Verify message was sent
|
||||
payload = mock_post.call_args.kwargs["json"]
|
||||
assert "No positions" in payload["text"]
|
||||
assert "No balance information available" in payload["text"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_positions_command_error_handling(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user