GitHub Copilot commited on
Commit
2dc114e
Β·
1 Parent(s): 96964a0

Add Alpaca broker integration

Browse files
Files changed (5) hide show
  1. ALPACA_TRADING_SETUP.md +83 -0
  2. README.md +6 -0
  3. app.py +40 -0
  4. trading/alpaca_broker.py +251 -0
  5. trading/worker.py +31 -7
ALPACA_TRADING_SETUP.md ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Alpaca Trading API Kurulumu
2
+
3
+ Bu uygulamada ABD hisseleri icin ilk gercek broker entegrasyonu Alpaca uzerinden yapilir.
4
+
5
+ ## Modlar
6
+
7
+ - `--mode paper`: Mevcut lokal kagit broker. Gercek API kullanmaz.
8
+ - `--mode sandbox --market us`: Alpaca paper trading API kullanir. Gercek para kullanmaz.
9
+ - `--mode live --market us`: Alpaca live endpoint kullanir. Gercek para riski vardir ve ekstra env kilidi ister.
10
+
11
+ BIST icin Alpaca kullanilmaz. BIST tarafinda ayri broker/Matriks bridge gerekir.
12
+
13
+ ## Gerekli Env Degiskenleri
14
+
15
+ Paper trading icin:
16
+
17
+ ```env
18
+ TRADING_BROKER=alpaca
19
+ ALPACA_API_KEY=...
20
+ ALPACA_API_SECRET=...
21
+ ALPACA_ORDER_WAIT_SEC=20
22
+ ```
23
+
24
+ Live trading icin bunlara ek olarak:
25
+
26
+ ```env
27
+ ALPACA_ENABLE_LIVE=true
28
+ MAX_ORDER_NOTIONAL=25000
29
+ ```
30
+
31
+ `ALPACA_ENABLE_LIVE` set edilmeden live endpoint acilmaz.
32
+
33
+ Opsiyonel endpoint override:
34
+
35
+ ```env
36
+ ALPACA_PAPER_BASE_URL=https://paper-api.alpaca.markets
37
+ ALPACA_LIVE_BASE_URL=https://api.alpaca.markets
38
+ ```
39
+
40
+ ## Guvenli Test SirasΔ±
41
+
42
+ 1. Credential ve hesap kontrolu:
43
+
44
+ ```bash
45
+ curl https://veteroner-borsa.hf.space/api/diag/alpaca
46
+ ```
47
+
48
+ 2. Yerelde Alpaca paper ile tek cycle:
49
+
50
+ ```bash
51
+ cd huggingface-space
52
+ TRADING_BROKER=alpaca python3 -m trading.worker --cycle --market us --mode sandbox --force
53
+ ```
54
+
55
+ 3. Daemon olarak Alpaca paper:
56
+
57
+ ```bash
58
+ cd huggingface-space
59
+ TRADING_BROKER=alpaca python3 -m trading.worker --daemon --market us --mode sandbox --interval 3600
60
+ ```
61
+
62
+ 4. Live icin son kilit:
63
+
64
+ ```bash
65
+ cd huggingface-space
66
+ TRADING_BROKER=alpaca ALPACA_ENABLE_LIVE=true python3 -m trading.worker --daemon --market us --mode live --interval 3600
67
+ ```
68
+
69
+ Daemon live modda terminalde `YES` onayi ister.
70
+
71
+ ## Risk Kilitleri
72
+
73
+ Worker emir gondermeden once mevcut guvenlik katmanlarini kullanir:
74
+
75
+ - Kill switch
76
+ - Circuit breaker
77
+ - Market saatleri kontrolu
78
+ - Gunluk emir limiti
79
+ - Tek emir notional limiti
80
+ - Minimum nakit rezervi
81
+ - Model risk ve scorecard kapilari
82
+
83
+ Ilk live kullanimda `MAX_ORDER_NOTIONAL` dusuk tutulmali ve Telegram onayli emir akisi eklenene kadar otomatik live daemon calistirilmamalidir.
README.md CHANGED
@@ -34,6 +34,12 @@ ML sinyallerini gerΓ§ek para ile kullanmadan ΓΆnce, yerel walk-forward backtest
34
 
35
  - [WALK_FORWARD_BACKTEST.md](WALK_FORWARD_BACKTEST.md)
36
 
 
 
 
 
 
 
37
  ## πŸ“¦ Kurulum
38
 
39
  ### 1. BağımlΔ±lΔ±klarΔ± YΓΌkle
 
34
 
35
  - [WALK_FORWARD_BACKTEST.md](WALK_FORWARD_BACKTEST.md)
36
 
37
+ ## ABD Hisseleri GerΓ§ek Broker Entegrasyonu
38
+
39
+ US piyasasΔ± iΓ§in Alpaca Trading API adaptΓΆrΓΌ eklendi. Γ–nce Alpaca paper endpoint ile doğrulama, sonra aΓ§Δ±k env kilidiyle live endpoint kullanΔ±mΔ± ΓΆnerilir:
40
+
41
+ - [ALPACA_TRADING_SETUP.md](ALPACA_TRADING_SETUP.md)
42
+
43
  ## πŸ“¦ Kurulum
44
 
45
  ### 1. BağımlΔ±lΔ±klarΔ± YΓΌkle
app.py CHANGED
@@ -1535,6 +1535,46 @@ def diag_telegram():
1535
  return report
1536
 
1537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1538
  # ─── Telegram Bot Webhook / Menu ────────────────────────────────────────────
1539
 
1540
  def _telegram_webhook_secret() -> str:
 
1535
  return report
1536
 
1537
 
1538
+ @app.get("/api/diag/alpaca")
1539
+ def diag_alpaca(paper: bool = Query(True, description="Use Alpaca paper endpoint")):
1540
+ """Diagnostic: report Alpaca trading API readiness without exposing secrets."""
1541
+ key = os.environ.get("ALPACA_API_KEY") or os.environ.get("ALPACA_KEY_ID") or ""
1542
+ secret = os.environ.get("ALPACA_API_SECRET") or os.environ.get("ALPACA_SECRET_KEY") or ""
1543
+ live_enabled = str(os.environ.get("ALPACA_ENABLE_LIVE", "")).strip().lower() in {"1", "true", "yes", "on"}
1544
+ report = {
1545
+ "broker": "alpaca",
1546
+ "paper": bool(paper),
1547
+ "apiKeySet": bool(key),
1548
+ "apiKeyLength": len(key),
1549
+ "apiSecretSet": bool(secret),
1550
+ "apiSecretLength": len(secret),
1551
+ "liveEnabled": live_enabled,
1552
+ "accountOk": False,
1553
+ }
1554
+ if not key or not secret:
1555
+ report["error"] = "alpaca_credentials_missing"
1556
+ return report
1557
+ if not paper and not live_enabled:
1558
+ report["error"] = "live_endpoint_blocked_set_ALPACA_ENABLE_LIVE"
1559
+ return report
1560
+ try:
1561
+ from trading.alpaca_broker import AlpacaBroker
1562
+
1563
+ broker = AlpacaBroker.from_env(paper=bool(paper))
1564
+ account = broker.get_account_info()
1565
+ report.update({
1566
+ "accountOk": True,
1567
+ "cash": account.get("cash"),
1568
+ "equity": account.get("equity"),
1569
+ "positionCount": len(account.get("positions", {})),
1570
+ "accountStatus": (account.get("raw_account") or {}).get("status"),
1571
+ "tradingBlocked": (account.get("raw_account") or {}).get("trading_blocked"),
1572
+ })
1573
+ except Exception as e:
1574
+ report["error"] = f"{type(e).__name__}: {e}"
1575
+ return report
1576
+
1577
+
1578
  # ─── Telegram Bot Webhook / Menu ────────────────────────────────────────────
1579
 
1580
  def _telegram_webhook_secret() -> str:
trading/alpaca_broker.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Alpaca broker adapter for US equities.
2
+
3
+ This adapter implements BrokerBase using Alpaca's Trading API v2.
4
+ Default safe path:
5
+ - sandbox mode -> Alpaca paper endpoint
6
+ - live mode -> Alpaca live endpoint only when explicitly enabled by env
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ import time
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, Optional
16
+
17
+ import requests
18
+ from requests.adapters import HTTPAdapter
19
+ from urllib3.util.retry import Retry
20
+
21
+ from trading.broker_base import (
22
+ BrokerBase,
23
+ FillResult,
24
+ OrderRequest,
25
+ OrderSide,
26
+ OrderStatus,
27
+ OrderType,
28
+ SlippageModel,
29
+ )
30
+
31
+ logger = logging.getLogger("trading.alpaca_broker")
32
+
33
+
34
+ class AlpacaConfigError(RuntimeError):
35
+ """Raised when Alpaca credentials or live guards are not configured."""
36
+
37
+
38
+ class AlpacaBroker(BrokerBase):
39
+ """BrokerBase implementation backed by Alpaca Trading API.
40
+
41
+ Env vars:
42
+ ALPACA_API_KEY / ALPACA_KEY_ID
43
+ ALPACA_API_SECRET / ALPACA_SECRET_KEY
44
+ ALPACA_PAPER_BASE_URL default https://paper-api.alpaca.markets
45
+ ALPACA_LIVE_BASE_URL default https://api.alpaca.markets
46
+ ALPACA_ENABLE_LIVE must be true/1/yes for live endpoint
47
+ ALPACA_ORDER_WAIT_SEC seconds to wait for market order fill
48
+ """
49
+
50
+ PAPER_BASE_URL = "https://paper-api.alpaca.markets"
51
+ LIVE_BASE_URL = "https://api.alpaca.markets"
52
+
53
+ def __init__(
54
+ self,
55
+ api_key: str,
56
+ api_secret: str,
57
+ *,
58
+ paper: bool = True,
59
+ base_url: str = "",
60
+ timeout: int = 30,
61
+ wait_for_fill_sec: float = 20.0,
62
+ slippage_model: Optional[SlippageModel] = None,
63
+ ) -> None:
64
+ super().__init__(name="AlpacaPaper" if paper else "AlpacaLive", slippage_model=slippage_model)
65
+ if not api_key or not api_secret:
66
+ raise AlpacaConfigError("Alpaca API key/secret are required")
67
+ self.api_key = api_key
68
+ self.api_secret = api_secret
69
+ self.paper = paper
70
+ self.base_url = (base_url or (self.PAPER_BASE_URL if paper else self.LIVE_BASE_URL)).rstrip("/")
71
+ self.timeout = timeout
72
+ self.wait_for_fill_sec = max(0.0, float(wait_for_fill_sec))
73
+
74
+ self._session = requests.Session()
75
+ self._session.headers.update({
76
+ "APCA-API-KEY-ID": self.api_key,
77
+ "APCA-API-SECRET-KEY": self.api_secret,
78
+ "Content-Type": "application/json",
79
+ "Accept": "application/json",
80
+ })
81
+ retries = Retry(
82
+ total=3,
83
+ backoff_factor=0.5,
84
+ status_forcelist=[429, 500, 502, 503, 504],
85
+ allowed_methods=["GET", "POST", "DELETE"],
86
+ )
87
+ adapter = HTTPAdapter(max_retries=retries)
88
+ self._session.mount("https://", adapter)
89
+ self._session.mount("http://", adapter)
90
+
91
+ @classmethod
92
+ def from_env(cls, *, paper: bool = True) -> "AlpacaBroker":
93
+ api_key = os.environ.get("ALPACA_API_KEY") or os.environ.get("ALPACA_KEY_ID") or ""
94
+ api_secret = os.environ.get("ALPACA_API_SECRET") or os.environ.get("ALPACA_SECRET_KEY") or ""
95
+ if not paper:
96
+ live_enabled = str(os.environ.get("ALPACA_ENABLE_LIVE", "")).strip().lower()
97
+ if live_enabled not in {"1", "true", "yes", "on"}:
98
+ raise AlpacaConfigError(
99
+ "ALPACA_ENABLE_LIVE must be true/1/yes before live Alpaca orders are allowed"
100
+ )
101
+ return cls(
102
+ api_key=api_key,
103
+ api_secret=api_secret,
104
+ paper=paper,
105
+ base_url=os.environ.get("ALPACA_PAPER_BASE_URL" if paper else "ALPACA_LIVE_BASE_URL", ""),
106
+ timeout=int(os.environ.get("ALPACA_TIMEOUT_SEC", "30")),
107
+ wait_for_fill_sec=float(os.environ.get("ALPACA_ORDER_WAIT_SEC", "20")),
108
+ )
109
+
110
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
111
+ url = f"{self.base_url}{path}"
112
+ resp = self._session.request(method, url, timeout=self.timeout, **kwargs)
113
+ if resp.status_code >= 400:
114
+ body = resp.text[:500]
115
+ raise RuntimeError(f"Alpaca API {resp.status_code} {path}: {body}")
116
+ if not resp.content:
117
+ return None
118
+ return resp.json()
119
+
120
+ def _execute(self, req: OrderRequest) -> FillResult:
121
+ payload: Dict[str, Any] = {
122
+ "symbol": str(req.symbol).upper(),
123
+ "qty": str(int(req.quantity)),
124
+ "side": req.side.value.lower(),
125
+ "type": "market" if req.order_type == OrderType.MARKET else "limit",
126
+ "time_in_force": self._map_time_in_force(req.time_in_force),
127
+ "client_order_id": req.client_order_id,
128
+ }
129
+ if req.order_type == OrderType.LIMIT:
130
+ payload["limit_price"] = str(req.limit_price)
131
+
132
+ try:
133
+ submitted = self._request("POST", "/v2/orders", json=payload)
134
+ order = self._wait_for_terminal_or_fill(submitted.get("id")) if submitted.get("id") else submitted
135
+ return self._fill_from_order(req, order or submitted)
136
+ except Exception as exc:
137
+ logger.warning("Alpaca order failed: %s", exc)
138
+ return FillResult(
139
+ order_id="alpaca_rejected",
140
+ client_order_id=req.client_order_id,
141
+ symbol=req.symbol,
142
+ side=req.side,
143
+ requested_qty=req.quantity,
144
+ filled_qty=0,
145
+ avg_fill_price=0.0,
146
+ commission=0.0,
147
+ slippage_bps=0.0,
148
+ status=OrderStatus.REJECTED,
149
+ timestamp=datetime.now(timezone.utc).isoformat(),
150
+ broker_name=self.name,
151
+ reject_reason=str(exc),
152
+ )
153
+
154
+ def _wait_for_terminal_or_fill(self, order_id: str) -> Dict[str, Any]:
155
+ deadline = time.time() + self.wait_for_fill_sec
156
+ last = self._request("GET", f"/v2/orders/{order_id}")
157
+ terminal = {"filled", "partially_filled", "canceled", "expired", "rejected"}
158
+ while time.time() < deadline:
159
+ status = str(last.get("status") or "").lower()
160
+ if status in terminal:
161
+ return last
162
+ time.sleep(1.0)
163
+ last = self._request("GET", f"/v2/orders/{order_id}")
164
+ return last
165
+
166
+ def _fill_from_order(self, req: OrderRequest, order: Dict[str, Any]) -> FillResult:
167
+ status = self._map_status(str(order.get("status") or ""))
168
+ filled_qty = int(float(order.get("filled_qty") or 0))
169
+ avg_price = float(order.get("filled_avg_price") or order.get("limit_price") or 0.0)
170
+ order_id = str(order.get("id") or "")
171
+ if status == OrderStatus.PENDING and filled_qty > 0:
172
+ status = OrderStatus.PARTIALLY_FILLED
173
+ if status in {OrderStatus.REJECTED, OrderStatus.CANCELLED, OrderStatus.EXPIRED}:
174
+ filled_qty = 0
175
+ avg_price = 0.0
176
+ return FillResult(
177
+ order_id=order_id or "alpaca_order",
178
+ client_order_id=str(order.get("client_order_id") or req.client_order_id),
179
+ symbol=req.symbol,
180
+ side=req.side,
181
+ requested_qty=req.quantity,
182
+ filled_qty=filled_qty,
183
+ avg_fill_price=avg_price,
184
+ commission=0.0,
185
+ slippage_bps=0.0,
186
+ status=status,
187
+ timestamp=str(order.get("filled_at") or order.get("submitted_at") or datetime.now(timezone.utc).isoformat()),
188
+ broker_name=self.name,
189
+ raw_response=order,
190
+ reject_reason=str(order.get("failed_at") or order.get("expired_at") or "") or None,
191
+ )
192
+
193
+ def get_account_info(self) -> Dict[str, Any]:
194
+ account = self._request("GET", "/v2/account")
195
+ positions_raw = self._request("GET", "/v2/positions") or []
196
+ positions: Dict[str, Dict[str, Any]] = {}
197
+ for pos in positions_raw:
198
+ symbol = str(pos.get("symbol") or "").upper()
199
+ if not symbol:
200
+ continue
201
+ qty = int(float(pos.get("qty") or 0))
202
+ if qty <= 0:
203
+ continue
204
+ positions[symbol] = {
205
+ "qty": qty,
206
+ "avg_cost": float(pos.get("avg_entry_price") or 0.0),
207
+ "market_value": float(pos.get("market_value") or 0.0),
208
+ "unrealized_pl": float(pos.get("unrealized_pl") or 0.0),
209
+ "unrealized_plpc": float(pos.get("unrealized_plpc") or 0.0),
210
+ }
211
+ equity = float(account.get("equity") or 0.0)
212
+ last_equity = float(account.get("last_equity") or equity or 1.0)
213
+ pnl_pct = ((equity / last_equity) - 1.0) * 100 if last_equity else 0.0
214
+ return {
215
+ "cash": round(float(account.get("cash") or 0.0), 2),
216
+ "equity": round(equity, 2),
217
+ "initial_cash": round(last_equity, 2),
218
+ "pnl_pct": round(pnl_pct, 2),
219
+ "positions": positions,
220
+ "trade_count": 0,
221
+ "total_commission": 0.0,
222
+ "buying_power": float(account.get("buying_power") or 0.0),
223
+ "raw_account": {
224
+ "status": account.get("status"),
225
+ "trading_blocked": account.get("trading_blocked"),
226
+ "account_blocked": account.get("account_blocked"),
227
+ "pattern_day_trader": account.get("pattern_day_trader"),
228
+ },
229
+ }
230
+
231
+ @staticmethod
232
+ def _map_time_in_force(value: str) -> str:
233
+ normalized = str(value or "DAY").strip().lower()
234
+ if normalized in {"gtc", "day", "ioc", "opg", "cls"}:
235
+ return normalized
236
+ return "day"
237
+
238
+ @staticmethod
239
+ def _map_status(value: str) -> OrderStatus:
240
+ normalized = value.lower()
241
+ if normalized == "filled":
242
+ return OrderStatus.FILLED
243
+ if normalized == "partially_filled":
244
+ return OrderStatus.PARTIALLY_FILLED
245
+ if normalized in {"canceled", "cancelled"}:
246
+ return OrderStatus.CANCELLED
247
+ if normalized == "expired":
248
+ return OrderStatus.EXPIRED
249
+ if normalized in {"rejected", "stopped", "suspended"}:
250
+ return OrderStatus.REJECTED
251
+ return OrderStatus.PENDING
trading/worker.py CHANGED
@@ -622,15 +622,37 @@ class TradingWorker:
622
 
623
  # Broker (based on mode)
624
  if self.mode == "live":
625
- from trading.midas_broker import MidasBroker
626
- self._broker = MidasBroker.from_env(sandbox=False, dry_run=False)
 
 
 
 
 
 
 
 
 
 
627
  logger.warning("=" * 60)
628
  logger.warning("*** LIVE TRADING MODE β€” REAL MONEY ***")
 
629
  logger.warning("=" * 60)
630
  elif self.mode == "sandbox":
631
- from trading.midas_broker import MidasBroker
632
- self._broker = MidasBroker.from_env(sandbox=True, dry_run=False)
633
- logger.info("Sandbox mode β€” using Midas demo environment")
 
 
 
 
 
 
 
 
 
 
 
634
  else:
635
  # Restore broker from state.json if available
636
  state_data = {}
@@ -1408,11 +1430,13 @@ class TradingWorker:
1408
  state_data["total_trades"] = total_trades
1409
  state_data["broker_cash"] = account.get("cash", 0)
1410
  state_data["is_running"] = False
1411
- # Save broker positions for paper mode
1412
- if self.mode == "paper" and hasattr(self._broker, "_positions"):
1413
  state_data["broker_positions"] = {
1414
  s: dict(p) for s, p in self._broker._positions.items() # type: ignore
1415
  }
 
 
1416
  state_data["updated_at"] = datetime.now(timezone.utc).isoformat()
1417
  tmp = state_file.with_suffix(".tmp")
1418
  tmp.write_text(json.dumps(state_data, indent=2, default=str))
 
622
 
623
  # Broker (based on mode)
624
  if self.mode == "live":
625
+ broker_provider = os.environ.get(
626
+ "TRADING_BROKER",
627
+ "alpaca" if self.market_id == "us" else "midas",
628
+ ).strip().lower()
629
+ if broker_provider == "alpaca":
630
+ if self.market_id != "us":
631
+ raise RuntimeError("Alpaca live broker is only supported for market_id='us'")
632
+ from trading.alpaca_broker import AlpacaBroker
633
+ self._broker = AlpacaBroker.from_env(paper=False)
634
+ else:
635
+ from trading.midas_broker import MidasBroker
636
+ self._broker = MidasBroker.from_env(sandbox=False, dry_run=False)
637
  logger.warning("=" * 60)
638
  logger.warning("*** LIVE TRADING MODE β€” REAL MONEY ***")
639
+ logger.warning("Broker provider: %s", broker_provider)
640
  logger.warning("=" * 60)
641
  elif self.mode == "sandbox":
642
+ broker_provider = os.environ.get(
643
+ "TRADING_BROKER",
644
+ "alpaca" if self.market_id == "us" else "midas",
645
+ ).strip().lower()
646
+ if broker_provider == "alpaca":
647
+ if self.market_id != "us":
648
+ raise RuntimeError("Alpaca paper broker is only supported for market_id='us'")
649
+ from trading.alpaca_broker import AlpacaBroker
650
+ self._broker = AlpacaBroker.from_env(paper=True)
651
+ logger.info("Sandbox mode β€” using Alpaca paper trading API")
652
+ else:
653
+ from trading.midas_broker import MidasBroker
654
+ self._broker = MidasBroker.from_env(sandbox=True, dry_run=False)
655
+ logger.info("Sandbox mode β€” using Midas demo environment")
656
  else:
657
  # Restore broker from state.json if available
658
  state_data = {}
 
1430
  state_data["total_trades"] = total_trades
1431
  state_data["broker_cash"] = account.get("cash", 0)
1432
  state_data["is_running"] = False
1433
+ # Save broker positions for dashboard/sync in every mode.
1434
+ if hasattr(self._broker, "_positions"):
1435
  state_data["broker_positions"] = {
1436
  s: dict(p) for s, p in self._broker._positions.items() # type: ignore
1437
  }
1438
+ else:
1439
+ state_data["broker_positions"] = account.get("positions", {})
1440
  state_data["updated_at"] = datetime.now(timezone.utc).isoformat()
1441
  tmp = state_file.with_suffix(".tmp")
1442
  tmp.write_text(json.dumps(state_data, indent=2, default=str))