Compare commits
2 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57a45a24cb | ||
| a7696568cc |
91
src/main.py
91
src/main.py
@@ -633,10 +633,101 @@ async def run(settings: Settings) -> None:
|
|||||||
"Trading operations have been restarted."
|
"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 = (
|
||||||
|
"<b>📊 Trading Status</b>\n\n"
|
||||||
|
f"<b>Mode:</b> {settings.MODE.upper()}\n"
|
||||||
|
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}%"
|
||||||
|
)
|
||||||
|
await telegram.send_message(message)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Error in /status handler: %s", exc)
|
||||||
|
await telegram.send_message(
|
||||||
|
"<b>⚠️ Error</b>\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(
|
||||||
|
"<b>💼 Current Holdings</b>\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 = ["<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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
"<b>⚠️ Error</b>\n\nFailed to retrieve positions."
|
||||||
|
)
|
||||||
|
|
||||||
command_handler.register_command("start", handle_start)
|
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)
|
||||||
|
command_handler.register_command("status", handle_status)
|
||||||
|
command_handler.register_command("positions", handle_positions)
|
||||||
|
|
||||||
# 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)
|
||||||
|
|||||||
@@ -442,6 +442,199 @@ class TestTradingControlCommands:
|
|||||||
assert "already active" in payload["text"]
|
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 = (
|
||||||
|
"<b>📊 Trading Status</b>\n\n"
|
||||||
|
"<b>Mode:</b> PAPER\n"
|
||||||
|
"<b>Markets:</b> Korea, United States\n"
|
||||||
|
"<b>Trading:</b> Active\n\n"
|
||||||
|
"<b>Current P&L:</b> +2.50%\n"
|
||||||
|
"<b>Circuit Breaker:</b> -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(
|
||||||
|
"<b>⚠️ Error</b>\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 = (
|
||||||
|
"<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"
|
||||||
|
)
|
||||||
|
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 = (
|
||||||
|
"<b>💼 Current Holdings</b>\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(
|
||||||
|
"<b>⚠️ Error</b>\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:
|
class TestBasicCommands:
|
||||||
"""Test basic command implementations."""
|
"""Test basic command implementations."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user