yatws 0.1.2

Yet Another TWS (Interactive Brokers TWS API) Implementation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
// yatws/test_order.rs
use anyhow::{anyhow, Context, Result};
use log::{debug, error, info, warn};
use std::time::Duration;

use yatws::{
  IBKRError,
  IBKRClient,
  order::{OrderRequest, OrderSide, OrderType, TimeInForce, OrderStatus},
  OrderBuilder,
  contract::{Contract, SecType, OptionRight, DateOrMonth},
};

pub(super) fn order_market_impl(client: &IBKRClient, is_live: bool) -> Result<()> {
  info!("--- Testing Market Order ---");
  if is_live {
    warn!("Ensure market is open and liquid for SPY stock for this test to fill MKT orders quickly.");
    warn!("This test will BUY 1 share of SPY and then SELL it.");
    std::thread::sleep(Duration::from_secs(3)); // Give user time to read warning
  }

  let order_mgr = client.orders();
  let acct_mgr = client.account();

  let (contract, buy_request) = OrderBuilder::new(OrderSide::Buy, 1.0).market().for_stock("SPY").build()?;
  debug!("Request: {:?}", buy_request);
  let buy_order_id = order_mgr.place_order(contract.clone(), buy_request).context("Failed to place BUY order")?;
  info!("BUY order placed with {}", buy_order_id);
  // Wait for Buy Execution
  let wait_timeout = Duration::from_secs(20);
  info!("Waiting up to {:?} for BUY order {} execution...", wait_timeout, buy_order_id);
  match order_mgr.try_wait_order_executed(&buy_order_id, wait_timeout) {
    Ok(OrderStatus::Filled) => info!("BUY order {} filled.", buy_order_id),
    Ok(status) => {
      error!("BUY order {} did not fill as expected. Final status: {:?}", buy_order_id, status);
      attempt_cleanup(client, &contract)?; // Try cleanup even if buy failed
      return Err(anyhow!("BUY order {} did not fill. Status: {:?}", buy_order_id, status));
    }
    Err(e) => {
      error!("Error or timeout waiting for BUY order {}: {:?}", buy_order_id, e);
      attempt_cleanup(client, &contract)?; // Try cleanup even if buy failed
      return Err(e).context(format!("Waiting for BUY order {} failed", buy_order_id));
    }
  }

  // Verify Position Exists
  info!("Verifying position exists after BUY...");
  std::thread::sleep(Duration::from_secs(3)); // Allow account data propagation
  match acct_mgr.list_open_positions()?.iter().find(|p| p.contract.symbol == "SPY") {
    Some(pos) if (pos.quantity - 1.0).abs() < f64::EPSILON => info!("Verified position exists: SPY Quantity = {}", pos.quantity),
    Some(pos) => {
      error!("Position exists but quantity is unexpected after BUY: {}", pos.quantity);
      attempt_cleanup(client, &contract)?;
      return Err(anyhow!("Unexpected position quantity after BUY: {}", pos.quantity));
    }
    None => {
      error!("Position SPY not found after supposed BUY execution!");
      attempt_cleanup(client, &contract)?; // Still try cleanup? Maybe partial state exists.
      return Err(anyhow!("Position SPY not found after BUY execution"));
    }
  }

  // --- SELL Order ---
  info!("Placing SELL MKT order for 1 share to close position...");
  let (_, sell_request) = OrderBuilder::new(OrderSide::Sell, 1.0).market().for_stock("SPY").build()?;
  let sell_order_id = order_mgr.place_order(contract.clone(), sell_request).context("Failed to place SELL order")?;
  info!("SELL order placed with ID: {}", sell_order_id);

  // Wait for Sell Execution
  info!("Waiting up to {:?} for SELL order {} execution...", wait_timeout, sell_order_id);
  match order_mgr.try_wait_order_executed(&sell_order_id, wait_timeout) {
    Ok(OrderStatus::Filled) => info!("SELL order {} filled.", sell_order_id),
    Ok(status) => {
      // Don't fail immediately, check final position state
      warn!("SELL order {} reached status {:?}, not Filled. Checking final position state.", sell_order_id, status);
    }
    Err(e) => {
      // Don't fail immediately, check final position state
      warn!("Error or timeout waiting for SELL order {}: {:?}. Checking final position state.", sell_order_id, e);
    }
  }

  // Verify Position Closed
  info!("Verifying position closed after SELL...");
  std::thread::sleep(Duration::from_secs(3)); // Allow account data propagation
  match acct_mgr.list_open_positions()?.iter().find(|p| p.contract.symbol == "SPY") {
    Some(pos) if pos.quantity.abs() < f64::EPSILON => {
      info!("Verified position closed (Zero Quantity). Test successful.");
      Ok(()) // Success!
    }
    Some(pos) => {
      error!("Position SPY still exists after SELL order! Final Quantity = {}", pos.quantity);
      error!("Manual intervention likely required to close the SPY position.");
      Err(anyhow!("Position SPY not closed. Final Quantity: {}", pos.quantity))
    }
    None => {
      info!("Verified position closed (Not found in list). Test successful.");
      Ok(()) // Success!
    }
  }?;
  // Now print out the executions for the day.
  info!("Retrieving all executed trades.");
  for e in acct_mgr.get_day_executions()? {
    info!("  oid: {}, {} {} x {:.0} @ {:.2}: comm: {:.2}",
          e.order_id, e.side, e.symbol, e.quantity, e.price,
          e.commission.unwrap_or(0.0));
  }
  Ok(())
}


pub(super) fn order_many_impl(client: &IBKRClient, _is_live: bool) -> Result<()> {
  info!("--- Testing Sending Orders In Rapid Successiuon ---");
  let order_mgr = client.orders();
  let limit_price = 546.0;  // Slightly lower than ask.
  let mut oid = vec![];
  for k in 0..10 {
    let limit_price = limit_price + (k as f64) * 0.01;
    let (contract, buy_request) = OrderBuilder::new(OrderSide::Buy, 1.0).limit(limit_price).for_stock("SPY").build()?;
    debug!("Request[{}]: {:?}", k, buy_request);
    let buy_order_id = order_mgr.place_order(contract.clone(), buy_request).context("Failed to place BUY order")?;
    info!("BUY order placed with {}", buy_order_id);
    oid.push(buy_order_id);
  }

  let oid = oid;  // not mut.
  log::info!("Wait for orders submitted.");
  for o in &oid {
    match order_mgr.try_wait_order_submitted(o, Duration::from_secs(1)) {
      Ok(status) => {
        info!("BUY order {} status: {:?}", o, status);
      }
      Err(e) => {
        error!("Error or timeout waiting for BUY order {}: {:?}", o, e);
      }
    }
  }
  log::info!("Cancel orders");
  for o in &oid {
    order_mgr.cancel_order(o).unwrap_or_else(|e| { log::warn!("Failed to cancel order: {:?}", e); false });
    match order_mgr.try_wait_order_canceled(o, Duration::from_secs(3)) {
      Ok(status) => {
        info!("BUY order {} status: {:?}", o, status);
      }
      Err(e) => {
        error!("Error or timeout waiting for cancel order {}: {:?}", o, e);
      }
    }
  }
  log::info!("All done");
  Ok(())
}

pub(super) fn order_limit_impl(client: &IBKRClient, _is_live: bool) -> Result<()> {
  info!("--- Testing Limit Order ---");
  let order_mgr = client.orders();
  let limit_price = 548.0;  // Slightly lower than ask.
  let (contract, buy_request) = OrderBuilder::new(OrderSide::Buy, 1.0).limit(limit_price).for_stock("SPY").build()?;
  debug!("Request: {:?}", buy_request);
  let buy_order_id = order_mgr.place_order(contract.clone(), buy_request).context("Failed to place BUY order")?;
  info!("BUY order placed with {}, waiting for submit ack", buy_order_id);

  match order_mgr.try_wait_order_submitted(&buy_order_id, Duration::from_secs(1)) {
    Ok(status) => {
      info!("BUY order {} status: {:?}", buy_order_id, status);
    }
    Err(e) => {
      error!("Error or timeout waiting for BUY order {}: {:?}", buy_order_id, e);
    }
  }
  log::info!("Cancel order");
  order_mgr.cancel_order(&buy_order_id)?;
  match order_mgr.try_wait_order_canceled(&buy_order_id, Duration::from_secs(3)) {
    Ok(status) => {
      info!("BUY order {} status: {:?}", buy_order_id, status);
    }
    Err(e) => {
      error!("Error or timeout waiting for cancel order {}: {:?}", buy_order_id, e);
    }
  }
  log::info!("All done");
  Ok(())
}

// --- Helper for Order Test ---
pub(crate) fn attempt_cleanup(client: &IBKRClient, contract: &Contract) -> Result<()> {
  warn!("Attempting emergency cleanup for SPY position...");
  let order_mgr = client.orders();
  let acct_mgr = client.account();
  std::thread::sleep(Duration::from_secs(1)); // Small delay before check

  // Use last known state even if refresh failed
  let positions = match acct_mgr.list_open_positions() {
    Ok(p) => p,
    Err(e) => {
      error!("Cleanup: Failed to list positions: {:?}", e);
      return Ok(()); // Cannot proceed with cleanup if list fails
    }
  };

  if let Some(pos) = positions.iter().find(|p| p.contract.symbol == "SPY") {
    if pos.quantity.abs() > f64::EPSILON {
      warn!("Cleanup: Found SPY position with quantity {}. Placing MKT order to close.", pos.quantity);
      let side = if pos.quantity > 0.0 { OrderSide::Sell } else { OrderSide::Buy };
      let qty_to_close = pos.quantity.abs();

      let cleanup_request = OrderRequest {
        side, order_type: OrderType::Market, quantity: qty_to_close,
        time_in_force: TimeInForce::Day, transmit: true, ..Default::default()
      };
      match order_mgr.place_order(contract.clone(), cleanup_request) {
        Ok(cleanup_id) => {
          info!("Cleanup: Placed closing order {}. Execution not guaranteed.", cleanup_id);
        }
        Err(e) => error!("Cleanup: Failed to place closing order: {:?}", e),
      }
    } else {
      info!("Cleanup: Found SPY position with zero quantity.");
    }
  } else {
    info!("Cleanup: No SPY position found to clean up.");
  }
  Ok(())
}

pub(super) fn cleanup_orders_impl(client: &IBKRClient, _is_live: bool) -> Result<()> {
  info!("--- Testing Cleanup Orders (Cancel All Open Orders & List Positions) ---");
  if client.client_id() != 0 {
    return Err(anyhow!("Must cleanup from client 0, got {}", client.client_id()));
  }
  let order_mgr = client.orders();
  let acct_mgr = client.account();
  let refresh_timeout = Duration::from_secs(10);
  let cancel_wait_timeout = Duration::from_secs(5);
  let mut cancellation_errors = Vec::new();

  // 1. Refresh and list open orders
  info!("Refreshing order book...");
  match order_mgr.refresh_orderbook(refresh_timeout) {
    Ok(_) => info!("Order book refresh complete."),
    Err(IBKRError::Timeout(msg)) => {
      warn!("Order book refresh timed out: {}. Proceeding with potentially stale data.", msg);
      // Continue anyway, maybe some orders were received before timeout
    }
    Err(e) => {
      error!("Failed to refresh order book: {:?}", e);
      return Err(e).context("Order book refresh failed");
    }
  }

  let open_orders = order_mgr.get_open_orders();
  if open_orders.is_empty() {
    info!("No open orders found to cancel.");
  } else {
    info!("Found {} open orders. Attempting cancellation...", open_orders.len());
    for order in open_orders {
      info!("Cancelling order ID: {} (Symbol: {}, Side: {:?}, Qty: {})",
            order.id, order.contract.symbol, order.request.side, order.request.quantity);
      match order_mgr.cancel_order(&order.id) {
        Ok(true) => {
          info!("Cancel request sent for {}. Waiting for confirmation...", order.id);
          match order_mgr.try_wait_order_canceled(&order.id, cancel_wait_timeout) {
            Ok(status) => info!("Order {} cancellation confirmed with status: {:?}", order.id, status),
            Err(IBKRError::Timeout(_)) => {
              let msg = format!("Timeout waiting for cancellation confirmation for order {}", order.id);
              warn!("{}", msg);
              cancellation_errors.push(msg);
            }
            Err(e) => {
              let msg = format!("Error waiting for cancellation confirmation for order {}: {:?}", order.id, e);
              error!("{}", msg);
              cancellation_errors.push(msg);
            }
          }
        }
        Ok(false) => {
          // This means cancel_order decided not to send (e.g., already terminal)
          info!("Cancellation request for order {} not sent (likely already terminal).", order.id);
        }
        Err(e) => {
          let msg = format!("Failed to send cancel request for order {}: {:?}", order.id, e);
          error!("{}", msg);
          cancellation_errors.push(msg);
        }
      }
      // Small delay between cancellations? Maybe not necessary.
      // std::thread::sleep(Duration::from_millis(100));
    }
  }

  // 2. Refresh account details (summary & positions)
  // Updates come every 3 minutes.
  std::thread::sleep(Duration::from_millis(500));

  // 3. List final positions
  info!("Fetching final open positions...");
  match acct_mgr.list_open_positions() {
    Ok(positions) => {
      if positions.is_empty() {
        info!("No open positions found.");
      } else {
        info!("Final Open Positions:");
        for pos in positions {
          info!(
            "  Symbol: {}, Qty: {}, AvgCost: {}, MktPx: {}, MktVal: {}, UnPNL: {}",
            pos.symbol,
            pos.quantity,
            pos.average_cost,
            pos.market_price,
            pos.market_value,
            pos.unrealized_pnl
          );
        }
      }
    }
    Err(e) => {
      let msg = format!("Failed to list final open positions: {:?}", e);
      error!("{}", msg);
      cancellation_errors.push(msg);
    }
  }

  // 4. Report overall success/failure
  if cancellation_errors.is_empty() {
    info!("Cleanup orders test completed successfully.");
    Ok(())
  } else {
    error!("Cleanup orders test completed with errors:");
    for err in &cancellation_errors {
      error!("  - {}", err);
    }
    Err(anyhow!("One or more errors occurred during cleanup: {:?}", cancellation_errors))
  }
}

pub(super) fn order_global_cancel_impl(client: &IBKRClient, is_live: bool) -> Result<()> {
  info!("--- Testing Global Order Cancel ---");
  if is_live {
    warn!("This test will place two GTC limit orders for SPY and then attempt a global cancel.");
    warn!("Ensure SPY is liquid and the limit prices are away from the market to prevent immediate fills.");
    std::thread::sleep(Duration::from_secs(3));
  }

  let order_mgr = client.orders();
  let contract = Contract::stock("SPY");

  // Place two GTC limit orders that are unlikely to fill immediately
  let (spy_contract_ref, order1_req) = OrderBuilder::new(OrderSide::Buy, 1.0)
    .limit(100.00) // Far from market
    .with_tif(TimeInForce::GoodTillCancelled)
    .for_contract(contract.clone()) // Use the cloned contract
    .build()?;
  let order1_id = order_mgr.place_order(spy_contract_ref.clone(), order1_req).context("Failed to place order 1")?;
  info!("Order 1 (BUY SPY @ 100.00 GTC) placed with ID: {}", order1_id);

  let (_spy_contract_ref, order2_req) = OrderBuilder::new(OrderSide::Sell, 1.0)
    .limit(999.00) // Far from market
    .with_tif(TimeInForce::GoodTillCancelled)
    .for_contract(contract.clone()) // Use the cloned contract
    .build()?;
  let order2_id = order_mgr.place_order(spy_contract_ref, order2_req).context("Failed to place order 2")?;
  info!("Order 2 (SELL SPY @ 999.00 GTC) placed with ID: {}", order2_id);

  // Wait for orders to be submitted
  let submit_timeout = Duration::from_secs(20);
  info!("Waiting for orders to be submitted (timeout: {:?})...", submit_timeout);
  match order_mgr.try_wait_order_submitted(&order1_id, submit_timeout) {
    Ok(status) => info!("Order {} submitted with status: {:?}", order1_id, status),
    Err(e) => {
      error!("Error waiting for order {} submission: {:?}", order1_id, e);
      // Attempt to cancel individually if global cancel might fail or for cleanup
      let _ = order_mgr.cancel_order(&order1_id);
      let _ = order_mgr.cancel_order(&order2_id);
      return Err(e).context(format!("Order {} submission failed", order1_id));
    }
  }
  match order_mgr.try_wait_order_submitted(&order2_id, submit_timeout) {
    Ok(status) => info!("Order {} submitted with status: {:?}", order2_id, status),
    Err(e) => {
      error!("Error waiting for order {} submission: {:?}", order2_id, e);
      let _ = order_mgr.cancel_order(&order1_id);
      let _ = order_mgr.cancel_order(&order2_id);
      return Err(e).context(format!("Order {} submission failed", order2_id));
    }
  }

  // Perform global cancel
  info!("Requesting global cancel...");
  order_mgr.cancel_all_orders_globally().context("Failed to send global cancel request")?;
  info!("Global cancel request sent. Waiting for orders to be cancelled...");

  // Wait for orders to be cancelled
  let cancel_timeout = Duration::from_secs(30);
  let mut all_cancelled_successfully = true;

  for order_id_str in [&order1_id, &order2_id] {
    info!("Waiting for order {} to be cancelled (timeout: {:?})...", order_id_str, cancel_timeout);
    match order_mgr.try_wait_order_canceled(order_id_str, cancel_timeout) {
      Ok(status) if status == OrderStatus::Cancelled || status == OrderStatus::ApiCancelled => {
        info!("Order {} successfully cancelled with status: {:?}", order_id_str, status);
      }
      Ok(status) => {
        error!("Order {} reached unexpected status after global cancel: {:?}", order_id_str, status);
        all_cancelled_successfully = false;
      }
      Err(e) => {
        error!("Error waiting for order {} cancellation: {:?}", order_id_str, e);
        all_cancelled_successfully = false;
      }
    }
  }

  if all_cancelled_successfully {
    info!("Global cancel test completed successfully. Both orders cancelled.");
    Ok(())
  } else {
    Err(anyhow!("One or more orders were not successfully cancelled after global cancel request."))
  }
}

pub(super) fn order_exercise_option_impl(client: &IBKRClient, is_live: bool) -> Result<()> {
  info!("--- Testing Option Exercise/Lapse ---");
  if is_live {
    warn!("This test will BUY 2 IBM call option contracts and then EXERCISE 1 and LAPSE 1.");
    warn!("The test will select the lowest strike (most in-the-money) call option for the current month.");
    warn!("Ensure you have sufficient buying power for IBM options.");
    std::thread::sleep(Duration::from_secs(5));
  }

  let order_mgr = client.orders();
  let acct_mgr = client.account();
  let ref_data_mgr = client.data_ref();

  // 1. Get account ID
  let account_id = acct_mgr.get_account_info()?.account_id;
  info!("Using account ID: {}", account_id);

  // 2. Find IBM call options for the current month
  let mut option_search_contract = Contract::new();
  option_search_contract.symbol = "IBM".to_string();
  option_search_contract.sec_type = SecType::Option;
  option_search_contract.currency = "USD".to_string();
  option_search_contract.exchange = "SMART".to_string();
  option_search_contract.last_trade_date_or_contract_month = Some(DateOrMonth::Date(OrderBuilder::next_monthly_option_expiry()));
  option_search_contract.right = Some(OptionRight::Call);

  let option_details_list = ref_data_mgr.get_contract_details(&option_search_contract)
    .context("Failed to get option contract details for IBM")?;

  if option_details_list.is_empty() {
    return Err(anyhow!("No IBM call options found for current month. Ensure market data subscription for IBM options."));
  }

  // 3. Find the lowest strike call option (most likely to be in-the-money)
  let mut best_option = None;
  let mut lowest_strike = f64::MAX;

  for details in &option_details_list {
    if let Some(strike) = details.contract.strike {
      if strike < lowest_strike {
        lowest_strike = strike;
        best_option = Some(details.contract.clone());
      }
    }
  }

  let option_contract = best_option.ok_or_else(|| anyhow!("No valid IBM call option found with strike price"))?;

  info!("Selected lowest strike call option: ConID={}, Symbol={}, Expiry={}, Strike={}, Right={:?}",
        option_contract.con_id, option_contract.symbol,
        option_contract.last_trade_date_or_contract_month.clone().map(|x| x.to_string()).unwrap_or("N/A".to_string()),
        option_contract.strike.unwrap_or(0.0), option_contract.right);

  // 4. Buy 2 option contracts at market price
  info!("Placing market order to BUY 2 {} call option contracts...", option_contract.symbol);

  let option_buy_request = OrderRequest {
    side: OrderSide::Buy,
    order_type: OrderType::Market,
    quantity: 2.0, // Buy 2 contracts so we can exercise 1 and lapse 1
    time_in_force: TimeInForce::Day,
    transmit: true,
    ..Default::default()
  };

  let buy_order_id = order_mgr.place_order(option_contract.clone(), option_buy_request)
    .context("Failed to place option buy order")?;

  info!("Option BUY order placed with ID: {}. Waiting for execution...", buy_order_id);

  // 5. Wait for the option purchase to fill
  let buy_timeout = Duration::from_secs(30);
  match order_mgr.try_wait_order_executed(&buy_order_id, buy_timeout) {
    Ok(OrderStatus::Filled) => {
      info!("Option BUY order {} filled successfully.", buy_order_id);
    }
    Ok(status) => {
      error!("Option BUY order {} did not fill. Final status: {:?}", buy_order_id, status);
      return Err(anyhow!("Option BUY order {} failed to fill. Status: {:?}", buy_order_id, status));
    }
    Err(e) => {
      error!("Error waiting for option BUY order {}: {:?}", buy_order_id, e);
      return Err(e).context(format!("Waiting for option BUY order {} failed", buy_order_id));
    }
  }

  // 6. Verify we have the option position
  info!("Verifying option position exists...");
  std::thread::sleep(Duration::from_secs(3)); // Allow position data to update

  let positions = acct_mgr.list_open_positions()?;
  let option_position = positions.iter().find(|p| {
    p.contract.symbol == option_contract.symbol &&
    p.contract.sec_type == SecType::Option &&
    p.contract.strike == option_contract.strike &&
    p.contract.right == option_contract.right &&
    p.contract.last_trade_date_or_contract_month == option_contract.last_trade_date_or_contract_month
  });

  match option_position {
    Some(pos) if pos.quantity >= 2.0 => {
      info!("Verified option position: {} has quantity {}", pos.symbol, pos.quantity);
    }
    Some(pos) => {
      warn!("Option position exists but quantity {} is less than expected 2.0", pos.quantity);
      // Continue anyway, adjust quantities
    }
    None => {
      error!("Option position not found after purchase!");
      return Err(anyhow!("Option position not found after successful purchase"));
    }
  }

  // 7. Exercise 1 contract
  let exercise_req_id = 9001;
  let exercise_qty = 1;
  let override_exercise = false;

  info!("Attempting to EXERCISE {} contract(s) of {} with ReqID {}",
        exercise_qty, option_contract.local_symbol.as_deref().unwrap_or(&option_contract.symbol), exercise_req_id);

  match order_mgr.exercise_option(exercise_req_id, &option_contract, yatws::order::ExerciseAction::Exercise, exercise_qty, &account_id, override_exercise) {
    Ok(_) => info!("Exercise request sent for ReqID {}. Monitor account updates.", exercise_req_id),
    Err(e) => {
      error!("Failed to send EXERCISE request for ReqID {}: {:?}", exercise_req_id, e);
      return Err(e).context("Exercise option request failed");
    }
  }

  // Allow time for processing
  std::thread::sleep(Duration::from_secs(5));

  // 8. Lapse 1 contract (if we still have position)
  let lapse_req_id = 9002;
  let lapse_qty = 1;
  let override_lapse = false;

  info!("Attempting to LAPSE {} contract(s) of {} with ReqID {}",
        lapse_qty, option_contract.local_symbol.as_deref().unwrap_or(&option_contract.symbol), lapse_req_id);

  match order_mgr.exercise_option(lapse_req_id, &option_contract, yatws::order::ExerciseAction::Lapse, lapse_qty, &account_id, override_lapse) {
    Ok(_) => info!("Lapse request sent for ReqID {}. Monitor account updates.", lapse_req_id),
    Err(e) => {
      warn!("Failed to send LAPSE request for ReqID {}: {:?}. This might be expected if position was reduced by exercise.", lapse_req_id, e);
    }
  }

  if is_live {
    info!("Exercise/Lapse requests completed. Waiting 15 seconds for processing...");
    std::thread::sleep(Duration::from_secs(15));

    // Check final positions
    info!("Checking final positions after exercise/lapse operations...");
    let final_positions = acct_mgr.list_open_positions()?;

    // Look for the underlying stock position (from exercise)
    if let Some(stock_pos) = final_positions.iter().find(|p| p.contract.symbol == "IBM" && p.contract.sec_type == SecType::Stock) {
      info!("Found IBM stock position from exercise: quantity = {}", stock_pos.quantity);
    }

    // Look for remaining option position
    if let Some(opt_pos) = final_positions.iter().find(|p| {
      p.contract.symbol == option_contract.symbol &&
      p.contract.sec_type == SecType::Option &&
      p.contract.strike == option_contract.strike
    }) {
      info!("Remaining {} option position: quantity = {}", opt_pos.symbol, opt_pos.quantity);
    }

    info!("Manual verification of exercise/lapse operations and resulting positions is recommended.");
  } else {
    info!("Exercise/Lapse test sequence completed for replay mode.");
  }

  Ok(())
}


pub(super) fn order_what_if_impl(client: &IBKRClient, _is_live: bool) -> Result<()> {
  info!("--- Testing What-If Order Check ---");
  let order_mgr = client.orders();

  // Define a potential order (e.g., buy 100 shares of AAPL limit 150)
  let (contract, order_req) = OrderBuilder::new(OrderSide::Buy, 100.0)
    .limit(150.00)
    .for_stock("AAPL")
    .with_tif(TimeInForce::Day)
  // Example: Add a time condition - ensure you have `chrono::Utc` in scope
  // .add_time_condition(Utc::now() + ChronoDuration::minutes(5), true) // Condition: time is after 5 mins from now
  // .with_account("YOUR_ACCOUNT_ID") // Optional: Specify account if needed
  // Example: Add another condition with explicit conjunction
  // .with_next_condition_conjunction(yatws::order_builder::ConditionConjunction::And)
  // .add_volume_condition(265598, "SMART", 1000000, true) // Example volume condition
    .build()?;

  info!("Checking What-If for: {:?} {} {} @ {}",
        order_req.side, order_req.quantity, contract.symbol,
        order_req.limit_price.map_or("MKT".to_string(), |p| p.to_string()));

  let timeout = Duration::from_secs(15);
  match order_mgr.check_what_if_order(&contract, &order_req, timeout) {
    Ok(state) => {
      info!("Successfully received What-If results:");
      info!("  Status (should be PreSubmitted/Submitted): {:?}", state.status);
      info!("  Initial Margin Before: {:?}", state.initial_margin_before);
      info!("  Maintenance Margin Before: {:?}", state.maintenance_margin_before);
      info!("  Equity With Loan Before: {:?}", state.equity_with_loan_before);
      info!("  Initial Margin Change: {:?}", state.initial_margin_change);
      info!("  Maintenance Margin Change: {:?}", state.maintenance_margin_change);
      info!("  Equity With Loan Change: {:?}", state.equity_with_loan_change);
      info!("  Initial Margin After: {:?}", state.initial_margin_after);
      info!("  Maintenance Margin After: {:?}", state.maintenance_margin_after);
      info!("  Equity With Loan After: {:?}", state.equity_with_loan_after);
      info!("  Commission: {:?} {}", state.commission, state.commission_currency.as_deref().unwrap_or(""));
      info!("  Min Commission: {:?}", state.min_commission);
      info!("  Max Commission: {:?}", state.max_commission);
      info!("  Warning Text: {:?}", state.warning_text);

      // Basic validation: Check if some key fields were populated
      if state.initial_margin_after.is_none() && state.commission.is_none() {
        warn!("What-If check returned state, but key fields (margin, commission) are missing.");
        // Depending on strictness, could return Err here.
      }
      Ok(())
    }
    Err(e) => {
      error!("What-If check failed: {:?}", e);
      Err(e.into())
    }
  }
}