Skip to content

Commit 6d373e1

Browse files
committed
FEAT: support multiple tickers
1 parent 7de9df8 commit 6d373e1

File tree

11 files changed

+301
-63
lines changed

11 files changed

+301
-63
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Added
910
* MAINT: update setup.py to support multiple modes
11+
* API: support multiple tickers
1012
### Fixed
1113
* Align with the pandas new APIs.
1214

capon/__init__.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1-
from .backends.yahoo import stock
1+
import logging
2+
from .wrappers import stock, stocks
23
from .backends.nasdaq import metadata
34

5+
6+
logger = logging.getLogger(__name__)
7+
8+
49
try:
510
from .visualization import template
611
except ModuleNotFoundError as e:
7-
import logging
8-
9-
logger = logging.getLogger(__name__)
1012
logger.warning(e)
1113

1214
try:
1315
# from .visualization import plot_history as plot
1416
from .visualization.altairplotter import plot_history as plot
1517
except ModuleNotFoundError as e:
16-
import logging
17-
18-
logger = logging.getLogger(__name__)
1918
logger.warning(e)
2019

2120
def plot(*args, **kwargs):
2221
raise ImportError(
23-
"Missing optional dependency 'altair'. Use pip or conda to install it."
22+
"Missing optional dependency 'altair'. Use pip or conda to install it."
2423
)

capon/backends/utils.py

+47-14
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,69 @@
22
import re
33
import requests
44

5+
import pandas as pd
6+
7+
8+
#
9+
# Requests
10+
#
11+
512

613
def get_json(url, headers={}):
714
response = requests.get(url, headers=headers)
8-
logging.info(f'{response.url}.. {response.status_code}')
15+
logging.info(f"{response.url}.. {response.status_code}")
916

1017
jo = {}
11-
if response.status_code==200:
18+
if response.status_code == 200:
1219
jo = response.json()
1320

1421
return jo
1522

1623

1724
def camel_case_keys(df):
18-
last_dim = len(df.shape)-1
19-
df = df.rename({k: camel_to_snake_case(k) for k in df.keys()}, axis=last_dim) \
20-
.rename({
21-
'forward_pe1_yr': 'forward_pe_1_yr',
22-
'is_nasdaq100': 'is_nasdaq_100',
23-
}, axis=last_dim)
25+
last_dim = len(df.shape) - 1
26+
df = df.rename(
27+
{k: camel_to_snake_case(k) for k in df.keys()}, axis=last_dim
28+
).rename(
29+
{
30+
"forward_pe1_yr": "forward_pe_1_yr",
31+
"is_nasdaq100": "is_nasdaq_100",
32+
},
33+
axis=last_dim,
34+
)
2435
return df
2536

2637

2738
def camel_to_snake_case(name):
28-
name = re.sub(' ', '', name)
29-
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
30-
name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
39+
name = re.sub(" ", "", name)
40+
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
41+
name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
3142
return name
3243

3344

45+
#
46+
# Pandas
47+
#
48+
49+
50+
def reindex(df: pd.DataFrame, moved: list, position: str = "start", axis: int = 1):
51+
assert axis == 1
52+
53+
rest = [c for c in df if c not in moved]
54+
if position == "start":
55+
new_labels = moved + rest
56+
elif position == "end":
57+
new_labels = rest + moved
58+
else:
59+
# can support int position too
60+
raise ValueError()
61+
62+
df_reindexed = df.reindex(new_labels, axis=axis)
63+
64+
return df_reindexed
65+
66+
3467
if False:
35-
camel_to_snake_case('getHTTPResponseCode') == 'get_http_response_code'
36-
camel_to_snake_case('ForwardPE1Yr') == 'forward_pe1_yr'
37-
camel_to_snake_case('Security Name') == 'security_name'
68+
camel_to_snake_case("getHTTPResponseCode") == "get_http_response_code"
69+
camel_to_snake_case("ForwardPE1Yr") == "forward_pe1_yr"
70+
camel_to_snake_case("Security Name") == "security_name"

capon/backends/yahoo.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from datetime import datetime
99

1010
import pandas as pd
11+
from capon.backends import utils
1112

1213

1314
logger = logging.getLogger(__name__)
@@ -72,7 +73,7 @@ def get_stock(symbol, range="1d", interval=None, start_date=None, end_date=None)
7273
return jo
7374

7475

75-
def stock(symbol, range="1d", interval=None, start_date=None, end_date=None):
76+
def stock(symbol, range="1d", interval=None, start=None, end=None):
7677
"""Get live & historical stock prices.
7778
7879
Parameters
@@ -92,7 +93,7 @@ def stock(symbol, range="1d", interval=None, start_date=None, end_date=None):
9293
low, and close price for a given timepoint. If relevant, it will also include the adjusted closing price.
9394
"""
9495
jo = get_stock(
95-
symbol, range=range, interval=interval, start_date=start_date, end_date=end_date
96+
symbol, range=range, interval=interval, start_date=start, end_date=end
9697
)
9798

9899
result = jo["chart"]["result"][0]
@@ -114,7 +115,11 @@ def stock(symbol, range="1d", interval=None, start_date=None, end_date=None):
114115
)
115116

116117
# len(ts), len(quote), len(adjclose)
117-
stock = pd.concat([ts, quote, adjclose], axis=1)
118+
stock = pd.concat([ts, quote, adjclose], axis=1).pipe(
119+
utils.reindex,
120+
["timestamp", "symbol", "currency", "open", "low", "high", "close", "volume"],
121+
)
122+
118123
return stock
119124

120125

capon/portfolio.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def fetch_history(self, symbols=None, start_time=None, end_time=None, tz=None):
220220
history = pd.concat(
221221
[
222222
capon.stock(
223-
symbol, start_date=start_time, end_date=end_time, interval="1d"
223+
symbol, start=start_time, end=end_time, interval="1d"
224224
).dropna()
225225
for symbol in tqdm(symbols)
226226
],

capon/tests/test_core.py

+98-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,110 @@
1+
import pandas.testing as pdt
2+
13
import capon
24

35

46
def test_adjclose():
5-
amd_daily = capon.stock('AMD', range='1mo', interval='1d')
6-
assert 'adjclose' in amd_daily
7+
amd_daily = capon.stock("AMD", range="1mo", interval="1d")
8+
assert "adjclose" in amd_daily
79

8-
amd_minutely = capon.stock('AMD', range='1d', interval='2m')
9-
assert 'close' in amd_minutely
10+
amd_minutely = capon.stock("AMD", range="1d", interval="2m")
11+
assert "close" in amd_minutely
1012

1113

1214
def test_index():
13-
sp500 = capon.stock('^GSPC')
15+
sp500 = capon.stock("^GSPC")
1416
assert len(sp500) > 0
1517

1618

1719
def test_metadata():
18-
metadata = capon.metadata('AMD')
19-
assert metadata['symbol'] == 'AMD'
20+
metadata = capon.metadata("AMD")
21+
assert metadata["symbol"] == "AMD"
22+
23+
24+
if False:
25+
import logging
26+
27+
from IPython.display import display
28+
29+
logging.basicConfig()
30+
logging.getLogger("capon").setLevel(logging.DEBUG)
31+
32+
33+
base_tickers = ["MSFT", "AAPL", "NVDA"]
34+
35+
36+
def test_stocks():
37+
capon.stocks(["MSFT", "AAPL", "NVDA"])
38+
39+
40+
def test_stocks_njobs():
41+
all_quotes = []
42+
for n_jobs in [None, -1]:
43+
quotes = capon.stocks(
44+
base_tickers,
45+
start="2020-01-01",
46+
end="2021-01-01",
47+
interval="1d",
48+
n_jobs=n_jobs,
49+
)
50+
all_quotes.append(quotes)
51+
52+
pdt.assert_frame_equal(all_quotes[0], all_quotes[1])
53+
54+
55+
def test_stocks_missing():
56+
missing_ticker = "BZZ"
57+
58+
quotes = capon.stocks(
59+
base_tickers + [missing_ticker],
60+
start="2020-01-01",
61+
end="2021-01-01",
62+
interval="1d",
63+
)
64+
# display(quotes)
65+
assert (quotes["symbol"].unique() == base_tickers).all()
66+
67+
68+
def test_stocks_multiple_tz():
69+
international_ticker = "^TA125.TA"
70+
71+
for timestamp_normalizer in ["auto", None, "date"]:
72+
quotes = capon.stocks(
73+
base_tickers + [international_ticker],
74+
start="2020-01-01",
75+
end="2021-01-01",
76+
interval="1d",
77+
timestamp_normalizer=timestamp_normalizer,
78+
)
79+
# display(quotes)
80+
81+
assert (
82+
quotes["symbol"].unique() == base_tickers + [international_ticker]
83+
).all()
84+
85+
86+
def test_stocks_single_ticker():
87+
quotes = capon.stocks(
88+
base_tickers[0],
89+
start="2020-01-01",
90+
end="2021-01-01",
91+
interval="1d",
92+
)
93+
94+
assert (quotes["symbol"].unique() == base_tickers[0]).all()
95+
96+
97+
# def test_stocks_timestamp_auto():
98+
# quotes_1h = capon.stocks(
99+
# base_tickers,
100+
# range="1mo",
101+
# interval="1h",
102+
# timestamp_normalizer="auto",
103+
# )
104+
105+
# quotes_1d = capon.stocks(
106+
# base_tickers,
107+
# range="1mo",
108+
# interval="1d",
109+
# timestamp_normalizer="auto",
110+
# )
+7-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import plotly.io as pio
21
import capon
32

3+
try:
4+
import plotly.io as pio
45

5-
def test_template_registration():
6-
assert 'capon' in pio.templates
6+
def test_template_registration():
7+
assert "capon" in pio.templates
8+
9+
except ModuleNotFoundError as e:
10+
print(e)

capon/visualization/template.py

+11-19
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,21 @@
33
import plotly.express as px
44

55

6-
pio.templates['capon'] = go.layout.Template(
6+
pio.templates["capon"] = go.layout.Template(
77
layout=go.Layout(
8-
font=dict(color='#887e7d'),
9-
paper_bgcolor='#fff1e5',
10-
plot_bgcolor='#fff1e5',
11-
8+
font=dict(color="#887e7d"),
9+
paper_bgcolor="#fff1e5",
10+
plot_bgcolor="#fff1e5",
1211
title=dict(
13-
font=dict(size=22, color='black', family='Palatino'),
14-
xanchor='left',
15-
x=0.05),
16-
12+
font=dict(size=22, color="black", family="Palatino"), xanchor="left", x=0.05
13+
),
1714
colorway=px.colors.qualitative.T10,
18-
1915
colorscale={
20-
'sequential': 'Geyser_r',
21-
'sequentialminus': 'Geyser_r',
22-
}
16+
"sequential": "Geyser_r",
17+
"sequentialminus": "Geyser_r",
18+
},
2319
),
24-
25-
data_scatter=[
26-
go.Scatter(line=dict(width=2.5))
27-
],
28-
20+
data_scatter=[go.Scatter(line=dict(width=2.5))],
2921
)
3022

3123
# https://plotly.com/python/builtin-colorscales/
@@ -34,4 +26,4 @@
3426

3527
# from plotly.offline import plot
3628
# fig = px.line(x=[1,2,3], y=[1,3,2], title='Test', template='capon')
37-
# plot(fig)
29+
# plot(fig)

0 commit comments

Comments
 (0)