Skip to main content

Synthetic after-hours quote generator

Project description

afterquote

Synthetic after-hours pricing for leveraged and cross-currency securities.

PyPI version PyPI downloads CI


The London market closes at 4:30pm. TSLA keeps trading until 9pm EST. If you hold 3TSL.L — a 3× leveraged ETP in GBp — you have no live price for the next several hours. afterquote fills that gap: it synthesises a real-time OHLC quote by applying the underlying's move (with leverage and FX adjustment) to the last known close.

pip install afterquote
from afterquote import SecurityPair

pair = SecurityPair("3TSL.L", "TSLA")
pair.info()
                                base_security underlying_security  base_is_live  leverage           base_close_time  base_close_price  adj_percent_return  quote_price
quote_time
2026-06-19 00:59:00+01:00        3TSL.L               TSLA         False         3 2026-06-18 16:30:00+01:00        177.869995            0.621451   178.975369

How it works

When the base exchange is closed and the underlying is still trading, afterquote builds a synthetic quote by decomposing each underlying bar into two multiplicative legs:

  • Gap return — inter-bar move (underlying open vs its previous close), scaled by leverage
  • Intra return — intra-bar move (underlying close vs its open), scaled by leverage

For cross-currency pairs (e.g. a GBp ETP tracking a USD stock), the FX rate is fetched and applied as a separate 1× leg — so leverage applies only to the underlying's return, not the currency move.

Every synthetic candle grows from a single anchor (the base's last close), so the chain is continuous and High ≥ max(Open, Close) ≥ Low holds by construction.


Usage

Synthetic quote

pair = SecurityPair("3TSL.L", "TSLA")

pair.info()
                                base_security underlying_security  base_is_live  leverage           base_close_time  base_close_price  adj_percent_return  quote_price
quote_time
2026-06-19 00:59:00+01:00        3TSL.L               TSLA         False         3 2026-06-18 16:30:00+01:00        177.869995            0.621451   178.975369
pair.pricing()
                              Impl_Open    Impl_High     Impl_Low   Impl_Close
Datetime
2026-06-18 16:30:00+01:00  177.869995  178.077587  177.733974  178.070422
2026-06-18 16:31:00+01:00  178.070422  178.185074  177.941470  177.941470
2026-06-18 16:32:00+01:00  177.927178  177.962971  177.769626  177.884217
2026-06-18 16:33:00+01:00  177.884217  177.934294  177.619327  177.741023
2026-06-18 16:34:00+01:00  177.762466  178.141756  177.762466  177.991464
...
2026-06-19 00:59:00+01:00  178.946613  179.061640  178.946613  178.975369

[509 rows × 4 columns]

Confidence band

pair.info(confidence=0.95)
                                base_security underlying_security  base_is_live  leverage           base_close_time  base_close_price  adj_percent_return  quote_price  lower_bound  upper_bound
quote_time
2026-06-19 00:59:00+01:00        3TSL.L               TSLA         False         3 2026-06-18 16:30:00+01:00        177.869995            0.621451   178.975369      176.420      181.530

lower_bound / upper_bound come from the empirical distribution of past prediction errors — no Gaussian assumption. The band is asymmetric when errors are skewed.

Benchmark

from afterquote import benchmark, metrics

results = benchmark(pair, days=90)
print(results)
            base_close   synth_open  actual_open    residual  direction_correct
2026-03-26  148.320007  163.052340  152.440002    10.612338               True
2026-03-27  152.440002  144.918760  161.800003   -16.881240              False
2026-03-28  161.800003  174.338120  168.220001     6.118119               True
2026-03-31  168.220001  159.774480  163.559998    -3.785518               True
2026-04-01  163.559998  141.832900  129.680008    12.152892               True
...
2026-06-18  177.869995  179.341200  178.240005     1.101195               True

[62 rows × 5 columns]
metrics(results)
{'rmse': 74.1, 'mae': 58.3, 'direction_correct': 0.71, 'tracking_error': 61.2, 'n': 62}

The model called the direction right 71% of the time over 62 sessions.

Correlation health check

pair.correlation()
# 0.7612

Pearson daily-return correlation between base and underlying over the last 90 days. Emits UserWarning when |corr| < 0.5.

Portfolio P&L

holdings.csv:

base,underlying,quantity
3TSL.L,TSLA,1000
3USL.L,SPY,500
from afterquote import portfolio_pnl

portfolio_pnl("holdings.csv")
     base underlying  quantity  base_close_price  quote_price      pnl
   3TSL.L       TSLA    1000.0        177.869995   178.975369  1105.37
   3USL.L        SPY     500.0        312.540001   313.706240   583.12
    TOTAL                1500.0               NaN          NaN  1688.49

Point-in-time queries

All methods accept as_of for historical reconstruction — no look-ahead bias:

pair.info(as_of=pd.Timestamp("2026-06-18 20:00:00-04:00"))
pair.pricing(as_of=pd.Timestamp("2026-06-18 20:00:00-04:00"))

CLI

afterquote 3TSL.L TSLA                                    # synthetic quote
afterquote 3TSL.L TSLA --confidence 0.95                  # with confidence band
afterquote 3TSL.L TSLA --pricing                          # full OHLC bars
afterquote 3TSL.L TSLA --benchmark                        # backtest + metrics
afterquote 3TSL.L TSLA --correlation                      # correlation check
afterquote 3TSL.L TSLA --as-of "2026-06-18 20:00-04:00"  # historical query
afterquote --holdings holdings.csv                        # portfolio P&L
$ afterquote 3TSL.L TSLA --benchmark

            base_close   synth_open  actual_open    residual  direction_correct
2026-03-26  148.320007  163.052340  152.440002    10.612338               True
...
2026-06-18  177.869995  179.341200  178.240005     1.101195               True

  rmse: 74.1
  mae: 58.3
  direction_correct: 0.71
  tracking_error: 61.2
  n: 62
$ afterquote 3TSL.L TSLA --correlation

correlation: 0.7612

Demo pairs

Pair Leverage FX Use case
3TSL.L / TSLA GBp → USD Both leverage and FX active — the full model
3USL.L / SPY Leverage only — same-currency baseline

Testing

pip install -e ".[test]"
pytest tests/          # 68 unit tests, mocked, ~0.4s — no network calls
pytest tests/ --runlive  # + live yfinance validation

Contributing

Issues and PRs welcome. — Junaid

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

afterquote-1.0.0.tar.gz (23.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

afterquote-1.0.0-py3-none-any.whl (14.8 kB view details)

Uploaded Python 3

File details

Details for the file afterquote-1.0.0.tar.gz.

File metadata

  • Download URL: afterquote-1.0.0.tar.gz
  • Upload date:
  • Size: 23.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for afterquote-1.0.0.tar.gz
Algorithm Hash digest
SHA256 067053586a1fe6dd143419ea11e9d64cd3e9d94f21e93dac69b47d256d019c7d
MD5 45a46a2c55ef68cc52acda34c33e719a
BLAKE2b-256 c6a735c3faeda54f9e5c2edf28a39dfa36ab46baca6d290d9586e868891ac5a5

See more details on using hashes here.

File details

Details for the file afterquote-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: afterquote-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 14.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for afterquote-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dba07942cf9b1dbd9e382b68614d96aa250077e3f0eab0fbe75847a097ac376a
MD5 c91f1b7cda96fcf2cb420ef708e3d83c
BLAKE2b-256 815680e293bde3b2e49d4e3f59bfa9c17837d75b23f0b32dd67ddead45790bcd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page