Compare commits
4 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a098d9a6 | ||
| d2b07326ed | |||
|
|
1c5eadc23b | ||
| 10ff718045 |
92
src/main.py
92
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"
|
||||||
@@ -639,11 +624,21 @@ async def run(settings: Settings) -> None:
|
|||||||
# Get trading status
|
# Get trading status
|
||||||
trading_status = "Active" if pause_trading.is_set() else "Paused"
|
trading_status = "Active" if pause_trading.is_set() else "Paused"
|
||||||
|
|
||||||
# Get current P&L from risk manager
|
# Calculate P&L from balance data
|
||||||
try:
|
try:
|
||||||
balance = await broker.get_balance()
|
balance = await broker.get_balance()
|
||||||
current_pnl = risk.calculate_pnl(balance)
|
output2 = balance.get("output2", [{}])
|
||||||
pnl_str = f"{current_pnl:+.2f}%"
|
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:
|
except Exception as exc:
|
||||||
logger.warning("Failed to get P&L: %s", exc)
|
logger.warning("Failed to get P&L: %s", exc)
|
||||||
pnl_str = "N/A"
|
pnl_str = "N/A"
|
||||||
@@ -657,7 +652,7 @@ async def run(settings: Settings) -> None:
|
|||||||
f"<b>Markets:</b> {markets_str}\n"
|
f"<b>Markets:</b> {markets_str}\n"
|
||||||
f"<b>Trading:</b> {trading_status}\n\n"
|
f"<b>Trading:</b> {trading_status}\n\n"
|
||||||
f"<b>Current P&L:</b> {pnl_str}\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)
|
await telegram.send_message(message)
|
||||||
|
|
||||||
@@ -668,52 +663,40 @@ async def run(settings: Settings) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def handle_positions() -> None:
|
async def handle_positions() -> None:
|
||||||
"""Handle /positions command - show current holdings."""
|
"""Handle /positions command - show account summary."""
|
||||||
try:
|
try:
|
||||||
# Get account balance
|
# Get account balance
|
||||||
balance = await broker.get_balance()
|
balance = await broker.get_balance()
|
||||||
|
output2 = balance.get("output2", [{}])
|
||||||
|
|
||||||
# Check if there are any positions
|
if not output2:
|
||||||
if not balance.stocks:
|
|
||||||
await telegram.send_message(
|
await telegram.send_message(
|
||||||
"<b>💼 Current Holdings</b>\n\n"
|
"<b>💼 Account Summary</b>\n\n"
|
||||||
"No positions currently held."
|
"No balance information available."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Group positions by market (domestic vs overseas)
|
# Extract account-level data
|
||||||
domestic_positions = []
|
total_eval = safe_float(output2[0].get("tot_evlu_amt", "0"))
|
||||||
overseas_positions = []
|
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:
|
# Calculate P&L
|
||||||
position_str = (
|
pnl_pct = (
|
||||||
f"• {stock.code}: {stock.quantity} shares @ "
|
((total_eval - purchase_total) / purchase_total * 100)
|
||||||
f"{stock.avg_price:,.0f}"
|
if purchase_total > 0
|
||||||
)
|
else 0.0
|
||||||
|
|
||||||
# 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}"
|
|
||||||
)
|
)
|
||||||
|
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)
|
await telegram.send_message(message)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -722,7 +705,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)
|
||||||
|
|||||||
@@ -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."""
|
||||||
@@ -524,7 +549,7 @@ class TestStatusCommands:
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_positions_command_shows_holdings(self) -> None:
|
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)
|
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||||
handler = TelegramCommandHandler(client)
|
handler = TelegramCommandHandler(client)
|
||||||
|
|
||||||
@@ -536,12 +561,12 @@ class TestStatusCommands:
|
|||||||
async def mock_positions() -> None:
|
async def mock_positions() -> None:
|
||||||
"""Mock /positions handler."""
|
"""Mock /positions handler."""
|
||||||
message = (
|
message = (
|
||||||
"<b>💼 Current Holdings</b>\n"
|
"<b>💼 Account Summary</b>\n\n"
|
||||||
"\n🇰🇷 <b>Korea</b>\n"
|
"<b>Total Evaluation:</b> ₩10,500,000\n"
|
||||||
"• 005930: 10 shares @ 70,000\n"
|
"<b>Available Cash:</b> ₩5,000,000\n"
|
||||||
"\n🇺🇸 <b>Overseas</b>\n"
|
"<b>Purchase Total:</b> ₩10,000,000\n"
|
||||||
"• AAPL: 15 shares @ 175\n"
|
"<b>P&L:</b> +5.00%\n\n"
|
||||||
"\n<b>Cash:</b> ₩5,000,000"
|
"<i>Note: Individual position details require API enhancement</i>"
|
||||||
)
|
)
|
||||||
await client.send_message(message)
|
await client.send_message(message)
|
||||||
|
|
||||||
@@ -561,8 +586,9 @@ class TestStatusCommands:
|
|||||||
# Verify message was sent
|
# Verify message was sent
|
||||||
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 "Current Holdings" in payload["text"]
|
assert "Account Summary" in payload["text"]
|
||||||
assert "shares" in payload["text"]
|
assert "Total Evaluation" in payload["text"]
|
||||||
|
assert "P&L" in payload["text"]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_positions_command_empty_holdings(self) -> None:
|
async def test_positions_command_empty_holdings(self) -> None:
|
||||||
@@ -578,8 +604,8 @@ class TestStatusCommands:
|
|||||||
async def mock_positions_empty() -> None:
|
async def mock_positions_empty() -> None:
|
||||||
"""Mock /positions handler with no positions."""
|
"""Mock /positions handler with no positions."""
|
||||||
message = (
|
message = (
|
||||||
"<b>💼 Current Holdings</b>\n\n"
|
"<b>💼 Account Summary</b>\n\n"
|
||||||
"No positions currently held."
|
"No balance information available."
|
||||||
)
|
)
|
||||||
await client.send_message(message)
|
await client.send_message(message)
|
||||||
|
|
||||||
@@ -598,7 +624,7 @@ class TestStatusCommands:
|
|||||||
|
|
||||||
# Verify message was sent
|
# Verify message was sent
|
||||||
payload = mock_post.call_args.kwargs["json"]
|
payload = mock_post.call_args.kwargs["json"]
|
||||||
assert "No positions" in payload["text"]
|
assert "No balance information available" in payload["text"]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_positions_command_error_handling(self) -> None:
|
async def test_positions_command_error_handling(self) -> None:
|
||||||
@@ -638,51 +664,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 +679,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 +704,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