Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 105 additions & 3 deletions openbb_ibkr/ibkr_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -1526,7 +1526,38 @@ def configure(
)


@router.command(methods=["GET"])
@router.command(
methods=["GET"],
widget_config={
"name": "IBKR Account Summary",
"description": "Account tags: NetLiquidation, TotalCashValue, BuyingPower, margins, and more.",
"type": "table",
"source": ["IBKR"],
"category": "IBKR",
"subCategory": "Portfolio",
"widgetId": "ibkr_account_summary_custom_obb",
"gridData": {"w": 40, "h": 12},
"data": {
"table": {
"showAll": True,
"enableAdvanced": True,
"enableCharts": True,
"chartView": {
"enabled": False,
"chartType": "bar",
"cellRangeCols": {"bar": ["tag", "value"]},
"ignoreCellRange": True,
},
"columnsDefs": [
_column("tag", "Tag", "text", "none", "category", "left"),
_column("value", "Value", "text", chart_data_type="series"),
_column("currency", "Currency", "text", chart_data_type="category"),
_column("account", "Account", "text", chart_data_type="excluded"),
],
}
},
},
)
def account_summary(
host: Optional[str] = None,
port: Optional[str] = None,
Expand Down Expand Up @@ -1556,7 +1587,42 @@ def account_summary(
return OBBject(results=results)


@router.command(methods=["GET"])
@router.command(
methods=["GET"],
widget_config={
"name": "IBKR Margin Summary",
"description": "Margin requirements: init/maint margin, available funds, excess liquidity, cushion.",
"type": "table",
"source": ["IBKR"],
"category": "IBKR",
"subCategory": "Portfolio",
"widgetId": "ibkr_margin_summary_custom_obb",
"gridData": {"w": 40, "h": 10},
"data": {
"table": {
"showAll": True,
"enableAdvanced": True,
"enableCharts": True,
"chartView": {
"enabled": False,
"chartType": "bar",
"cellRangeCols": {"bar": ["account", "init_margin_req", "maint_margin_req", "available_funds"]},
"ignoreCellRange": True,
},
"columnsDefs": [
_column("account", "Account", "text", "none", "category", "left"),
_column("currency", "Currency", "text", chart_data_type="excluded"),
_column("init_margin_req", "Init Margin", "number", chart_data_type="series"),
_column("maint_margin_req", "Maint Margin", "number", chart_data_type="series"),
_column("available_funds", "Available Funds", "number", chart_data_type="series"),
_column("excess_liquidity", "Excess Liquidity", "number", chart_data_type="series"),
_column("cushion", "Cushion", "number", "normalizedPercent", "series"),
_column("sma", "SMA", "number", chart_data_type="series"),
],
}
},
},
)
def margin_summary(
host: Optional[str] = None,
port: Optional[str] = None,
Expand Down Expand Up @@ -1612,7 +1678,43 @@ def account_values(
return OBBject(results=values)


@router.command(methods=["GET"])
@router.command(
methods=["GET"],
widget_config={
"name": "IBKR Positions",
"description": "Portfolio positions with market value, cost basis, and P&L.",
"type": "table",
"source": ["IBKR"],
"category": "IBKR",
"subCategory": "Portfolio",
"widgetId": "ibkr_positions_custom_obb",
"gridData": {"w": 40, "h": 15},
"data": {
"table": {
"showAll": True,
"enableAdvanced": True,
"enableCharts": True,
"chartView": {
"enabled": False,
"chartType": "treemap",
"cellRangeCols": {"treemap": ["symbol", "market_value"]},
"ignoreCellRange": True,
},
"columnsDefs": [
_column("symbol", "Symbol", "text", "none", "category", "left"),
_column("sec_type", "Type", "text", chart_data_type="excluded"),
_column("position", "Position", "number", chart_data_type="series"),
_column("market_price", "Price", "number", chart_data_type="series"),
_column("market_value", "Market Value", "number", chart_data_type="series"),
_column("average_cost", "Avg Cost", "number", chart_data_type="series"),
_column("unrealized_pnl", "Unrealized P&L", "number", chart_data_type="series"),
_column("realized_pnl", "Realized P&L", "number", chart_data_type="series"),
_column("currency", "Currency", "text", chart_data_type="excluded"),
],
}
},
},
)
def positions(
host: Optional[str] = None,
port: Optional[str] = None,
Expand Down
46 changes: 46 additions & 0 deletions tests/test_widget_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Tests for widget_config presence on core portfolio endpoints."""

import ast
import re
from pathlib import Path

ROUTER_PATH = Path(__file__).resolve().parent.parent / "openbb_ibkr" / "ibkr_router.py"

EXPECTED_WIDGET_CONFIGS = {
"account_summary": "ibkr_account_summary_custom_obb",
"positions": "ibkr_positions_custom_obb",
"margin_summary": "ibkr_margin_summary_custom_obb",
}


def test_router_parses_with_widget_configs():
"""Router file is syntactically valid after widget_config additions."""
ast.parse(ROUTER_PATH.read_text())


def test_widget_ids_present_in_source():
"""All expected widgetId strings exist in the router source."""
content = ROUTER_PATH.read_text()
for endpoint, widget_id in EXPECTED_WIDGET_CONFIGS.items():
assert widget_id in content, f"widgetId '{widget_id}' not found for {endpoint}"


def test_widget_config_has_columns_defs():
"""Each core endpoint's widget_config contains columnsDefs."""
content = ROUTER_PATH.read_text()
for endpoint, widget_id in EXPECTED_WIDGET_CONFIGS.items():
idx = content.find(f'"{widget_id}"')
assert idx != -1, f"widgetId '{widget_id}' not found"
# columnsDefs appears after widgetId in the same widget_config block
block = content[max(0, idx - 500):idx + 2000]
assert "columnsDefs" in block, f"columnsDefs missing near {widget_id}"


def test_widget_config_has_required_keys():
"""Each core widget_config contains name, type, category, subCategory."""
content = ROUTER_PATH.read_text()
for endpoint, widget_id in EXPECTED_WIDGET_CONFIGS.items():
idx = content.find(f'"{widget_id}"')
block = content[max(0, idx - 1500):idx + 500]
for key in ['"name"', '"type"', '"category"', '"subCategory"']:
assert key in block, f"{key} missing near {widget_id}"
Loading