FROZEN AS HISTORY (Aug 21, 2026, Finding #386 per R4): pre-live test plan superseded by live operations; retained only as a regression checklist of WHAT to verify — every parameter below is pre-live and stale (see the STALE-WARNING). Indexed in
docs/archive/INDEX.md.Version: 1.4 | Last Updated: February 7, 2026 | Status: 📋 READY FOR EXECUTION (superseded by live operations — bot LIVE since Jan 2026; retained as a regression checklist)
⚠️ STALE-WARNING (Apr 26, 2026 — Phase 1 of Strategy Analysis Revamp). This doc was written pre-live and references plain "STOP" order type. The current operative behavior since Finding #179 (Feb 10, 2026) is STP-LMT with a 2% buffer on every stop, with
outsideRth=True. The pre-live test categories below remain a useful checklist of what to verify, but the order-type column should be read as STP-LMT, not STOP. Other current parameters (sizing 30/22/15%, stops 3/4/5%, $1,000 volume gate per Finding #311) supersede any older numbers in this doc.
Validate all order types and execution flows used in the Earnings Trading Strategy. (Historical note: this was a pre-live checklist; the bot has been LIVE since Jan 2026, so these categories now serve as a regression/verification reference rather than a gate before first deployment.)
Paper Account: DU…91 (Port 4002) Starting Balance: ~$1,000,000
| Test ID | Test Name | Order Type | Expected Result | Status |
|---|---|---|---|---|
| 1.1 | Market Buy | MARKET | Immediate fill at market price | ⬜ |
| 1.2 | Market Sell | MARKET | Immediate fill at market price | ⬜ |
| 1.3 | Limit Buy (at ask) | LIMIT | Fill within seconds | ⬜ |
| 1.4 | Limit Buy (below bid) | LIMIT | No fill (expected) | ⬜ |
| 1.5 | Limit Sell (at bid) | LIMIT | Fill within seconds | ⬜ |
| 1.6 | Stop Loss (sell) | STP-LMT | Triggers when price hits stop (#179) | ⬜ |
| 1.7 | Stop Loss (buy to cover) | STP-LMT | Triggers when price hits stop (#179) | ⬜ |
| Test ID | Test Name | Flow | Expected Result | Status |
|---|---|---|---|---|
| 2.1 | Guaranteed Fill Entry | LIMIT → MARKET fallback | Full position filled | ⬜ |
| 2.2 | Entry with Stop Attachment | Entry + STP-LMT | Both orders placed (#179) | ⬜ |
| 2.3 | Scheduled Exit | LIMIT → MARKET fallback | Position closed | ⬜ |
| 2.4 | Instant Exit (cancel stop + market) | CANCEL + MARKET | Stop cancelled, position closed | ⬜ |
| Test ID | Test Name | Direction | Expected Result | Status |
|---|---|---|---|---|
| 3.1 | Long Entry | BUY | Position opened | ⬜ |
| 3.2 | Long Exit | SELL | Position closed | ⬜ |
| 3.3 | Short Entry | SELL SHORT | Short position opened | ⬜ |
| 3.4 | Short Exit | BUY TO COVER | Short position closed | ⬜ |
| Test ID | Test Name | Time | Expected Result | Status |
|---|---|---|---|---|
| 4.1 | Pre-market order (if tested during pre-market) | 4-9:30 AM | outsideRth=True works | ⬜ |
| 4.2 | After-hours order (if tested during after-hours) | 4-8 PM | outsideRth=True works | ⬜ |
| Test ID | Test Name | Scenario | Expected Result | Status |
|---|---|---|---|---|
| 5.1 | Invalid Symbol | Order for "ZZZZZ" | Graceful error, no crash | ⬜ |
| 5.2 | Insufficient Funds | Order > buying power | Rejected, logged | ⬜ |
| 5.3 | Cancel Non-existent Order | Cancel invalid ID | Graceful error | ⬜ |
| Test ID | Test Name | Scenario | Expected Result | Status |
|---|---|---|---|---|
| 6.1 | Future Entry Time Conviction | Query conviction for BMO tomorrow | Returns latest available data | ✅ |
| 6.2 | Market Cap Lookup (Major Banks) | Get market cap for BAC, C, WFC | Returns correct value, not None | ✅ |
| 6.3 | Market Cap Lookup (Unknown Stock) | Get market cap for rare ticker | Falls back to yfinance | ⬜ |
| 6.4 | Schema Query (Conviction) | Query conviction from correct table | Uses price_snapshots.yes_price | ✅ |
| 6.5 | Schema Query (Volume) | Query volume from correct table | Uses markets.volume_total | ✅ |
| 6.6 | SHORT Signal Detection | Generate signal for ≤30% conviction | Creates SHORT signal correctly | ⬜ |
| 6.7 | Signal Generation Dry Run | Run signal generation T-12h before market | Returns valid signals, not 0 | ✅ |
Note: Tests 6.1, 6.2, 6.4, 6.5, 6.7 were added and verified after Finding #32 (pre-trade simulation) on Jan 13, 2026.
#!/usr/bin/env python3
"""
Paper Trading Test Suite
Run all order type tests against IBKR Paper Trading account.
"""
import sys
import time
import logging
from datetime import datetime
from typing import Dict, List, Any
# Setup path
sys.path.insert(0, '[hidden]')
# NOTE: this standalone test harness uses StopOrder (plain STOP) only to exercise
# the paper gateway. PRODUCTION uses StopLimitOrder (STP-LMT) with a 2% buffer and
# outsideRth=True on every stop (Finding #179) — not plain STOP/GTC.
from ib_insync import IB, Stock, MarketOrder, LimitOrder, StopOrder, util
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Test configuration
PAPER_PORT = 4002
TEST_SYMBOL = "AAPL" # Highly liquid stock
TEST_SHARES = 1 # Minimal size for testing
class PaperTradingTester:
def __init__(self):
self.ib = IB()
self.results: List[Dict[str, Any]] = []
def connect(self) -> bool:
"""Connect to paper trading gateway."""
try:
self.ib.connect('127.0.0.1', PAPER_PORT, clientId=50, readonly=False)
accounts = self.ib.managedAccounts()
logger.info(f"Connected to paper account: {accounts[0]}")
if not accounts[0].startswith('DU'):
logger.error("NOT A PAPER ACCOUNT! Aborting.")
return False
return True
except Exception as e:
logger.error(f"Connection failed: {e}")
return False
def disconnect(self):
"""Disconnect from gateway."""
self.ib.disconnect()
def get_contract(self, symbol: str) -> Stock:
"""Get qualified stock contract."""
contract = Stock(symbol, 'SMART', 'USD')
self.ib.qualifyContracts(contract)
return contract
def get_quote(self, contract: Stock) -> Dict:
"""Get current bid/ask."""
self.ib.reqMktData(contract, '', False, False)
self.ib.sleep(2)
ticker = self.ib.ticker(contract)
return {
'bid': ticker.bid,
'ask': ticker.ask,
'last': ticker.last,
}
def run_test(self, test_id: str, test_name: str, test_func) -> Dict:
"""Run a single test and record result."""
logger.info(f"\n{'='*60}")
logger.info(f"TEST {test_id}: {test_name}")
logger.info('='*60)
result = {
'test_id': test_id,
'test_name': test_name,
'status': 'PENDING',
'details': '',
'timestamp': datetime.now().isoformat(),
}
try:
success, details = test_func()
result['status'] = 'PASS' if success else 'FAIL'
result['details'] = details
except Exception as e:
result['status'] = 'ERROR'
result['details'] = str(e)
logger.error(f"Test error: {e}")
self.results.append(result)
logger.info(f"Result: {result['status']} - {result['details']}")
return result
# === TEST IMPLEMENTATIONS ===
def test_1_1_market_buy(self):
"""Test 1.1: Market Buy"""
contract = self.get_contract(TEST_SYMBOL)
order = MarketOrder('BUY', TEST_SHARES)
order.outsideRth = True
trade = self.ib.placeOrder(contract, order)
self.ib.sleep(5)
if trade.orderStatus.status == 'Filled':
return True, f"Filled {trade.orderStatus.filled} @ ${trade.orderStatus.avgFillPrice:.2f}"
return False, f"Status: {trade.orderStatus.status}"
def test_1_2_market_sell(self):
"""Test 1.2: Market Sell (close position from 1.1)"""
contract = self.get_contract(TEST_SYMBOL)
order = MarketOrder('SELL', TEST_SHARES)
order.outsideRth = True
trade = self.ib.placeOrder(contract, order)
self.ib.sleep(5)
if trade.orderStatus.status == 'Filled':
return True, f"Filled {trade.orderStatus.filled} @ ${trade.orderStatus.avgFillPrice:.2f}"
return False, f"Status: {trade.orderStatus.status}"
def test_1_3_limit_buy_at_ask(self):
"""Test 1.3: Limit Buy at Ask (should fill)"""
contract = self.get_contract(TEST_SYMBOL)
quote = self.get_quote(contract)
limit_price = round(quote['ask'] * 1.001, 2) # Slightly above ask
order = LimitOrder('BUY', TEST_SHARES, limit_price)
order.outsideRth = True
trade = self.ib.placeOrder(contract, order)
self.ib.sleep(10)
if trade.orderStatus.status == 'Filled':
return True, f"Filled @ ${trade.orderStatus.avgFillPrice:.2f} (limit was ${limit_price})"
# Cancel if not filled
self.ib.cancelOrder(order)
return False, f"Status: {trade.orderStatus.status}"
def test_1_4_limit_buy_below_bid(self):
"""Test 1.4: Limit Buy below Bid (should NOT fill)"""
contract = self.get_contract(TEST_SYMBOL)
quote = self.get_quote(contract)
limit_price = round(quote['bid'] * 0.95, 2) # 5% below bid
order = LimitOrder('BUY', TEST_SHARES, limit_price)
order.outsideRth = True
order.tif = 'IOC' # Immediate or Cancel
trade = self.ib.placeOrder(contract, order)
self.ib.sleep(3)
# Should be cancelled (not filled)
if trade.orderStatus.status in ['Cancelled', 'Inactive']:
return True, f"Correctly not filled at ${limit_price} (bid was ${quote['bid']})"
elif trade.orderStatus.status == 'Filled':
return False, f"Unexpectedly filled (should not have)"
return True, f"Status: {trade.orderStatus.status} (expected no fill)"
def test_1_5_limit_sell_at_bid(self):
"""Test 1.5: Limit Sell at Bid (close position from 1.3)"""
contract = self.get_contract(TEST_SYMBOL)
quote = self.get_quote(contract)
limit_price = round(quote['bid'] * 0.999, 2) # Slightly below bid
order = LimitOrder('SELL', TEST_SHARES, limit_price)
order.outsideRth = True
trade = self.ib.placeOrder(contract, order)
self.ib.sleep(10)
if trade.orderStatus.status == 'Filled':
return True, f"Filled @ ${trade.orderStatus.avgFillPrice:.2f}"
# Cancel and use market if not filled
self.ib.cancelOrder(order)
self.ib.sleep(1)
market_order = MarketOrder('SELL', TEST_SHARES)
trade2 = self.ib.placeOrder(contract, market_order)
self.ib.sleep(3)
return True, f"Limit not filled, used market @ ${trade2.orderStatus.avgFillPrice:.2f}"
def test_1_6_stop_loss_sell(self):
"""Test 1.6: Stop Loss (Sell) - Place and verify, then cancel"""
contract = self.get_contract(TEST_SYMBOL)
quote = self.get_quote(contract)
stop_price = round(quote['last'] * 0.95, 2) # 5% below current
# First buy a share
buy_order = MarketOrder('BUY', TEST_SHARES)
buy_trade = self.ib.placeOrder(contract, buy_order)
self.ib.sleep(3)
# Place stop loss
# NOTE: harness uses plain StopOrder; PRODUCTION uses StopLimitOrder (STP-LMT)
# with a 2% buffer and outsideRth=True per Finding #179.
stop_order = StopOrder('SELL', TEST_SHARES, stop_price)
stop_order.outsideRth = True
stop_order.tif = 'GTC'
stop_trade = self.ib.placeOrder(contract, stop_order)
self.ib.sleep(2)
# Verify stop is placed
if stop_trade.orderStatus.status in ['PreSubmitted', 'Submitted']:
# Cancel stop and close position
self.ib.cancelOrder(stop_order)
self.ib.sleep(1)
sell_order = MarketOrder('SELL', TEST_SHARES)
self.ib.placeOrder(contract, sell_order)
self.ib.sleep(3)
return True, f"Stop placed @ ${stop_price}, then cancelled and position closed"
return False, f"Stop status: {stop_trade.orderStatus.status}"
def test_2_1_guaranteed_fill(self):
"""Test 2.1: Guaranteed Fill (LIMIT with MARKET fallback)"""
contract = self.get_contract(TEST_SYMBOL)
quote = self.get_quote(contract)
# Aggressive limit price
limit_price = round(quote['ask'] * 1.003, 2) # 0.3% above ask
order = LimitOrder('BUY', TEST_SHARES, limit_price)
order.outsideRth = True
trade = self.ib.placeOrder(contract, order)
# Wait up to 10 seconds for fill
for _ in range(20):
self.ib.sleep(0.5)
if trade.orderStatus.status == 'Filled':
# Close position
sell_order = MarketOrder('SELL', TEST_SHARES)
self.ib.placeOrder(contract, sell_order)
self.ib.sleep(3)
return True, f"Limit filled @ ${trade.orderStatus.avgFillPrice:.2f}"
# Fallback to market
self.ib.cancelOrder(order)
self.ib.sleep(1)
market_order = MarketOrder('BUY', TEST_SHARES)
market_trade = self.ib.placeOrder(contract, market_order)
self.ib.sleep(3)
if market_trade.orderStatus.status == 'Filled':
# Close position
sell_order = MarketOrder('SELL', TEST_SHARES)
self.ib.placeOrder(contract, sell_order)
self.ib.sleep(3)
return True, f"Market fallback filled @ ${market_trade.orderStatus.avgFillPrice:.2f}"
return False, "Neither limit nor market filled"
def test_3_3_short_entry(self):
"""Test 3.3: Short Entry"""
contract = self.get_contract(TEST_SYMBOL)
order = MarketOrder('SELL', TEST_SHARES) # Short sell
order.outsideRth = True
trade = self.ib.placeOrder(contract, order)
self.ib.sleep(5)
if trade.orderStatus.status == 'Filled':
return True, f"Short filled @ ${trade.orderStatus.avgFillPrice:.2f}"
return False, f"Status: {trade.orderStatus.status}"
def test_3_4_short_exit(self):
"""Test 3.4: Short Exit (cover position from 3.3)"""
contract = self.get_contract(TEST_SYMBOL)
order = MarketOrder('BUY', TEST_SHARES) # Buy to cover
order.outsideRth = True
trade = self.ib.placeOrder(contract, order)
self.ib.sleep(5)
if trade.orderStatus.status == 'Filled':
return True, f"Cover filled @ ${trade.orderStatus.avgFillPrice:.2f}"
return False, f"Status: {trade.orderStatus.status}"
def test_5_1_invalid_symbol(self):
"""Test 5.1: Invalid Symbol - should error gracefully"""
try:
contract = Stock("ZZZZZ123", 'SMART', 'USD')
self.ib.qualifyContracts(contract)
return False, "Should have failed for invalid symbol"
except Exception as e:
return True, f"Gracefully handled: {str(e)[:50]}"
def run_all_tests(self):
"""Run all tests in sequence."""
if not self.connect():
return
try:
# Basic order types
self.run_test("1.1", "Market Buy", self.test_1_1_market_buy)
self.run_test("1.2", "Market Sell", self.test_1_2_market_sell)
self.run_test("1.3", "Limit Buy (at ask)", self.test_1_3_limit_buy_at_ask)
self.run_test("1.4", "Limit Buy (below bid)", self.test_1_4_limit_buy_below_bid)
self.run_test("1.5", "Limit Sell (at bid)", self.test_1_5_limit_sell_at_bid)
self.run_test("1.6", "Stop Loss (sell)", self.test_1_6_stop_loss_sell)
# Strategy flows
self.run_test("2.1", "Guaranteed Fill Entry", self.test_2_1_guaranteed_fill)
# Short selling
self.run_test("3.3", "Short Entry", self.test_3_3_short_entry)
self.run_test("3.4", "Short Exit", self.test_3_4_short_exit)
# Error handling
self.run_test("5.1", "Invalid Symbol", self.test_5_1_invalid_symbol)
finally:
self.disconnect()
# Print summary
self.print_summary()
def print_summary(self):
"""Print test summary."""
print("\n" + "="*60)
print("TEST SUMMARY")
print("="*60)
passed = sum(1 for r in self.results if r['status'] == 'PASS')
failed = sum(1 for r in self.results if r['status'] == 'FAIL')
errors = sum(1 for r in self.results if r['status'] == 'ERROR')
for r in self.results:
icon = "✅" if r['status'] == 'PASS' else "❌" if r['status'] == 'FAIL' else "⚠️"
print(f"{icon} {r['test_id']}: {r['test_name']} - {r['status']}")
if r['details']:
print(f" → {r['details']}")
print(f"\nTotal: {len(self.results)} tests")
print(f"✅ Passed: {passed}")
print(f"❌ Failed: {failed}")
print(f"⚠️ Errors: {errors}")
if __name__ == "__main__":
tester = PaperTradingTester()
tester.run_all_tests()
# SSH to server
ssh root@[hidden]
# Run test suite
cd [hidden]
source venv/bin/activate
python3 docs/paper_trading_tests.py
All tests should PASS. If any fail: 1. Check Gateway connection 2. Verify API settings (Read-Only = OFF) 3. Check market hours 4. Review error details
# Check if AAPL (NASDAQ) is fractional-eligible
is_eligible = client.is_fractional_eligible('AAPL')
assert is_eligible == True # NYSE/NASDAQ stocks should be eligible
# Check if OTC stock is NOT eligible (if you have one to test)
# is_eligible = client.is_fractional_eligible('OTCBB_STOCK')
# assert is_eligible == False
# Calculate fractional shares for $1,000 at $185.50
shares, is_fractional = client.calculate_fractional_shares('AAPL', 1000.0, 185.50)
assert is_fractional == True
assert abs(shares - 5.3908) < 0.0001 # Should be ~5.3908 shares
# Place a fractional limit order (paper trading)
result = client.place_limit_order(
symbol='AAPL',
quantity=1.5, # Fractional quantity
side='BUY',
limit_price=185.50,
extended_hours=True
)
assert result.is_fractional == True
# After an exit order fills, verify IBKR position is 0
result = executor.place_instant_exit('AAPL', direction='LONG', shares=1)
assert result.success == True
assert result.exit_verified == True
assert result.ibkr_position_at_exit == 0
# Simulate partial fill scenario (manual test)
# 1. Open position for 2 shares
# 2. Manually sell 1 share via different clientId
# 3. Bot attempts exit - should detect remaining shares
# Expected: exit_verified = False, alert triggered
# On orchestrator startup, check reconciliation log
# SELECT * FROM position_reconciliation_log ORDER BY run_time DESC LIMIT 1
# Expected: db_positions_checked >= 0, no mismatches for clean state
# Place order with clientId != 1 (e.g., via manual VNC/TWS)
# Start earnings-trader
# Check external_trades table
# Expected: Entry logged with detected_at timestamp
| Test | Description | Expected | Status |
|---|---|---|---|
| EV.1 | Real-time stop fill detection | When stop order fills in IBKR, _on_ibkr_fill() fires and publishes STOP_FILLED event. Position cleanup queued on main thread. |
Complete (Phase 3+4) |
| EV.2 | EventBus handler isolation | If one event handler throws exception, other handlers still execute | Verified (unit test) |
| EV.3 | Fill callback during disconnection | Callback not called when IBKR disconnected (no crash) | Complete (Phase 3+4) |
| EV.4 | IBKR position cache invalidation | Cache invalidated immediately after any order fill | Verified (code review) |
Last updated: February 7, 2026