Skip to content
Open
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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Summary

Version - 0.1.0 - This is hobbyist client side agentic trading package (builds with the [allocation-manager](https://github.com/OptimChain/allocation-manager/) service).
**Version 1.0.0** — hobbyist client-side agentic trading package (builds with the [allocation-manager](https://github.com/OptimChain/allocation-manager/) service).

1. **Install dependencies**:
```bash
Expand Down Expand Up @@ -32,6 +32,7 @@ python -m trading_system.main --live
robinhood-trading/
├── trading_system/
│ ├── main.py # 30-day breakout strategy
│ ├── brokers/ # MockIbkrClient (offline); IbTwsClient (ib_insync → TWS API)
│ ├── data_providers/ # Market data (Twelve Data API)
│ ├── strategies/ # Trading strategies
│ ├── state/ # State management
Expand All @@ -48,6 +49,14 @@ robinhood-trading/

For detailed structure, see [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)

### Interactive Brokers (TWS API)

Enable **API** in TWS: *File → Global Configuration → API* (socket clients, port **7496** paper / **7497** live). Then use `IbTwsClient` from `trading_system.brokers` (requires `ib_insync` in `requirements.txt`).

- **Unit tests (mocked, no TWS):** `python -m pytest tests/brokers/test_ib_tws_client_mocked.py -v`
- **Live read-only (paper, TWS running):** `TWS_INTEGRATION=1 python -m pytest tests/brokers/test_ib_tws_live.py -v`
- **Live submit + cancel a test limit (paper only):** also set `TWS_PLACE_ORDER=1` and use port 7496 (the suite refuses 7497).

**30-Day Breakout Strategy**:
- Buy when price hits 30-day low
- Sell when price hits 30-day high
Expand Down
Binary file added docs/pr-55-attachments/ibkr-tws-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/pr-55-attachments/ibkr-tws-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "allocation-engine-api",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"description": "Swagger API endpoints for allocation engine trading system",
"dependencies": {
Expand Down
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[pytest]
# Exclude scripts directory from test collection
testpaths = tests
markers =
integration: live TWS / external (set TWS_INTEGRATION=1)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ requests>=2.31.0
pandas>=2.0.0
matplotlib>=3.7.0
fpdf2>=2.7.0
ib_insync>=0.9.86

# Optional: Execution quality cross-referencing
# alpaca-py>=0.30.0
Expand Down
79 changes: 79 additions & 0 deletions tests/brokers/test_ib_tws_client_mocked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Unit tests for :class:`IbTwsClient` with ``ib_insync`` mocked (no TWS process).
"""

from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest

from trading_system.brokers.ib_tws_client import IbTwsClient, TwsClientConfig


@pytest.fixture
def mock_ib() -> MagicMock:
ib = MagicMock()
ib.isConnected.return_value = True
return ib


@patch("trading_system.brokers.ib_tws_client.IB")
def test_connect_and_read_account_values(mock_ib_class, mock_ib: MagicMock) -> None:
av = SimpleNamespace(tag="NetLiquidation", value="100000.00", currency="USD", account="PAPER1")
mock_ib.accountValues.return_value = [av]
mock_ib_class.return_value = mock_ib

c = IbTwsClient(TwsClientConfig(port=7496, client_id=2))
assert c.connect() is True
assert c.is_connected
rows = c.get_account_values()
assert len(rows) == 1
assert rows[0]["tag"] == "NetLiquidation"
assert rows[0]["value"] == "100000.00"
m = c.get_account_summary_map()
assert m["NetLiquidation"] == "100000.00"
c.disconnect()
assert not c.is_connected
mock_ib.disconnect.assert_called()


@patch("trading_system.brokers.ib_tws_client.IB")
def test_get_positions(mock_ib_class, mock_ib: MagicMock) -> None:
pos = SimpleNamespace(
position=10.0,
avgCost=50.0,
contract=SimpleNamespace(symbol="SPY", conId=123, exchange="SMART", currency="USD"),
)
mock_ib.positions.return_value = [pos]
mock_ib_class.return_value = mock_ib

c = IbTwsClient()
c.connect()
out = c.get_positions()
assert len(out) == 1
assert out[0]["symbol"] == "SPY"
assert out[0]["position"] == 10.0
assert out[0]["avgCost"] == 50.0
c.disconnect()


@patch("trading_system.brokers.ib_tws_client.IB")
def test_write_limit_and_cancel(mock_ib_class, mock_ib: MagicMock) -> None:
"""Place order returns order id; cancel finds it in openTrades."""
order = MagicMock()
order.orderId = 42
trade = MagicMock()
trade.order = order
trade.contract = MagicMock()
mock_ib.placeOrder.return_value = trade
mock_ib.openTrades.return_value = [trade]
mock_ib_class.return_value = mock_ib

c = IbTwsClient()
c.connect()
oid = c.place_limit_order("SPY", "BUY", 1, 1.0)
assert oid == 42
mock_ib.qualifyContracts.assert_called_once()
assert c.cancel_order(42) is True
mock_ib.cancelOrder.assert_called_once()
c.disconnect()
69 changes: 69 additions & 0 deletions tests/brokers/test_ib_tws_live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Optional live TWS integration (paper by default on port 7496).

Set environment variable ``TWS_INTEGRATION=1`` to run. Read-only tests are always
safe. Set ``TWS_PLACE_ORDER=1`` only on **paper** to submit and cancel a silly
limit order (verify you are not on live).

Example::

TWS_INTEGRATION=1 python3 -m pytest tests/brokers/test_ib_tws_live.py -v
TWS_INTEGRATION=1 TWS_PLACE_ORDER=1 python3 -m pytest tests/brokers/test_ib_tws_live.py -v -k place
"""

from __future__ import annotations

import os

import pytest

from trading_system.brokers.ib_tws_client import IbTwsClient, TwsClientConfig


pytestmark = pytest.mark.integration


def _skip_if_disabled() -> None:
if os.environ.get("TWS_INTEGRATION") != "1":
pytest.skip("set TWS_INTEGRATION=1 and run TWS with API on 7496 (paper)")


def test_live_account_and_positions() -> None:
_skip_if_disabled()
port = int(os.environ.get("TWS_PORT", "7496"))
client_id = int(os.environ.get("TWS_CLIENT_ID", "7"))
c = IbTwsClient(TwsClientConfig(port=port, client_id=client_id))
assert c.connect() is True
try:
vals = c.get_account_values()
assert isinstance(vals, list)
if vals:
assert "tag" in vals[0]
sm = c.get_account_summary_map()
assert isinstance(sm, dict)
pos = c.get_positions()
assert isinstance(pos, list)
finally:
c.disconnect()


def test_live_place_limit_and_cancel_spy() -> None:
"""Submit a deep OTM limit that should not fill, then cancel."""
_skip_if_disabled()
if os.environ.get("TWS_PLACE_ORDER") != "1":
pytest.skip("set TWS_PLACE_ORDER=1 to test order submit+cancel (paper only)")

port = int(os.environ.get("TWS_PORT", "7496"))
if port == 7497:
pytest.fail("Refusing to place test order on live port 7497; use paper 7496")

client_id = int(os.environ.get("TWS_CLIENT_ID", "8"))
c = IbTwsClient(TwsClientConfig(port=port, client_id=client_id))
assert c.connect() is True
try:
# Far below market — still not risk-free; paper only
oid = c.place_limit_order("SPY", "BUY", 1, 1.0)
assert oid > 0
assert c.cancel_order(oid) is True
finally:
c.disconnect()
62 changes: 62 additions & 0 deletions tests/brokers/test_ibkr_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Tests for the in-memory IBKR-style mock client."""

from trading_system.brokers.ibkr_mock import MockIbkrClient


def test_connect_disconnect_and_account_tags():
c = MockIbkrClient()
assert c.connected is False
assert c.connect() is True
assert c.connected is True
summary = c.get_account_summary()
assert "NetLiquidation" in summary
assert "TotalCashValue" in summary
assert float(summary["NetLiquidation"]) == 1_000_000.0
c.disconnect()
assert c.connected is False


def test_market_buy_reduces_cash_and_creates_position():
c = MockIbkrClient(initial_cash=100_000.0, last_prices={"SPY": 400.0})
c.connect()
r = c.place_market_order("SPY", "BUY", 10)
assert r["orderType"] == "MKT"
assert r["status"] == "Filled"
pos = c.get_positions()
assert len(pos) == 1
assert pos[0]["symbol"] == "SPY"
assert pos[0]["position"] == 10.0
assert c.get_open_orders() == []


def test_market_sell_rejected_without_position():
c = MockIbkrClient(last_prices={"SPY": 100.0})
c.connect()
r = c.place_market_order("SPY", "SELL", 5)
assert r["status"] == "Cancelled"


def test_last_price_setter():
c = MockIbkrClient(last_prices={})
c.set_last_price("AAPL", 199.0)
assert c.get_last_price("aapl") == 199.0


def test_limit_order_stays_open():
c = MockIbkrClient()
c.connect()
r = c.place_limit_order("SPY", "BUY", 1, 100.0)
assert r["orderType"] == "LMT"
assert r["status"] == "PreSubmitted"
open_o = c.get_open_orders()
assert len(open_o) == 1
assert c.cancel_order(r["orderId"]) is True
assert c.get_open_orders() == []


def test_no_auto_fill_when_disabled():
c = MockIbkrClient(auto_fill_market=False, last_prices={"QQQ": 100.0})
c.connect()
r = c.place_market_order("QQQ", "BUY", 1)
assert r["status"] == "Submitted"
assert c.get_open_orders()
4 changes: 4 additions & 0 deletions trading_system/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
Trading System Package
A clean, modular trading system with market data integration and breakout strategies.
"""

from trading_system.version import __version__

__all__ = ["__version__"]
22 changes: 22 additions & 0 deletions trading_system/brokers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Broker adapters (mock and future live clients)."""

from __future__ import annotations

import typing

from trading_system.brokers.ibkr_mock import MockIbkrClient

# Lazy: importing ib_tws_client pulls ib_insync (heavy); keep MockIbkrClient import cheap.
if typing.TYPE_CHECKING:
from trading_system.brokers.ib_tws_client import IbTwsClient as IbTwsClient
from trading_system.brokers.ib_tws_client import TwsClientConfig as TwsClientConfig

__all__ = ["IbTwsClient", "TwsClientConfig", "MockIbkrClient"]


def __getattr__(name: str):
if name in ("IbTwsClient", "TwsClientConfig"):
from trading_system.brokers import ib_tws_client as _tws

return getattr(_tws, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Loading
Loading