diff --git a/src/main.py b/src/main.py
index e32701d..e372a5e 100644
--- a/src/main.py
+++ b/src/main.py
@@ -633,10 +633,101 @@ async def run(settings: Settings) -> None:
"Trading operations have been restarted."
)
+ async def handle_status() -> None:
+ """Handle /status command - show trading status."""
+ try:
+ # Get trading status
+ trading_status = "Active" if pause_trading.is_set() else "Paused"
+
+ # Get current P&L from risk manager
+ try:
+ balance = await broker.get_balance()
+ current_pnl = risk.calculate_pnl(balance)
+ pnl_str = f"{current_pnl:+.2f}%"
+ except Exception as exc:
+ logger.warning("Failed to get P&L: %s", exc)
+ pnl_str = "N/A"
+
+ # Format market list
+ markets_str = ", ".join(settings.enabled_market_list)
+
+ message = (
+ "š Trading Status\n\n"
+ f"Mode: {settings.MODE.upper()}\n"
+ f"Markets: {markets_str}\n"
+ f"Trading: {trading_status}\n\n"
+ f"Current P&L: {pnl_str}\n"
+ f"Circuit Breaker: {risk.circuit_breaker_threshold:.1f}%"
+ )
+ await telegram.send_message(message)
+
+ except Exception as exc:
+ logger.error("Error in /status handler: %s", exc)
+ await telegram.send_message(
+ "ā ļø Error\n\nFailed to retrieve trading status."
+ )
+
+ async def handle_positions() -> None:
+ """Handle /positions command - show current holdings."""
+ try:
+ # Get account balance
+ balance = await broker.get_balance()
+
+ # Check if there are any positions
+ if not balance.stocks:
+ await telegram.send_message(
+ "š¼ Current Holdings\n\n"
+ "No positions currently held."
+ )
+ return
+
+ # Group positions by market (domestic vs overseas)
+ domestic_positions = []
+ overseas_positions = []
+
+ 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 = ["š¼ Current Holdings\n"]
+
+ if domestic_positions:
+ message_parts.append("\nš°š· Korea")
+ message_parts.extend(domestic_positions)
+
+ if overseas_positions:
+ message_parts.append("\nšŗšø Overseas")
+ message_parts.extend(overseas_positions)
+
+ # Add total cash
+ message_parts.append(
+ f"\nCash: ā©{balance.total_cash:,.0f}"
+ )
+
+ message = "\n".join(message_parts)
+ await telegram.send_message(message)
+
+ except Exception as exc:
+ logger.error("Error in /positions handler: %s", exc)
+ await telegram.send_message(
+ "ā ļø Error\n\nFailed to retrieve positions."
+ )
+
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)
+ command_handler.register_command("status", handle_status)
+ command_handler.register_command("positions", handle_positions)
# Initialize volatility hunter
volatility_analyzer = VolatilityAnalyzer(min_volume_surge=2.0, min_price_change=1.0)
diff --git a/tests/test_telegram_commands.py b/tests/test_telegram_commands.py
index 978ef45..3438005 100644
--- a/tests/test_telegram_commands.py
+++ b/tests/test_telegram_commands.py
@@ -442,6 +442,199 @@ class TestTradingControlCommands:
assert "already active" in payload["text"]
+class TestStatusCommands:
+ """Test status query commands."""
+
+ @pytest.mark.asyncio
+ async def test_status_command_shows_trading_info(self) -> None:
+ """Status command displays mode, markets, and P&L."""
+ 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_status() -> None:
+ """Mock /status handler."""
+ message = (
+ "š Trading Status\n\n"
+ "Mode: PAPER\n"
+ "Markets: Korea, United States\n"
+ "Trading: Active\n\n"
+ "Current P&L: +2.50%\n"
+ "Circuit Breaker: -3.0%"
+ )
+ await client.send_message(message)
+
+ handler.register_command("status", mock_status)
+
+ with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
+ update = {
+ "update_id": 1,
+ "message": {
+ "chat": {"id": 456},
+ "text": "/status",
+ },
+ }
+
+ await handler._handle_update(update)
+
+ # Verify message was sent
+ assert mock_post.call_count == 1
+ payload = mock_post.call_args.kwargs["json"]
+ assert "Trading Status" in payload["text"]
+ assert "PAPER" in payload["text"]
+ assert "P&L" in payload["text"]
+
+ @pytest.mark.asyncio
+ async def test_status_command_error_handling(self) -> None:
+ """Status command handles errors gracefully."""
+ 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_status_error() -> None:
+ """Mock /status handler with error."""
+ await client.send_message(
+ "ā ļø Error\n\nFailed to retrieve trading status."
+ )
+
+ handler.register_command("status", mock_status_error)
+
+ with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
+ update = {
+ "update_id": 1,
+ "message": {
+ "chat": {"id": 456},
+ "text": "/status",
+ },
+ }
+
+ await handler._handle_update(update)
+
+ # Should send error message
+ payload = mock_post.call_args.kwargs["json"]
+ assert "Error" in payload["text"]
+
+ @pytest.mark.asyncio
+ async def test_positions_command_shows_holdings(self) -> None:
+ """Positions command displays current holdings."""
+ 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_positions() -> None:
+ """Mock /positions handler."""
+ message = (
+ "š¼ Current Holdings\n"
+ "\nš°š· Korea\n"
+ "⢠005930: 10 shares @ 70,000\n"
+ "\nšŗšø Overseas\n"
+ "⢠AAPL: 15 shares @ 175\n"
+ "\nCash: ā©5,000,000"
+ )
+ await client.send_message(message)
+
+ handler.register_command("positions", mock_positions)
+
+ with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
+ update = {
+ "update_id": 1,
+ "message": {
+ "chat": {"id": 456},
+ "text": "/positions",
+ },
+ }
+
+ await handler._handle_update(update)
+
+ # 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"]
+
+ @pytest.mark.asyncio
+ async def test_positions_command_empty_holdings(self) -> None:
+ """Positions command handles empty portfolio."""
+ 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_positions_empty() -> None:
+ """Mock /positions handler with no positions."""
+ message = (
+ "š¼ Current Holdings\n\n"
+ "No positions currently held."
+ )
+ await client.send_message(message)
+
+ handler.register_command("positions", mock_positions_empty)
+
+ with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
+ update = {
+ "update_id": 1,
+ "message": {
+ "chat": {"id": 456},
+ "text": "/positions",
+ },
+ }
+
+ await handler._handle_update(update)
+
+ # Verify message was sent
+ payload = mock_post.call_args.kwargs["json"]
+ assert "No positions" in payload["text"]
+
+ @pytest.mark.asyncio
+ async def test_positions_command_error_handling(self) -> None:
+ """Positions command handles errors gracefully."""
+ 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_positions_error() -> None:
+ """Mock /positions handler with error."""
+ await client.send_message(
+ "ā ļø Error\n\nFailed to retrieve positions."
+ )
+
+ handler.register_command("positions", mock_positions_error)
+
+ with patch("aiohttp.ClientSession.post", return_value=mock_resp) as mock_post:
+ update = {
+ "update_id": 1,
+ "message": {
+ "chat": {"id": 456},
+ "text": "/positions",
+ },
+ }
+
+ await handler._handle_update(update)
+
+ # Should send error message
+ payload = mock_post.call_args.kwargs["json"]
+ assert "Error" in payload["text"]
+
+
class TestBasicCommands:
"""Test basic command implementations."""