diff --git a/README.md b/README.md index 8685ad6..6d9e600 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/docs/pr-55-attachments/ibkr-tws-1.png b/docs/pr-55-attachments/ibkr-tws-1.png new file mode 100644 index 0000000..493595d Binary files /dev/null and b/docs/pr-55-attachments/ibkr-tws-1.png differ diff --git a/docs/pr-55-attachments/ibkr-tws-2.png b/docs/pr-55-attachments/ibkr-tws-2.png new file mode 100644 index 0000000..c2ad33e Binary files /dev/null and b/docs/pr-55-attachments/ibkr-tws-2.png differ diff --git a/package.json b/package.json index a2361d1..fc8dfdb 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pytest.ini b/pytest.ini index b767c39..cefecfc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,5 @@ [pytest] # Exclude scripts directory from test collection testpaths = tests +markers = + integration: live TWS / external (set TWS_INTEGRATION=1) diff --git a/requirements.txt b/requirements.txt index 6bf89c2..90382ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/brokers/test_ib_tws_client_mocked.py b/tests/brokers/test_ib_tws_client_mocked.py new file mode 100644 index 0000000..6018952 --- /dev/null +++ b/tests/brokers/test_ib_tws_client_mocked.py @@ -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() diff --git a/tests/brokers/test_ib_tws_live.py b/tests/brokers/test_ib_tws_live.py new file mode 100644 index 0000000..73f94c4 --- /dev/null +++ b/tests/brokers/test_ib_tws_live.py @@ -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() diff --git a/tests/brokers/test_ibkr_mock.py b/tests/brokers/test_ibkr_mock.py new file mode 100644 index 0000000..40b827f --- /dev/null +++ b/tests/brokers/test_ibkr_mock.py @@ -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() diff --git a/trading_system/__init__.py b/trading_system/__init__.py index 1e8ccd9..7a685e0 100644 --- a/trading_system/__init__.py +++ b/trading_system/__init__.py @@ -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__"] diff --git a/trading_system/brokers/__init__.py b/trading_system/brokers/__init__.py new file mode 100644 index 0000000..dd43913 --- /dev/null +++ b/trading_system/brokers/__init__.py @@ -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}") diff --git a/trading_system/brokers/ib_tws_client.py b/trading_system/brokers/ib_tws_client.py new file mode 100644 index 0000000..634765a --- /dev/null +++ b/trading_system/brokers/ib_tws_client.py @@ -0,0 +1,156 @@ +""" +Live Interactive Brokers TWS / IB Gateway client via **ib_insync** + +Requires TWS or Gateway with **API** enabled: ``127.0.0.1``, port **7496** (paper) or +**7497** (live). This module does not start TWS; it only opens a socket. + +Use :class:`MockIbkrClient` for offline unit tests; use this class for real read/write +against a running session. +""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional + +# ib_insync / eventkit expect a main-thread event loop; Python 3.10+ may not +# create one on import, which breaks their module init. +def _ensure_event_loop() -> None: + try: + asyncio.get_running_loop() + except RuntimeError: + try: + asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + +_ensure_event_loop() + +try: + from ib_insync import IB, LimitOrder, Stock, Trade +except ImportError as e: # pragma: no cover + raise ImportError( + "ib_insync is required for IbTwsClient. Install with: pip install ib_insync" + ) from e + +Action = Literal["BUY", "SELL"] + + +@dataclass +class TwsClientConfig: + host: str = "127.0.0.1" + port: int = 7496 # paper; use 7497 for live + client_id: int = 1 + """Must be unique per concurrent connection to the same TWS.""" + + +class IbTwsClient: + """ + Thin sync wrapper: account values, positions, limit orders, cancel by order id. + Call :meth:`connect` before other methods, :meth:`disconnect` when done. + """ + + def __init__(self, config: Optional[TwsClientConfig] = None) -> None: + self._cfg = config or TwsClientConfig() + self._ib: Optional[IB] = None + + def connect(self) -> bool: + """Connect to TWS. Returns True on success.""" + if self._ib is not None and self._ib.isConnected(): + return True + ib = IB() + ib.connect( + self._cfg.host, + self._cfg.port, + clientId=self._cfg.client_id, + ) + self._ib = ib + return bool(ib.isConnected()) + + def disconnect(self) -> None: + if self._ib is not None and self._ib.isConnected(): + self._ib.disconnect() + self._ib = None + + @property + def is_connected(self) -> bool: + return self._ib is not None and self._ib.isConnected() + + def get_account_values(self) -> List[Dict[str, str]]: + """``reqAccountSummary``-style rows as plain dicts (tag, value, currency, account).""" + if not self._ib or not self._ib.isConnected(): + raise RuntimeError("not connected; call connect() first") + out: List[Dict[str, str]] = [] + for av in self._ib.accountValues(): + out.append( + { + "tag": av.tag, + "value": av.value, + "currency": av.currency, + "account": av.account, + } + ) + return out + + def get_account_summary_map(self) -> Dict[str, str]: + """ + Map common TWS tags (e.g. ``NetLiquidation``) to value strings, first currency match. + """ + m: Dict[str, str] = {} + for row in self.get_account_values(): + if row.get("tag") and row.get("value") is not None and row.get("tag") not in m: + m[str(row["tag"])] = str(row["value"]) + return m + + def get_positions(self) -> List[Dict[str, Any]]: + """Open positions (symbol, position, avgCost, conId, exchange).""" + if not self._ib or not self._ib.isConnected(): + raise RuntimeError("not connected; call connect() first") + out: List[Dict[str, Any]] = [] + for p in self._ib.positions(): + c = p.contract + out.append( + { + "symbol": getattr(c, "symbol", "") or str(c), + "position": float(p.position), + "avgCost": float(p.avgCost) if p.avgCost else 0.0, + "conId": int(getattr(c, "conId", 0) or 0), + "exchange": getattr(c, "exchange", "") or "", + "currency": getattr(c, "currency", "") or "", + } + ) + return out + + def place_limit_order( + self, + symbol: str, + action: Action, + quantity: float, + limit_price: float, + exchange: str = "SMART", + currency: str = "USD", + ) -> int: + """ + Submit a stock limit order (US equity via SMART by default). Returns **orderId**. + For paper, use a price safely away from market if you will cancel, or you risk a fill. + """ + if not self._ib or not self._ib.isConnected(): + raise RuntimeError("not connected; call connect() first") + contract = Stock(symbol, exchange, currency) + self._ib.qualifyContracts(contract) + order = LimitOrder(str(action), float(quantity), float(limit_price)) + trade: Trade = self._ib.placeOrder(contract, order) + return int(trade.order.orderId) + + def cancel_order(self, order_id: int) -> bool: + """Cancel a working order by id (scans :meth:`ib.openTrades`).""" + if not self._ib or not self._ib.isConnected(): + raise RuntimeError("not connected; call connect() first") + for t in self._ib.openTrades(): + if t.order.orderId == order_id: + self._ib.cancelOrder(t.contract, t.order) + return True + return False diff --git a/trading_system/brokers/ibkr_mock.py b/trading_system/brokers/ibkr_mock.py new file mode 100644 index 0000000..7996828 --- /dev/null +++ b/trading_system/brokers/ibkr_mock.py @@ -0,0 +1,231 @@ +""" +In-memory Interactive Brokers–style client for tests and local simulation. + +Mimics common TWS / IB Gateway account tags and order handles without ibapi, +sockets, or credentials. Not for production trading. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Any, Dict, List, Literal, Optional + + +Action = Literal["BUY", "SELL"] + + +@dataclass +class _PositionState: + quantity: float + avg_cost: float + + +def _utc_now_iso() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat() + + +class MockIbkrClient: + """ + Stand-in for IBKR account queries and order submission. + + - Account tags use the same names many TWS API examples use (string values). + - Market orders optionally auto-fill at ``last_prices[symbol]`` (default on). + - Cash and positions update in-memory only. + """ + + def __init__( + self, + *, + initial_cash: float = 1_000_000.0, + last_prices: Optional[Dict[str, float]] = None, + auto_fill_market: bool = True, + ) -> None: + self._connected = False + self._cash = float(initial_cash) + self._positions: Dict[str, _PositionState] = {} + self._last_prices: Dict[str, float] = dict(last_prices or {"SPY": 450.0, "QQQ": 380.0}) + self._orders: Dict[int, Dict[str, Any]] = {} + self._next_order_id = 1 + self._auto_fill_market = auto_fill_market + + # --- connection (no-op for mock; matches typical connect/disconnect flow) --- + + def connect(self, host: str = "127.0.0.1", port: int = 7497, client_id: int = 1) -> bool: + """Pretend to open a TWS session. Args are accepted for API similarity only.""" + _ = (host, port, client_id) + self._connected = True + return True + + def disconnect(self) -> None: + self._connected = False + + @property + def connected(self) -> bool: + return self._connected + + # --- quotes (test hooks) --- + + def set_last_price(self, symbol: str, price: float) -> None: + """Seed or update the last trade price used for marks and market fills.""" + self._last_prices[symbol.upper()] = float(price) + + def get_last_price(self, symbol: str) -> Optional[float]: + return self._last_prices.get(symbol.upper()) + + # --- account / portfolio (IB-style tag -> value strings) --- + + def get_account_summary(self) -> Dict[str, str]: + """Return selected account values in the style of ``reqAccountSummary`` tags.""" + nl = self._net_liquidation() + return { + "NetLiquidation": f"{nl:.2f}", + "TotalCashValue": f"{self._cash:.2f}", + "BuyingPower": f"{self._cash:.2f}", + "GrossPositionValue": f"{self._gross_position_value():.2f}", + } + + def get_positions(self) -> List[Dict[str, Any]]: + """One row per symbol, similar to ``position()`` callback fields.""" + rows: List[Dict[str, Any]] = [] + for sym, st in self._positions.items(): + px = self._mark_price(sym) + mkt = st.quantity * px + rows.append( + { + "symbol": sym, + "position": st.quantity, + "avgCost": st.avg_cost, + "marketValue": mkt, + "unrealizedPNL": mkt - st.quantity * st.avg_cost, + } + ) + return rows + + # --- orders --- + + def place_market_order(self, symbol: str, action: Action, quantity: float) -> Dict[str, Any]: + """Submit a MKT order; may auto-fill if ``auto_fill_market`` is True.""" + oid = self._next_id() + sym = symbol.upper() + rec: Dict[str, Any] = { + "orderId": oid, + "symbol": sym, + "action": action, + "orderType": "MKT", + "totalQuantity": quantity, + "status": "Submitted", + "submittedAt": _utc_now_iso(), + } + self._orders[oid] = rec + if self._auto_fill_market: + self._fill_market(oid, sym, action, quantity) + return dict(rec) + + def place_limit_order( + self, + symbol: str, + action: Action, + quantity: float, + limit_price: float, + ) -> Dict[str, Any]: + """Submit an LMT order (left open until cancelled or you extend the mock).""" + oid = self._next_id() + sym = symbol.upper() + rec: Dict[str, Any] = { + "orderId": oid, + "symbol": sym, + "action": action, + "orderType": "LMT", + "lmtPrice": float(limit_price), + "totalQuantity": quantity, + "status": "PreSubmitted", + "submittedAt": _utc_now_iso(), + } + self._orders[oid] = rec + return rec + + def cancel_order(self, order_id: int) -> bool: + rec = self._orders.get(order_id) + if not rec: + return False + if rec.get("status") in ("Cancelled", "Filled", "ApiCancelled"): + return False + rec["status"] = "Cancelled" + return True + + def get_open_orders(self) -> List[Dict[str, Any]]: + return [dict(o) for o in self._orders.values() if o.get("status") not in ("Filled", "Cancelled")] + + # --- internal --- + + def _next_id(self) -> int: + oid = self._next_order_id + self._next_order_id += 1 + return oid + + def _mark_price(self, symbol: str) -> float: + return float(self._last_prices.get(symbol, 0.0)) + + def _gross_position_value(self) -> float: + total = 0.0 + for sym, st in self._positions.items(): + total += abs(st.quantity) * self._mark_price(sym) + return total + + def _net_liquidation(self) -> float: + return self._cash + sum( + st.quantity * self._mark_price(sym) for sym, st in self._positions.items() + ) + + def _fill_market(self, order_id: int, symbol: str, action: Action, quantity: float) -> None: + rec = self._orders[order_id] + px = self._mark_price(symbol) + if px <= 0: + # Avoid bogus fills if no price — leave submitted + rec["status"] = "Inactive" + rec["note"] = "no last price for symbol" + return + + notional = px * quantity + if action == "BUY": + if notional > self._cash + 1e-6: + rec["status"] = "Cancelled" + rec["note"] = "insufficient cash" + return + self._cash -= notional + self._add_position(symbol, quantity, px) + else: # SELL + pos = self._positions.get(symbol) + if not pos or pos.quantity + 1e-9 < quantity: + rec["status"] = "Cancelled" + rec["note"] = "insufficient position" + return + self._cash += notional + self._add_position(symbol, -quantity, px) + + rec["status"] = "Filled" + rec["avgFillPrice"] = px + rec["filledAt"] = _utc_now_iso() + + def _add_position(self, symbol: str, delta_qty: float, trade_price: float) -> None: + st = self._positions.get(symbol) + if st is None: + if abs(delta_qty) < 1e-12: + return + self._positions[symbol] = _PositionState(quantity=delta_qty, avg_cost=trade_price) + return + old_q, old_a = st.quantity, st.avg_cost + new_q = old_q + delta_qty + if abs(new_q) < 1e-9: + del self._positions[symbol] + return + if old_q > 0 and new_q > 0 and new_q < old_q: + st.quantity, st.avg_cost = new_q, old_a + elif old_q < 0 and new_q < 0 and new_q > old_q: + st.quantity, st.avg_cost = new_q, old_a + elif (old_q > 0 and new_q < 0) or (old_q < 0 and new_q > 0): + st.quantity, st.avg_cost = new_q, trade_price + else: + st.avg_cost = (old_q * old_a + delta_qty * trade_price) / new_q + st.quantity = new_q diff --git a/trading_system/version.py b/trading_system/version.py new file mode 100644 index 0000000..46b97bc --- /dev/null +++ b/trading_system/version.py @@ -0,0 +1,3 @@ +"""Package version (allocation-engine 1.0 line).""" + +__version__ = "1.0.0"