ib_0dte
GitHub基于IBKR API自动执行0DTE信用价差交易,结合EMA交叉与VIX/VXN波动率 regime 筛选策略,支持指数及股票ETF的自动下单。
Trigger Scenarios
Install
npx skills add staskh/trading_skills --skill ib_0dte -g -y
SKILL.md
Frontmatter
{
"name": "ib_0dte",
"description": "Find and place the best 0DTE (zero-days-to-expiration) credit spreads from Interactive Brokers. Default execution route is the EMA9\/EMA21 + VIX\/VXN regime strategy (ema_vix_0dte.py), which auto-selects bull_put or bear_call from a bare EMA cross and skips the trade when the vol index is elevated (VXN ≥ 35 for NDX\/QQQ, VIX ≥ 20 otherwise). Optional --rr-gate and --time-gate add red→red and bar-timing confirmation. Supports cash-settled indices (SPX, NDX, RUT, VIX) and any optionable stock\/ETF. Manual spread type override is available via zero_dte.py. Requires TWS or IB Gateway running locally.",
"dependencies": [
"trading-skills"
]
}
IB 0DTE Credit Spread Finder & Executor
Default execution route: ema_vix_0dte.py — the EMA9/EMA21 + VIX/VXN regime
strategy. It reads the 30-min IB bars, checks the vol index (VXN ≥ 35 for NDX/QQQ,
VIX ≥ 20 otherwise), and auto-selects bull_put or bear_call from a bare EMA cross (or
skips entirely) before delegating to the spread finder. Optional --rr-gate and
--time-gate add confirmation. Use this unless the user explicitly requests a
manual spread type.
zero_dte.py is the manual override when the user specifies --type bear_call,
--type bull_put, or --type iron_condor directly.
Both scripts share the same spread-finding engine (find → propose → execute on
--execute) and all the same flags. All data comes from IBKR.
Supports cash-settled indices (SPX, NDX, RUT, VIX, XSP, DJX) — which trade as
Index contracts on their home exchange — as well as any optionable stock or ETF.
Prerequisites
TWS or IB Gateway running locally with the API enabled:
- Paper trading: port 7497 (default)
- Live trading: port 7496
Index options require the appropriate index-options market-data entitlement (separate from equity/ETF data). Without it, index quotes will not populate.
Instructions
EMA + VIX/VXN Strategy (recommended — fully automatic signal)
The script checks the vol index, reads the 30-min bars, determines bull_put vs bear_call from the EMA9/EMA21 cross, then calls the spread finder automatically. By default it runs a bare EMA cross with no bar-timing or red→red gate, so it can be run at any time of day. Opt into the confirmation gates per run.
# Dry run (propose only, no order placed) — bare EMA cross.
# Budget auto-sizes to 50% of the account's excess liquidity.
uv run python scripts/ema_vix_0dte.py NDX --account U790497 --port 7496
# With both confirmation gates on (original 10:30-ET behavior)
uv run python scripts/ema_vix_0dte.py NDX --account U790497 --port 7496 \
--rr-gate --time-gate
# Live execution
uv run python scripts/ema_vix_0dte.py NDX --account U790497 --port 7496 --execute
# Explicit budget override (skips the excess-liquidity lookup)
uv run python scripts/ema_vix_0dte.py NDX --budget 50000 --port 7496 \
--account U790497 --execute
# SPX variant
uv run python scripts/ema_vix_0dte.py SPX --account U790497 --port 7496 --execute
Vol index: NDX/QQQ are gated on VXN (CBOE Nasdaq-100 Volatility Index —
the correct vol gauge for a Nasdaq-100 trade); all other symbols on VIX. The
default cutoff is per-index — VXN 35, VIX 20 (VXN typically prints several
points above VIX for the same regime). The index used is echoed as vol_index in
the output; the vix_* fields hold that index's readings.
The gate is dual: both the intraday reading and the prior-day close must
sit below the cutoff, since a market recovering from a high-vol close is still
fragile. Both come from IB (vix_source: "ib") — one decision is never assembled
from two data sources. Without both readings the run stands down rather than
guessing (signal: "VOL-UNAVAILABLE").
Signal logic (default — exits early with success: false and a reason on skip):
- No vol reading (intraday or prior-day missing) → no trade (
signal: "VOL-UNAVAILABLE") - Vol index ≥ cutoff (VXN ≥ 35 for NDX/QQQ, else VIX ≥ 20, either reading) → no trade (
signal: "VIX-SKIP") - EMA9 last crossed above EMA21 →
bull_put(signal: "EMA-Up") - EMA9 last crossed below EMA21 →
bear_call(signal: "EMA-Dn")
Optional confirmation gates (both off by default):
--rr-gate— an EMA-down becomes a Bear Call only if today's two most recently closed bars are both red (signal: "EMA-Dn+RR"); otherwise no trade ("EMA-Dn-no-RR"). The confirmation tracks momentum at the moment of the run, so it means the same thing at 10:30 as at 15:00. A bar still in progress cannot confirm anything and is not counted.--time-gate— require today's 9:30 + 10:00 ET bars (run at 10:30 ET or later) and anchor the EMA-cross lookback to the 10:00 ET bar. Without it the lookback anchors to the latest available bar.
Additional flags:
--vix-threshold N— override the vol-index cutoff (default: per-index — VXN 35, VIX 20)--target-delta 0.12— short-leg delta target (default: 0.12, ≈1.5% OTM at VIX<20)- All other
zero_dte.pyflags (--max-width,--gex,--stop-mult, etc.) pass through
Manual spread finder (explicit type)
Optionally confirm a same-day (0DTE) expiry exists first:
uv run python scripts/zero_dte.py SYMBOL --expiries
Then find the best spreads (dry run — proposes only, places nothing):
uv run python scripts/zero_dte.py SYMBOL --type bear_call --budget 2000
Execute the chosen spread (places a live combo order):
uv run python scripts/zero_dte.py SYMBOL --type bear_call --budget 2000 \
--account U1234567 --execute # places the best pick
uv run python scripts/zero_dte.py SYMBOL --budget 2000 \
--account U1234567 --execute --pick 2 # places the 2nd-ranked pick
Arguments
SYMBOL— underlying (e.g.SPX,NDX,RUT,VIX,AAPL,SPY)--type—bear_call(default, bearish/neutral),bull_put(bullish/neutral), oriron_condor(neutral)--budget— max capital at risk in dollars. Caps total max loss; position size isfloor(budget / max-loss-per-spread). Default: auto-sized from the live account cushion — see Budget sizing below.--budget-frac— fraction of excess liquidity to deploy when auto-sizing (default:0.5). Ignored when--budgetis passed.--account— IBKR account the trade is committed to. Validated against the connection's managed accounts; echoed in the output. Defaults to the sole managed account when the login has exactly one. Required with--executewhen the login manages more than one account.--execute— place the chosen spread as a live combo order. Without it (the default), the tool is a dry run: it proposes but places nothing.--pick N— 1-based rank of the candidate to execute (default: 1 = best).--limit— absolute net-credit limit override. Default: the candidate'scombo_ask_credit(the marketable BUY-side of the combo NBBO —sum(short-leg bids) − sum(long-leg asks)), which fills at market. Passing a higher credit here (e.g. mid) is likely to sit at the combo bid and not fill.--limit-frac— walk between the combo NBBO's marketable side and its mid:combo_ask + frac × (net_credit − combo_ask).0= fully marketable (same as default),0.5= midpoint of combo NBBO,1.0= mid credit (rarely fills on multi-leg BAG combos). Computed at execution time, so it stays anchored to the fresh pull's combo quote. Ignored if--limitis set. (Note: IB paper often won't fill multi-leg 0DTE index combos at any marketable price — a paper-sim limitation, not a pricing issue.)--replace— if a live order for this symbol/expiry/type already rests, cancel and re-place it (default: refuse as a duplicate).--stop-mult— premium-cap stop: close when the spread reaches this multiple of the credit (default:2.0= lose ~1× credit).0disables the premium cap.--stop-buffer— points before the short strike to trigger the level stop (default:0= at the strike).--stop-delta— also stop when the short-leg delta reaches this level (optional, e.g.0.30).--profit-target— buy back after capturing this fraction of the credit, e.g.0.5= 50% (0disables). Default: per-symbol preset, else0.50.--time-exit— flatten remaining spreads at this ET time, e.g.15:30(nonedisables). Default: per-symbol preset, else15:30.--fill-timeout— seconds to wait for the entry to fill before cancelling it (default:60). The bracket needs a fill; if the entry doesn't fill it's cancelled so you're never unprotected.--verify-stops— check that every open 0DTE spread has a resting protective stop, then exit (no symbol required). Add--repairto place a strike-level stop on any unprotected position.--repair— with--verify-stops, auto-place a strike-level stop on unprotected positions.
Stop and exit defaults come from per-symbol presets (STOP_PRESETS in zero_dte_stop.py) — each maps mult, buffer, delta, target (profit-take), and time_exit. E.g. NDX uses mult 3.0 + 0.30 delta backstop, 50% target, 15:30 exit; SPX mult 2.5; unlisted symbols mult 2.0. Any explicit flag overrides the preset. Entry short-delta caps are separate (ENTRY_MAX_DELTA: 0.12 index / 0.20 stock). These are starting points; tune them with live data.
--expiry YYYYMMDD— override the expiry (default: today ET, i.e. true 0DTE)--top— number of candidates to return (default: 5)--min-pop— minimum probability of profit, 0–1 (default: 0, no filter)--max-width— cap the strike width in dollars (optional)--delta— cap the|delta|of the short leg(s) at entry. Applies to both verticals and (both short legs of) iron condors. Defaults by class: 0.12 for indexes, 0.20 for stocks (ENTRY_MAX_DELTAinzero_dte.py); pass a value here to override. The effective cap is echoed asmax_short_deltain the output.--gex— compute the dealer gamma-exposure profile (net GEX, gamma flip, call/put walls), annotate each candidate against the walls, and gateentry_qualityon the regime. See Gamma exposure (GEX) below. Costs an extra chain fetch (it pulls both option sides).--gex-weight— size measure behind each strike:volume(today's prints),oi(prior settlement's open interest), orauto(default: volume once it has printed, else OI).--allow-stale— if IBKR streams no live quotes/greeks (off-hours), price legs from yesterday's settlement close and derive greeks via Black-Scholes. Off by default: greeks come only from IBKR, so a closed market returns no candidates (with a hint) rather than stale, model-computed ones.--no-events— skip the live economic-calendar lookup (falls back to static event guidance). The calendar is fetched by default and needs no API key.--expiries— list available expiries and whether today has a 0DTE--port— IB port (default: 7497 paper; use 7496 for live)
Budget sizing (automatic)
When --budget is omitted, capital-at-risk is sized from the account's live margin
cushion: the run reads ExcessLiquidity for the resolved account off the connection
it already holds and sets
budget = ExcessLiquidity × --budget-frac (default 0.5)
A defined-risk vertical consumes roughly its max loss in margin, so the budget cap and the margin actually drawn are the same number — deploying the full cushion would leave zero buffer before IB's forced liquidation, which is why the default is half.
The output echoes budget, budget_source (excess_liquidity or explicit),
budget_frac, and the excess_liquidity reading it was derived from. Surface the
cushion and the derived budget when presenting candidates so the sizing is visible.
The lookup uses the same IB connection as the chain fetch — no second connect, so no client-ID collision.
It needs to know which account. Margin does not cross account boundaries, so the
budget can only be sized once an account resolves — automatic on a single-account login,
otherwise --account is required. Rather than fall back to an arbitrary default, the
run aborts with success: false and an explanatory error when:
- no account resolved (multi-account login without
--account), ExcessLiquiditycould not be read for that account, or- the cushion is ≤ 0 — no free margin means no new 0DTE spread; free margin first, or
pass
--budgetto override deliberately.
Pass --budget N any time you want a fixed number; it wins over the lookup entirely
(including --budget 0, which is honored as a real value, not as "unset").
Executing a trade
--execute places the chosen spread as a single native BAG combo — both legs (or
all four, for an iron condor) fill together, so IBKR margins it as one defined-risk
spread. The order is a limit at the net credit (BUY the combo at -credit, which
only fills at or better than that credit), time-in-force DAY (0DTE), tagged
orderRef=ZDTE_<type>_<symbol>_<expiry>, and routed to --account.
Guardrails before an order is sent:
- The account must resolve (explicit
--account, or a sole managed account). --pickmust be within the ranked list.- The chosen spread's sized max loss must still fit
--budget. - Duplicate guard — if an active order with the same
orderRef(ZDTE_<type>_<symbol>_<expiry>) already rests in the account, the placement is refused (reports the existingorder_id). Pass--replaceto cancel and re-place it instead. Prevents a second--executefrom stacking a duplicate spread.
The connection is read-only unless --execute is passed, so a plain analysis run
can never place an order. Confirm the proposal with the user before executing.
Exit bracket (automatic, non-negotiable on --execute)
Every --execute atomically attaches a full OCA exit bracket — you can never end
up holding an unmanaged 0DTE position:
- The entry combo is placed and the tool waits for it to fill (
--fill-timeout). - If it doesn't fill, the entry is cancelled (no position, no risk) and the result
reports why — retry with a marketable
--limitor during liquid hours. - On fill, three closing orders are placed in one OCA group (whichever fills first
cancels the others, server-side):
- Profit target — a resting limit to buy back at
(1 − target) × credit. - Stop — a conditional order that buys back when the underlying breaches the stop level.
- Time exit — a conditional order that flattens at the
--time-exitET time. If bracket placement fails, the position is emergency market-closed immediately.
- Profit target — a resting limit to buy back at
The profit target naturally captures near-worthless winners well before the timer, so the time exit mainly flattens positions still hovering near breakeven into the close.
The stop trigger is level-anchored on the underlying (robust to option-price noise), taking whichever of these fires first:
- Short strike (±
--stop-buffer) — the "thesis broken" level. - Premium cap (
--stop-mult) — the underlying level where the loss reachesmult × credit, computed via Black-Scholes at entry. - Short delta (
--stop-delta, optional).
Bear call → stops if the index rises; bull put → if it falls; iron condor → two OCA-linked stops (either breach closes the whole condor). The close is a marketable limit capped at the spread width — fills at market but never worse than the defined max loss. A stop reduces the average loss; it does not guarantee the price in a gap, so the budget-capped max loss remains the true floor.
Verifying / repairing stops
uv run python scripts/zero_dte.py --verify-stops --account U1234567 # report
uv run python scripts/zero_dte.py --verify-stops --repair --account U1234567 # auto-fix
Scans open 0DTE option positions per account and buckets them into protected
(a resting ZDTE_STOP_… order exists), unprotected, and unrecognized (legs that
don't form a recognized spread). With --repair, each unprotected recognized spread
gets a strike-level stop (± the symbol's preset buffer) — a safety net that needs
no live market data. Run it after entering trades, and periodically, to confirm nothing
is naked.
How ranking works
- Probability of profit (POP) — probability the spread finishes a winner: the
underlying stays on the safe side of the short strike(s) at expiration. From the
IBKR short-leg delta (
POP ≈ 1 − |delta|), BSN(d2)fallback. - Expected value (EV) — the expected P&L integrated over a lognormal with
realized vol =
--rv-ratio × implied(default 0.85). This counts partial losses (breach just past the short is a small loss), not a flat max loss — which fixes the far-OTM bias of the old binaryPOP×maxP − (1−POP)×maxL.ev_modelis echoed (expected_pnl_rv0.85); if a leg has no IV it falls back to the binary formula.- Lower
--rv-ratio= more credit-hungry (favors richer near-the-cap strikes);1.0≈ fair (EV near zero — don't use). It's an assumption, not a guarantee.
- Lower
What --rv-ratio 0.85 actually is
It is an unvalidated prior, not a measured quantity. No backtest in this repo established 0.85; it has not been fitted to SPX/NDX 0DTE data, and it should not be read as one.
The idea behind it is real — the volatility risk premium: implied vol tends to
print above subsequently realized vol, which is why selling premium has an edge at
all. 0.85 says "assume realized comes in 15% under implied". The direction is
well documented; this particular number is a round guess at the magnitude.
Why that matters when reading the output:
- The ratio is the edge. At
--rv-ratio 1.0expected EV goes to roughly zero. So every positiveev_totalthe tool reports is a restatement of the assumption, not independent evidence that the trade is good. Rank order between candidates is far more trustworthy than the absolute figures. - It is least reliable when it matters most. The premium compresses or inverts
exactly when realized vol spikes — the sessions where a 0DTE credit spread does
its real damage. The EV model has no awareness of regime; pair it with the
--gexread and thetiming.eventswarnings rather than trusting it alone.
To see how much of a candidate's EV is the assumption, re-run with
--rv-ratio 1.0 and compare. If EV collapses, the edge was the prior.
Treat it as a tunable, and tune it against your own fills — the paper-test report
(scripts/report.py) exists for exactly that.
--target-delta— pin the short leg(s) to a delta (± 0.05) for direct strike control, e.g.--target-delta 0.15. Still bounded by--delta.- Candidates are ranked by total EV, ties broken toward higher POP.
Output
Every run is automatically saved to sandbox/ as timestamped JSON (e.g.
NDX_0dte_bear_call_exec_2026-07-10_093015.json), and the path is echoed as
saved_to. This gives a durable trade log — including the order.bracket /
binding details that TWS alone doesn't reconstruct. No flag needed.
JSON with:
underlying_price,expiry,dte,spread_type,budget,asset_type,accountbudget_source(excess_liquidity/explicit),budget_frac,excess_liquidity— how the budget was arrived at (see Budget sizing)dry_run—trueunless--executewas passedtiming— built-in intraday guidance (see Timing & event guidance below)gex— dealer gamma-exposure profile when--gexis passed, elsenull(see Gamma exposure (GEX) below):regime,net_gex_bn,flip_level,call_wall,put_wall,heaviest_strikes,weight_source,coverage,caveats, and aguidancesub-blockbest— the top-ranked spreadcandidates— top-N spreads, each withlegs(action, right, strike, bid, ask, mid, delta, iv),net_credit(mid credit, used for max_profit/EV/breakevens),combo_ask_credit(marketable BUY-side of the combo NBBO — the default execution limit; what fills at market),combo_bid_credit(resting BUY bid — best possible credit, rarely fillable),width,pop,contracts, per-contract and totalmax_profit/max_loss,capital_at_risk,ev_total,breakeven(s),risk_reward,short_delta, anddistance_to_short/distance_to_short_pct(spot-to-short-strike cushion). Iron condors reportshort_call_delta/short_put_deltaandcall_distance_to_short/put_distance_to_short.picked— the 1-based rank executed (only when--execute)order— the placement result when--execute:order_id,status,filled,remaining,quantity,limit_price(negative = net credit),account,order_ref,entry_status,log(IB's status/error messages for this order — surfaces the reject reason when an entry goes terminal-Cancelled without filling), andbracket(the attached OCA exit bracket:profit_target(limit_debit),stops(each side'strigger/bindinglevel /order_id), andtime_exit(cutoff); or{"ok": false, ...}with anemergency_closeif bracket placement failed); or{"ok": false, "error": ...}if a guardrail blocked it
Present the top candidates as a table with columns: strikes, credit, POP,
short delta, distance-to-short (points and %), max profit, max loss, contracts, EV.
Lead with the best pick and state the direction and the price level it needs the
underlying to respect (the short strike / breakeven) — the distance-to-short is the
cushion before the trade starts losing.
Paper-test report
Aggregate your saved runs into a summary:
uv run python scripts/report.py # text summary
uv run python scripts/report.py --json # machine-readable
- Entries (from
sandbox/*_exec_*.json, automatic): trades placed by symbol/type, entry short-delta range, avg POP, capital at risk, and the stop level placed (binding) distribution. - Outcomes (from the latest
ib_0dte_paper_test_log_*.mdyou fill in): win rate, avg win/loss, expectancy per trade, max drawdown, and the actual closed-by leg distribution. Realized P&L only exists once a trade resolves, so it comes from the daily log'sP&L/Closed bycolumns — the entry JSON only captures placement.
Watch expectancy, not win rate: a high win rate with one fat loss can still be negative (the report makes that obvious).
Gamma exposure (GEX)
--gex adds a gex block estimating how much gamma market makers hold across the
chain, and what their hedging of it does to the tape:
uv run python scripts/zero_dte.py SPX --type bear_call --budget 2000 --gex
Per strike, GEX = gamma × size × 100 × spot² × 0.01 — the dollars of dealer delta
that must be re-hedged per 1% move. Net GEX is calls minus puts, on the standard
assumption that dealers are long call gamma / short put gamma (customers buy puts
and sell calls).
regime—positive_gamma: dealers hedge against the move (sell rallies, buy dips), so vol is suppressed and price mean-reverts — the supportive regime for short premium.negative_gamma: they hedge with the move, amplifying it — the regime that runs credit spreads over.neutral_gamma: the book is balanced, no edge either way.flip_level— the spot where net GEX crosses zero, found by re-deriving every strike's gamma across a ±5% spot grid. Above it you're in the suppressive regime, below it the amplifying one.guidance.spot_vs_flipsays which side you're on.call_wall/put_wall— the heaviest gamma strikes above and below spot. Dealer hedging tends to defend them, so they act as barriers.guidance.strike_guidance— where to put the short leg: at or beyond the wall (bear call ≥ call wall, bull put ≤ put wall), so the level dealers defend stands between spot and your short strike.- Each candidate gets a
gextag (beyond_wall/at_wall/inside_wallper short leg, withdistance_to_wall) and agex_okboolean.inside_wallmeans price can reach your short strike without ever contesting the wall. - The regime gate: a
negative_gammaread downgradestiming.entry_qualityone notch (best/good → fair → avoid) and recordstiming.gex_gatewith the original value and the reason. The clock only knows the time of day; the regime knows whether today's hedging damps moves or feeds them.
GEX is advisory only — it never changes strike selection, sizing, or the order path.
Surface gex.regime, the walls, and any guidance.warnings alongside the timing block.
Weighting caveat (read this). IBKR's open interest is the prior settlement's — on
a 0DTE expiry most of the book is opened the same morning, so OI misses the flow that
actually drives today's hedging. auto therefore weights by same-day volume, which
sees today's flow but double-counts round-trips and can't tell an opening trade from a
closing one. weight_source and caveats in the output state which was used. A GEX
number built on the wrong measure can point at the wrong wall entirely — treat it as a
regime filter and a strike-placement prior, not as an edge to lean on hard. The
dealer long-call/short-put assumption itself fails when customers are the ones buying
calls (a squeeze), which inverts the profile.
Timing & event guidance
Every run includes a timing block, computed from the current ET clock:
window—pre_market/opening_bell/morning_prime/midday/afternoon/power_hour/after_hours/weekendentry_quality—best/good/fair/avoid/closed, tailored to the spread type (credit spreads favor mid-morning; iron condors favor the midday lull; the open and the final power hour areavoid)recommendation— one-line plain-English guidance for the current windowevents— event-risk guidance, withsource:source: "nasdaq"(live economic calendar, keyless, via Nasdaq): reportsevents_today(each withevent,time_et,impact,actual/consensus/previous),high_impact_today(FOMC/CPI/PPI/NFP/PCE/GDP/retail/ISM + Fed-chair remarks), andwarningsfor high-impact and imminent (within ~30 min) releases.source: "static"(fallback when the calendar can't be reached, or--no-events): recurring intraday-window warnings plus averify_before_tradingchecklist.
- Timing is holiday/early-close aware via the NYSE calendar (half-days shift the close and power-hour windows).
Surface this prominently. Before proposing or (especially) executing, state the
timing.recommendation and any timing.events.warnings; if entry_quality is avoid
or closed, call that out and suggest waiting. Lead with any high-impact event today
(e.g. "FOMC at 14:00 ET") — it can gap the index straight through the short strikes.
The live calendar is fetched by default (no API key needed); pass --no-events to skip
it. Event data is US macro releases; for stock underlyings also confirm earnings.
Data sourcing
- Prices and greeks come from IBKR. Leg prices are IBKR bid/ask/last; delta and IV are IBKR model greeks. The skill only does the spread arithmetic (mid, credit, width, max P&L, POP, EV, distances) on top of that data.
- By default, if IBKR streams no live quotes/greeks (market closed), the skill returns
no candidates plus a
hint— it will not compute greeks itself. --allow-staleopts into an off-hours fallback: price from yesterday's settlementcloseand derive IV/delta via Black-Scholes. Flagged asdata_delay: "stalled - using yesterday's close". Useful for previews only — those marks are stale, so numbers (especially far-OTM credits) aren't tradeable.
Notes
- POP and EV are model-implied under standard (lognormal) assumptions — estimates of the odds, not guarantees.
- A credit spread's max loss is realized if price blows through the long wing; size accordingly. The budget caps that defined max loss, not slippage or gap risk.
Timezone
All timestamps and time-based calculations use America/New_York. JSON output
includes generated_at (NY time string) and data_delay.
Version History
- 9a1bf57 Current 2026-09-22 03:57
Dependencies
-
required
trading-skills


