|
21 | 21 | except ImportError: |
22 | 22 | compute_v1 = None |
23 | 23 |
|
24 | | -from ib_insync import IB, Stock, MarketOrder, LimitOrder |
| 24 | +from quant_platform_kit.common.models import OrderIntent |
| 25 | +from quant_platform_kit.ibkr import ( |
| 26 | + connect_ib as ibkr_connect_ib, |
| 27 | + ensure_event_loop, |
| 28 | + fetch_historical_price_series, |
| 29 | + fetch_portfolio_snapshot, |
| 30 | + fetch_quote_snapshots, |
| 31 | + submit_order_intent, |
| 32 | +) |
25 | 33 |
|
26 | 34 | app = Flask(__name__) |
27 | 35 |
|
@@ -224,51 +232,24 @@ def send_tg_message(message): |
224 | 232 | print(f"Telegram send failed: {e}", flush=True) |
225 | 233 |
|
226 | 234 |
|
227 | | -# --------------------------------------------------------------------------- |
228 | | -# IB Gateway connection |
229 | | -# --------------------------------------------------------------------------- |
230 | | -def ensure_event_loop(): |
231 | | - """ib_insync expects an event loop even inside Gunicorn worker threads.""" |
232 | | - try: |
233 | | - loop = asyncio.get_event_loop_policy().get_event_loop() |
234 | | - except RuntimeError: |
235 | | - loop = asyncio.new_event_loop() |
236 | | - asyncio.set_event_loop(loop) |
237 | | - return loop |
238 | | - |
239 | | - if loop.is_closed(): |
240 | | - loop = asyncio.new_event_loop() |
241 | | - asyncio.set_event_loop(loop) |
242 | | - |
243 | | - return loop |
244 | | - |
245 | | - |
246 | 235 | def connect_ib(): |
247 | | - ensure_event_loop() |
248 | | - ib = IB() |
249 | | - ib.connect(IB_HOST, IB_PORT, clientId=IB_CLIENT_ID, timeout=20) |
250 | | - return ib |
| 236 | + return ibkr_connect_ib(IB_HOST, IB_PORT, IB_CLIENT_ID) |
251 | 237 |
|
252 | 238 |
|
253 | 239 | def get_historical_close(ib, symbol, duration="2 Y", bar_size="1 day"): |
254 | | - """Fetch daily close prices from IBKR for a US stock/ETF.""" |
255 | | - contract = Stock(symbol, 'SMART', 'USD') |
256 | | - ib.qualifyContracts(contract) # Fix I4: qualify before requesting data |
257 | | - bars = ib.reqHistoricalData( |
258 | | - contract, |
259 | | - endDateTime='', |
260 | | - durationStr=duration, |
261 | | - barSizeSetting=bar_size, |
262 | | - whatToShow='ADJUSTED_LAST', |
263 | | - useRTH=True, |
264 | | - formatDate=1, |
| 240 | + """Fetch daily close prices from IBKR via QuantPlatformKit.""" |
| 241 | + series = fetch_historical_price_series( |
| 242 | + ib, |
| 243 | + symbol, |
| 244 | + duration=duration, |
| 245 | + bar_size=bar_size, |
265 | 246 | ) |
266 | | - if not bars: |
| 247 | + if not series.points: |
267 | 248 | return pd.Series(dtype=float) |
268 | | - df = pd.DataFrame(bars) |
269 | | - df['date'] = pd.to_datetime(df['date']) |
270 | | - df = df.set_index('date') |
271 | | - return df['close'] |
| 249 | + return pd.Series( |
| 250 | + data=[point.close for point in series.points], |
| 251 | + index=pd.to_datetime([point.as_of for point in series.points]), |
| 252 | + ) |
272 | 253 |
|
273 | 254 |
|
274 | 255 | # --------------------------------------------------------------------------- |
@@ -399,60 +380,32 @@ def compute_signals(ib, current_holdings): |
399 | 380 | # --------------------------------------------------------------------------- |
400 | 381 | def get_current_portfolio(ib): |
401 | 382 | """Get current positions and account values.""" |
402 | | - ib.reqPositions() |
403 | | - time.sleep(1) |
404 | | - |
| 383 | + snapshot = fetch_portfolio_snapshot(ib) |
405 | 384 | positions = {} |
406 | | - for pos in ib.positions(): |
407 | | - symbol = pos.contract.symbol |
408 | | - if pos.position != 0: |
409 | | - positions[symbol] = { |
410 | | - 'quantity': int(pos.position), |
411 | | - 'avg_cost': float(pos.avgCost), |
412 | | - } |
413 | | - |
414 | | - account_values = {} |
415 | | - for av in ib.accountValues(): |
416 | | - if av.tag == 'NetLiquidation' and av.currency == 'USD': |
417 | | - account_values['equity'] = float(av.value) |
418 | | - if av.tag == 'AvailableFunds' and av.currency == 'USD': |
419 | | - account_values['buying_power'] = float(av.value) |
| 385 | + for position in snapshot.positions: |
| 386 | + positions[position.symbol] = { |
| 387 | + 'quantity': int(position.quantity), |
| 388 | + 'avg_cost': float(position.average_cost or 0.0), |
| 389 | + } |
| 390 | + |
| 391 | + account_values = { |
| 392 | + 'equity': snapshot.total_equity, |
| 393 | + 'buying_power': snapshot.buying_power or 0.0, |
| 394 | + } |
420 | 395 |
|
421 | 396 | return positions, account_values |
422 | 397 |
|
423 | 398 |
|
424 | 399 | def get_market_prices(ib, symbols): |
425 | | - """Fetch market prices for multiple symbols in one pass. Fix C4: no redundant calls.""" |
426 | | - prices = {} |
427 | | - contracts = [] |
428 | | - for symbol in symbols: |
429 | | - contract = Stock(symbol, 'SMART', 'USD') |
430 | | - ib.qualifyContracts(contract) |
431 | | - contracts.append((symbol, contract)) |
432 | | - |
433 | | - # Request all at once |
434 | | - tickers = {} |
435 | | - for symbol, contract in contracts: |
436 | | - tickers[symbol] = ib.reqMktData(contract, '', False, False) |
437 | | - |
438 | | - time.sleep(3) # Single wait for all |
439 | | - |
440 | | - for symbol, contract in contracts: |
441 | | - ib.cancelMktData(contract) |
442 | | - tk = tickers[symbol] |
443 | | - price = tk.marketPrice() |
444 | | - if np.isnan(price) or price <= 0: |
445 | | - price = tk.close |
446 | | - if not np.isnan(price) and price > 0: |
447 | | - prices[symbol] = float(price) |
448 | | - |
449 | | - return prices |
450 | | - |
451 | | - |
452 | | -def check_order_submitted(trade, symbol, side_text, qty): |
| 400 | + """Fetch market prices for multiple symbols in one pass.""" |
| 401 | + quotes = fetch_quote_snapshots(ib, symbols) |
| 402 | + return {symbol: quote.last_price for symbol, quote in quotes.items()} |
| 403 | + |
| 404 | + |
| 405 | +def check_order_submitted(report): |
453 | 406 | """Check if order was accepted. DAY orders auto-expire at close if not filled.""" |
454 | | - order_id = trade.order.orderId |
455 | | - status = trade.orderStatus.status |
| 407 | + order_id = report.broker_order_id |
| 408 | + status = report.status |
456 | 409 |
|
457 | 410 | if status in ['Submitted', 'PreSubmitted', 'Filled']: |
458 | 411 | return True, f"✅ {t('submitted', order_id=order_id)}" |
@@ -505,13 +458,11 @@ def execute_rebalance(ib, target_weights, positions, account_values): |
505 | 458 | if qty <= 0: |
506 | 459 | continue |
507 | 460 |
|
508 | | - contract = Stock(symbol, 'SMART', 'USD') |
509 | | - ib.qualifyContracts(contract) |
510 | | - order = MarketOrder('SELL', qty) |
511 | | - trade = ib.placeOrder(contract, order) |
512 | | - time.sleep(1) |
513 | | - |
514 | | - ok, status_msg = check_order_submitted(trade, symbol, "Sell", qty) |
| 461 | + report = submit_order_intent( |
| 462 | + ib, |
| 463 | + OrderIntent(symbol=symbol, side='sell', quantity=qty), |
| 464 | + ) |
| 465 | + ok, status_msg = check_order_submitted(report) |
515 | 466 | log = t("market_sell", symbol=symbol, qty=qty) + f" {status_msg}" |
516 | 467 | trade_logs.append(log) |
517 | 468 | if ok: |
@@ -540,14 +491,18 @@ def execute_rebalance(ib, target_weights, positions, account_values): |
540 | 491 | if qty <= 0: |
541 | 492 | continue |
542 | 493 |
|
543 | | - contract = Stock(symbol, 'SMART', 'USD') |
544 | | - ib.qualifyContracts(contract) |
545 | | - order = LimitOrder('BUY', qty, limit_price) |
546 | | - order.tif = 'DAY' # Auto-expire at close if not filled |
547 | | - trade = ib.placeOrder(contract, order) |
548 | | - time.sleep(1) |
549 | | - |
550 | | - ok, status_msg = check_order_submitted(trade, symbol, "Buy", qty) |
| 494 | + report = submit_order_intent( |
| 495 | + ib, |
| 496 | + OrderIntent( |
| 497 | + symbol=symbol, |
| 498 | + side='buy', |
| 499 | + quantity=qty, |
| 500 | + order_type='limit', |
| 501 | + limit_price=limit_price, |
| 502 | + time_in_force='DAY', |
| 503 | + ), |
| 504 | + ) |
| 505 | + ok, status_msg = check_order_submitted(report) |
551 | 506 | log = t("limit_buy", symbol=symbol, qty=qty, price=f"{limit_price:.2f}") + f" {status_msg}" |
552 | 507 | trade_logs.append(log) |
553 | 508 | if ok: |
|
0 commit comments