A Taiwan index options trading prototype built with Python, Shioaji, and PyQt6. The system retrieves TX-series option contracts and live snapshots through Shioaji, selects call and put contracts, monitors open positions, and provides a graphical interface for triggering orders.
This project can connect to a live brokerage account and submit market orders. Validate it thoroughly in a simulation or isolated environment before use. This project does not constitute investment advice.
See the original author's Google Slides presentation for additional strategy background.
The code contains the following TAIEX options trading functionality:
- Filters
TX-series and nearest-expiry contracts fromContracts.Options. - Builds candidate lists for both calls and puts.
- Uses Shioaji to query market data and account positions and to submit option sell and closing orders.
- Uses PyQt6 to display trading-session status, suggested contracts, current positions, and unrealized profit and loss.
The strategy is intended to establish a two-sided short call/put position. The current implementation selects each leg independently and does not require the call and put to share the same strike. It may therefore create a short strangle rather than a strict short straddle. A strict straddle strategy must add a call_strike == put_strike constraint to the signal module.
The system is designed around four isolated responsibility boundaries. The PyQt6 interface is responsible only for displaying state and emitting user actions:
| Module | Responsibility | Current implementation |
|---|---|---|
| Market data | Login, certificate activation, contract discovery, snapshots, and account positions | api.py, getSymbol.py, snapshot.py, getPositionSS.py |
| Signal | Score candidate strikes and produce call/put contract suggestions | SuggestContract.py |
| Risk management | Validate trading hours, positions, take-profit/stop-loss rules, exposure, and permission to trade | Risk logic currently lives in the coordinator and should be extracted into a side-effect-free module |
| Execution | Convert approved trade intents into Shioaji orders, closing orders, and execution reports | order.py |
The high-level data flow is:
Shioaji → Market Data → Signal → Risk Management → Execution
│ │
└──── PyQt6 Live Monitor ────┘
ui.py implements the monitoring interface. main.py uses QThread to separate backend updates from the GUI event loop so market-data polling does not block the interface.
findContracts(snapshot, target) is not a trained statistical model. Among contracts whose bid is below the target premium, it selects the contract with the highest bid:
arg max(bid_i), subject to bid_i < target
Calls and puts use separate target premiums and may therefore produce different strikes. This is an explainable threshold-based baseline, but it does not yet account for implied volatility, Delta, liquidity, time to expiration, margin requirements, or tail risk.
A statistical implementation of the signal module should:
- Build features from the underlying price, historical and implied volatility, time to expiration, bid-ask spread, volume, and Greeks.
- Estimate the terminal-price distribution or the probability of each strike being breached.
- Calculate expected premium, expected loss, CVaR or stress-scenario losses, and capital usage for each candidate combination.
- Select the highest-scoring combination subject to liquidity, maximum-loss, margin, and position-size constraints.
- Enforce the same strike for both legs when trading a straddle. If strangles are allowed, explicitly constrain the distance between strikes and total Delta.
The statistical model should only produce recommendations. It must not call the brokerage API directly; every order must still pass through the independent risk-management module.
The current interface displays:
- Whether the market is within configured trading hours.
- A countdown to the next data refresh.
- Suggested call and put contracts with live prices.
- Existing call and put positions with unrealized profit and loss.
- A button for establishing the initial two-sided position.
A production interface should also show connection state, order and fill reports, rejection reasons, risk-lock status, and an emergency stop control.
Python 3.11 or later is required. The recommended package manager is uv:
uv syncCreate a deployment-only .env file and never commit real values:
API_KEY=
SECRET_KEY=
CA_CERT_PATH=/secure/path/to/broker-certificate.pfx
CA_PASSWORD=Restrict .env so that only the service account can read it, and store the brokerage certificate in a protected directory outside the repository. The current code logs into the live environment when api.py is imported. Before production deployment, replace this behavior with an explicit startup flow and add safeguards that clearly separate simulation from live trading.
Use a dedicated, least-privilege Linux service account on a GCE VM:
- Install Python 3.11,
uv, and the system libraries required by PyQt6. - Deploy the application to a directory such as
/opt/sj-trading, while keeping.envand.pfxfiles outside the repository. - Inject secrets through Secret Manager or an environment file with
0600permissions. - Use
systemdfor process supervision, automatic restarts, and log rotation. - PyQt6 requires a display server. Use a protected VNC or remote-desktop session for interactive monitoring, or
xvfb-runfor headless execution. - Open only the required firewall rules and apply least-privilege permissions to the VM, service account, and Secret Manager.
Example headless command:
xvfb-run -a uv run python -m sj_trading.mainmain.py currently depends on a local coordinator module that is not tracked by Git. Before a fresh clone can run, that module must be sanitized and added to version control, or its coordination, risk-management, and notification responsibilities must be refactored into deployable modules.
The following must never be committed:
.envand environment-specific variants.- Shioaji API keys, secret keys, identity details, and account passwords.
- Brokerage CA certificates and passwords, including
.pfx,.p12,.pem, and.keyfiles. - Telegram or other notification-service tokens.
- Trading data and runtime logs.
Adding an entry to .gitignore is not enough if a secret has already appeared in a commit. Revoke or rotate the secret first, then remove it from history with git filter-repo or an equivalent tool and coordinate a fresh clone for every contributor.