afterquote 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- afterquote-0.1.1/LICENSE +21 -0
- afterquote-0.1.1/PKG-INFO +68 -0
- afterquote-0.1.1/README.md +50 -0
- afterquote-0.1.1/afterquote/__init__.py +8 -0
- afterquote-0.1.1/afterquote/_market_calendar.py +102 -0
- afterquote-0.1.1/afterquote/_security_pair.py +88 -0
- afterquote-0.1.1/afterquote/_yfinance_wrapper.py +78 -0
- afterquote-0.1.1/afterquote.egg-info/PKG-INFO +68 -0
- afterquote-0.1.1/afterquote.egg-info/SOURCES.txt +12 -0
- afterquote-0.1.1/afterquote.egg-info/dependency_links.txt +1 -0
- afterquote-0.1.1/afterquote.egg-info/requires.txt +8 -0
- afterquote-0.1.1/afterquote.egg-info/top_level.txt +1 -0
- afterquote-0.1.1/pyproject.toml +25 -0
- afterquote-0.1.1/setup.cfg +4 -0
afterquote-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Junaid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: afterquote
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synthetic after-hours quote generator
|
|
5
|
+
Author-email: Mohammad Junaid <mohammadjunaiduk@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: yfinance
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: pytz
|
|
13
|
+
Requires-Dist: pandas_market_calendars
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: black; extra == "dev"
|
|
16
|
+
Requires-Dist: pylint; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# afterquote
|
|
20
|
+
|
|
21
|
+
**Synthetic after-hours quote generator based on an asset and its correlated underlying.**
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ๐ฆ What is this?
|
|
26
|
+
|
|
27
|
+
`afterquote` lets you estimate synthetic prices for a financial security based on the real-time performance of a given correlated underlying asset โ useful when one market is closed and the other is still trading.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ๐ Installation
|
|
32
|
+
|
|
33
|
+
### From PyPI (planned):
|
|
34
|
+
```bash
|
|
35
|
+
pip install afterquote
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Locally:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## ๐งช Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from afterquote import SecurityPair
|
|
48
|
+
|
|
49
|
+
pair = SecurityPair("MAG5.L", "MAGS")
|
|
50
|
+
quote_df = pair.quote()
|
|
51
|
+
print(quote_df)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## ๐ Example Output
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
base_security underlying_security leverage base_close_time base_close_price adj_percent_return quote_time quote_price
|
|
58
|
+
0 MAG5.L MAGS 5 2025-05-09 11:30:00-04:00 792.0 -1.044288 2025-05-09 19:59:00-04:00 783.729235
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## ๐ค Contributing
|
|
62
|
+
|
|
63
|
+
Feel free to open issues or submit pull requests if you find bugs or want to improve the package - Junaid :)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## ๐ License
|
|
67
|
+
|
|
68
|
+
MIT License. See the [LICENSE](./LICENSE) file for full details.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# afterquote
|
|
2
|
+
|
|
3
|
+
**Synthetic after-hours quote generator based on an asset and its correlated underlying.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ๐ฆ What is this?
|
|
8
|
+
|
|
9
|
+
`afterquote` lets you estimate synthetic prices for a financial security based on the real-time performance of a given correlated underlying asset โ useful when one market is closed and the other is still trading.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ๐ Installation
|
|
14
|
+
|
|
15
|
+
### From PyPI (planned):
|
|
16
|
+
```bash
|
|
17
|
+
pip install afterquote
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Locally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## ๐งช Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from afterquote import SecurityPair
|
|
30
|
+
|
|
31
|
+
pair = SecurityPair("MAG5.L", "MAGS")
|
|
32
|
+
quote_df = pair.quote()
|
|
33
|
+
print(quote_df)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ๐ Example Output
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
base_security underlying_security leverage base_close_time base_close_price adj_percent_return quote_time quote_price
|
|
40
|
+
0 MAG5.L MAGS 5 2025-05-09 11:30:00-04:00 792.0 -1.044288 2025-05-09 19:59:00-04:00 783.729235
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## ๐ค Contributing
|
|
44
|
+
|
|
45
|
+
Feel free to open issues or submit pull requests if you find bugs or want to improve the package - Junaid :)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## ๐ License
|
|
49
|
+
|
|
50
|
+
MIT License. See the [LICENSE](./LICENSE) file for full details.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Market calendar logic, to find open/close and trading days"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pandas_market_calendars as mcal
|
|
6
|
+
import pytz
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UnsupportedExchangeError(KeyError):
|
|
10
|
+
"""
|
|
11
|
+
Exception raised when a market calendar name is not found in pandas_market_calendars.
|
|
12
|
+
|
|
13
|
+
This usually occurs because yfinance exchange codes do not always match the calendar
|
|
14
|
+
names expected by pandas_market_calendars. If you encounter a missing or unrecognized
|
|
15
|
+
exchange, consider contributing to the exchange mapping in this package to improve coverage.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MarketCalendar:
|
|
20
|
+
"""Encapsulates exchange calendar logic for various exchanges"""
|
|
21
|
+
|
|
22
|
+
# Please contribute to this list if you find more exchanges
|
|
23
|
+
# Or find a better way (please) :) - Junaid
|
|
24
|
+
_exchange_map = {
|
|
25
|
+
"NMS": "NASDAQ",
|
|
26
|
+
"NGM": "NASDAQ",
|
|
27
|
+
"BTS": "BATS",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def is_exchange_open(
|
|
34
|
+
self,
|
|
35
|
+
yf_exchange_name,
|
|
36
|
+
timestamp: pd.Timestamp = pd.Timestamp(datetime.now(pytz.utc)),
|
|
37
|
+
) -> bool:
|
|
38
|
+
"""Checks if an exchange is trading at a given timestamp"""
|
|
39
|
+
|
|
40
|
+
cal = self.__get_calendar(yf_exchange_name)
|
|
41
|
+
schedule = self.__get_schedule(cal)
|
|
42
|
+
|
|
43
|
+
converted_timestamp = timestamp.tz_convert(cal.tz)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
return cal.open_at_time(schedule, converted_timestamp)
|
|
47
|
+
except (ValueError, IndexError):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def get_closing_time(self, yf_exchange_name) -> pd.Timestamp:
|
|
51
|
+
"""Returns last closing time of the exchange"""
|
|
52
|
+
|
|
53
|
+
exchange = self.__get_calendar(yf_exchange_name)
|
|
54
|
+
schedule = exchange.schedule(
|
|
55
|
+
start_date=datetime.now().today() - timedelta(days=5),
|
|
56
|
+
end_date=datetime.now().today(),
|
|
57
|
+
)
|
|
58
|
+
recent_closes = schedule[-2:]["market_close"].tolist()
|
|
59
|
+
recent_closes.reverse()
|
|
60
|
+
|
|
61
|
+
for close in recent_closes:
|
|
62
|
+
if close < datetime.now(pytz.utc):
|
|
63
|
+
return close
|
|
64
|
+
|
|
65
|
+
raise ValueError("Cannot find the last market close")
|
|
66
|
+
|
|
67
|
+
def get_exchange_tz(self, yf_exchange_name) -> pytz.tzinfo.BaseTzInfo:
|
|
68
|
+
"""Returns the timezone for a given exchange"""
|
|
69
|
+
|
|
70
|
+
cal = self.__get_calendar(yf_exchange_name)
|
|
71
|
+
return cal.tz
|
|
72
|
+
|
|
73
|
+
def __get_calendar(self, yf_exchange_name) -> mcal.MarketCalendar:
|
|
74
|
+
"""Retrieves a pandas_market_calendars calendar for the given exchange name"""
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
return mcal.get_calendar(
|
|
78
|
+
self._exchange_map.get(yf_exchange_name, yf_exchange_name)
|
|
79
|
+
)
|
|
80
|
+
except RuntimeError as e:
|
|
81
|
+
raise UnsupportedExchangeError(
|
|
82
|
+
f"Could not retrieve calendar for exchange '{yf_exchange_name}'. "
|
|
83
|
+
f"Please consider contributing to this file to add support - Junaid"
|
|
84
|
+
) from e
|
|
85
|
+
|
|
86
|
+
def __get_schedule(
|
|
87
|
+
self,
|
|
88
|
+
exchange_cal: mcal.MarketCalendar,
|
|
89
|
+
start=datetime.now().today(),
|
|
90
|
+
end=datetime.now().today(),
|
|
91
|
+
) -> pd.DataFrame:
|
|
92
|
+
"""Retrieves a schedule for a pandas market calendar"""
|
|
93
|
+
try:
|
|
94
|
+
return exchange_cal.schedule(
|
|
95
|
+
start_date=start, end_date=end, start="pre", end="post"
|
|
96
|
+
)
|
|
97
|
+
# Handle exchanges with no extended hours
|
|
98
|
+
except ValueError:
|
|
99
|
+
return exchange_cal.schedule(
|
|
100
|
+
start_date=start,
|
|
101
|
+
end_date=end,
|
|
102
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Providing a quote for a security from its underlying asset"""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from ._yfinance_wrapper import YFinanceSecurity
|
|
5
|
+
from ._market_calendar import MarketCalendar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecurityPair:
|
|
9
|
+
"""Class holding the leveraged etf and its underlying security"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, base, underlying):
|
|
12
|
+
self.base_yf = YFinanceSecurity(base)
|
|
13
|
+
self.underlying_yf = YFinanceSecurity(underlying)
|
|
14
|
+
|
|
15
|
+
if not self.is_valid_pair():
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Invalid security pair: {base}, {underlying}",
|
|
18
|
+
"Please use yfinance tickers",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
self.calendar = MarketCalendar()
|
|
22
|
+
|
|
23
|
+
def is_valid_pair(self) -> bool:
|
|
24
|
+
"""Returns if both of the tickers provided are found by yfinance"""
|
|
25
|
+
|
|
26
|
+
return self.underlying_yf.is_real_security() and self.base_yf.is_real_security()
|
|
27
|
+
|
|
28
|
+
def is_pair_fully_live(self) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Returns if both of the securities are currently trading,
|
|
31
|
+
meaning no synthetic return is needed
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
return self.calendar.is_exchange_open(
|
|
35
|
+
self.base_yf.get_exchange()
|
|
36
|
+
) and self.calendar.is_exchange_open(self.underlying_yf.get_exchange())
|
|
37
|
+
|
|
38
|
+
def quote(self) -> pd.DataFrame:
|
|
39
|
+
"""Returns a df with the latest possible quote for the underlying security"""
|
|
40
|
+
|
|
41
|
+
if self.is_pair_fully_live():
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
"Cannot compute synthetic return โ both securities are currently trading."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Get the last closing time of the base security
|
|
47
|
+
close_time = self.calendar.get_closing_time(self.base_yf.get_exchange())
|
|
48
|
+
close_price = self.base_yf.get_price_at(close_time)
|
|
49
|
+
# Convert that to the timezone of the underlying security
|
|
50
|
+
target_timezone = self.calendar.get_exchange_tz(
|
|
51
|
+
self.underlying_yf.get_exchange()
|
|
52
|
+
)
|
|
53
|
+
# The close of the base security is our start for the underlying security
|
|
54
|
+
start_time = close_time.astimezone(target_timezone)
|
|
55
|
+
|
|
56
|
+
# Get the start price of the underlying security
|
|
57
|
+
start_price = self.underlying_yf.get_price_at(start_time)
|
|
58
|
+
# Get the current price of the underlying security
|
|
59
|
+
live_data = self.underlying_yf.yf_ticker.history(
|
|
60
|
+
period="5d", interval="1m", prepost=True
|
|
61
|
+
)
|
|
62
|
+
latest_price = live_data["Close"].iloc[-1]
|
|
63
|
+
latest_time = live_data.index[-1]
|
|
64
|
+
|
|
65
|
+
# Calculate the percentage return of the underlying security
|
|
66
|
+
change = latest_price - start_price
|
|
67
|
+
percent_return = (change / start_price) * 100
|
|
68
|
+
|
|
69
|
+
leverage_factor = self.base_yf.get_leverage()
|
|
70
|
+
leveraged_return = percent_return * leverage_factor
|
|
71
|
+
|
|
72
|
+
# Calculate the quote price based on the leveraged return
|
|
73
|
+
quote_price = close_price * (1 + (leveraged_return / 100))
|
|
74
|
+
|
|
75
|
+
return pd.DataFrame(
|
|
76
|
+
[
|
|
77
|
+
{
|
|
78
|
+
"base_security": self.base_yf.ticker,
|
|
79
|
+
"underlying_security": self.underlying_yf.ticker,
|
|
80
|
+
"leverage": leverage_factor,
|
|
81
|
+
"base_close_time": start_time,
|
|
82
|
+
"base_close_price": close_price,
|
|
83
|
+
"adj_percent_return": leveraged_return,
|
|
84
|
+
"quote_time": latest_time,
|
|
85
|
+
"quote_price": quote_price,
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Used for querying information and pricing for securities"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import pytz
|
|
7
|
+
import yfinance as yf
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class YFinanceSecurity:
|
|
11
|
+
"""Wrapper for yfinance objects"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, ticker):
|
|
14
|
+
self.ticker = ticker
|
|
15
|
+
self.yf_ticker = yf.Ticker(ticker)
|
|
16
|
+
|
|
17
|
+
def is_real_security(self) -> bool:
|
|
18
|
+
"""Returns whether yfinance found the ticker"""
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
self.yf_ticker.info.get("longName")
|
|
22
|
+
return True
|
|
23
|
+
except AttributeError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def get_leverage(self) -> int:
|
|
27
|
+
"""Returns the leverage for a security"""
|
|
28
|
+
|
|
29
|
+
info = self.yf_ticker.info
|
|
30
|
+
long_name = info.get("longName", "Error finding long name")
|
|
31
|
+
match = re.search(r"(-?\d+x)", long_name)
|
|
32
|
+
if match:
|
|
33
|
+
leverage = match.group(0).replace("x", "")
|
|
34
|
+
if (
|
|
35
|
+
"short" in long_name.lower() or "inverse" in long_name.lower()
|
|
36
|
+
) and leverage[0] != "-":
|
|
37
|
+
leverage = f"-{leverage}"
|
|
38
|
+
return int(leverage)
|
|
39
|
+
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
def get_timezone(self) -> pytz.tzinfo.BaseTzInfo:
|
|
43
|
+
"""Returns a pytz timezone for a security"""
|
|
44
|
+
|
|
45
|
+
info = self.yf_ticker.info
|
|
46
|
+
timezone_name = info.get("timeZoneFullName")
|
|
47
|
+
if not timezone_name:
|
|
48
|
+
raise ValueError(f"Timezone not found for {self.ticker}")
|
|
49
|
+
return pytz.timezone(timezone_name)
|
|
50
|
+
|
|
51
|
+
def get_exchange(self) -> str:
|
|
52
|
+
"""Returns the exchange for a security"""
|
|
53
|
+
|
|
54
|
+
info = self.yf_ticker.info
|
|
55
|
+
exchange_name = info.get("exchange")
|
|
56
|
+
if not exchange_name:
|
|
57
|
+
raise ValueError(f"Exchange not found for {self.ticker}")
|
|
58
|
+
return exchange_name
|
|
59
|
+
|
|
60
|
+
def get_price_at(self, timestamp: pd.Timestamp) -> float:
|
|
61
|
+
"""Fetches the price closest to the given timestamp using 1-minute interval data"""
|
|
62
|
+
|
|
63
|
+
data = self.yf_ticker.history(
|
|
64
|
+
start=timestamp - timedelta(minutes=5),
|
|
65
|
+
end=timestamp + timedelta(minutes=5),
|
|
66
|
+
interval="1m",
|
|
67
|
+
prepost=True,
|
|
68
|
+
)
|
|
69
|
+
if data.empty:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"No pricing data for {self.ticker} found around {timestamp.isoformat()}"
|
|
72
|
+
)
|
|
73
|
+
if timestamp in data.index:
|
|
74
|
+
price = data.loc[timestamp].Open
|
|
75
|
+
else:
|
|
76
|
+
price = data.iloc[0].Open
|
|
77
|
+
|
|
78
|
+
return price
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: afterquote
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synthetic after-hours quote generator
|
|
5
|
+
Author-email: Mohammad Junaid <mohammadjunaiduk@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: yfinance
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: pytz
|
|
13
|
+
Requires-Dist: pandas_market_calendars
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: black; extra == "dev"
|
|
16
|
+
Requires-Dist: pylint; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# afterquote
|
|
20
|
+
|
|
21
|
+
**Synthetic after-hours quote generator based on an asset and its correlated underlying.**
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ๐ฆ What is this?
|
|
26
|
+
|
|
27
|
+
`afterquote` lets you estimate synthetic prices for a financial security based on the real-time performance of a given correlated underlying asset โ useful when one market is closed and the other is still trading.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ๐ Installation
|
|
32
|
+
|
|
33
|
+
### From PyPI (planned):
|
|
34
|
+
```bash
|
|
35
|
+
pip install afterquote
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Locally:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## ๐งช Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from afterquote import SecurityPair
|
|
48
|
+
|
|
49
|
+
pair = SecurityPair("MAG5.L", "MAGS")
|
|
50
|
+
quote_df = pair.quote()
|
|
51
|
+
print(quote_df)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## ๐ Example Output
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
base_security underlying_security leverage base_close_time base_close_price adj_percent_return quote_time quote_price
|
|
58
|
+
0 MAG5.L MAGS 5 2025-05-09 11:30:00-04:00 792.0 -1.044288 2025-05-09 19:59:00-04:00 783.729235
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## ๐ค Contributing
|
|
62
|
+
|
|
63
|
+
Feel free to open issues or submit pull requests if you find bugs or want to improve the package - Junaid :)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## ๐ License
|
|
67
|
+
|
|
68
|
+
MIT License. See the [LICENSE](./LICENSE) file for full details.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
afterquote/__init__.py
|
|
5
|
+
afterquote/_market_calendar.py
|
|
6
|
+
afterquote/_security_pair.py
|
|
7
|
+
afterquote/_yfinance_wrapper.py
|
|
8
|
+
afterquote.egg-info/PKG-INFO
|
|
9
|
+
afterquote.egg-info/SOURCES.txt
|
|
10
|
+
afterquote.egg-info/dependency_links.txt
|
|
11
|
+
afterquote.egg-info/requires.txt
|
|
12
|
+
afterquote.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
afterquote
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "afterquote"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Synthetic after-hours quote generator"
|
|
5
|
+
authors = [{ name = "Mohammad Junaid", email = "mohammadjunaiduk@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
"yfinance",
|
|
12
|
+
"pandas",
|
|
13
|
+
"pytz",
|
|
14
|
+
"pandas_market_calendars"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"black",
|
|
20
|
+
"pylint",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools>=61.0"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|