Merge pull request 'fix: resolve Telegram command handler errors for /status and /positions (issue #74)' (#75) from feature/issue-74-telegram-command-fix into main
Some checks failed
CI / test (push) Has been cancelled

Reviewed-on: #75
This commit was merged in pull request #75.
This commit is contained in:
2026-02-05 18:56:24 +09:00
2 changed files with 50 additions and 51 deletions

View File

@@ -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)
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}"
# 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 ""
# 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 = (
"<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>"
)
message = "\n".join(message_parts)
await telegram.send_message(message)
except Exception as exc:

View File

@@ -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: