ftgo 1.0.0__py3-none-any.whl
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.
- ftgo/__init__.py +29 -0
- ftgo/historical.py +237 -0
- ftgo/holdings.py +292 -0
- ftgo/infos.py +239 -0
- ftgo/search.py +160 -0
- ftgo-1.0.0.dist-info/METADATA +353 -0
- ftgo-1.0.0.dist-info/RECORD +11 -0
- ftgo-1.0.0.dist-info/WHEEL +5 -0
- ftgo-1.0.0.dist-info/entry_points.txt +2 -0
- ftgo-1.0.0.dist-info/licenses/LICENSE +21 -0
- ftgo-1.0.0.dist-info/top_level.txt +1 -0
ftgo/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FTMarkets - A Python library for fetching financial data from FT Markets
|
|
3
|
+
|
|
4
|
+
This library provides easy access to historical stock prices and search
|
|
5
|
+
functionality for financial instruments from Financial Times Markets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .search import search_securities, get_xid
|
|
9
|
+
from .historical import get_historical_prices, get_multiple_historical_prices
|
|
10
|
+
from .holdings import get_holdings, get_fund_breakdown
|
|
11
|
+
from .infos import get_fund_profile, get_fund_stats, get_available_fields, search_profile_field
|
|
12
|
+
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
__author__ = "gohibiki"
|
|
15
|
+
__email__ = "gohibiki@protonmail.com"
|
|
16
|
+
__description__ = "A Python library for fetching financial data from FT Markets"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"search_securities",
|
|
20
|
+
"get_xid",
|
|
21
|
+
"get_historical_prices",
|
|
22
|
+
"get_multiple_historical_prices",
|
|
23
|
+
"get_holdings",
|
|
24
|
+
"get_fund_breakdown",
|
|
25
|
+
"get_fund_profile",
|
|
26
|
+
"get_fund_stats",
|
|
27
|
+
"get_available_fields",
|
|
28
|
+
"search_profile_field"
|
|
29
|
+
]
|
ftgo/historical.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Historical data fetching functionality for FTMarkets.
|
|
3
|
+
|
|
4
|
+
This module provides functions to fetch and process historical stock price data
|
|
5
|
+
from FT Markets with support for OHLCV data and concurrent processing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cloudscraper
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from bs4 import BeautifulSoup
|
|
12
|
+
from typing import Dict, Any, List
|
|
13
|
+
import logging
|
|
14
|
+
import concurrent.futures
|
|
15
|
+
|
|
16
|
+
# Set up logging
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def fetch_historical_prices(xid: str, date_from: str, date_to: str) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Fetch historical price data from FT Markets API.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
xid: The FT Markets XID
|
|
26
|
+
date_from: Start date in DDMMYYYY format (e.g., "01012024")
|
|
27
|
+
date_to: End date in DDMMYYYY format (e.g., "31122024")
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
JSON response from the API
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
requests.exceptions.HTTPError: If the API request fails
|
|
34
|
+
"""
|
|
35
|
+
scraper = cloudscraper.create_scraper(
|
|
36
|
+
browser={
|
|
37
|
+
'browser': 'chrome',
|
|
38
|
+
'platform': 'windows',
|
|
39
|
+
'desktop': True
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Convert DDMMYYYY to FT format (YYYY%2FMM%2FDD)
|
|
45
|
+
start_date = datetime.strptime(date_from, "%d%m%Y")
|
|
46
|
+
end_date = datetime.strptime(date_to, "%d%m%Y")
|
|
47
|
+
|
|
48
|
+
start_formatted = start_date.strftime("%Y%%2F%m%%2F%d")
|
|
49
|
+
end_formatted = end_date.strftime("%Y%%2F%m%%2F%d")
|
|
50
|
+
|
|
51
|
+
url = f"https://markets.ft.com/data/equities/ajax/get-historical-prices?startDate={start_formatted}&endDate={end_formatted}&symbol={xid}"
|
|
52
|
+
|
|
53
|
+
response = scraper.get(url)
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
return response.json()
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Failed to fetch historical data for XID {xid}: {e}")
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def html_to_dataframe(html_content: str) -> pd.DataFrame:
|
|
62
|
+
"""
|
|
63
|
+
Convert HTML response to a pandas DataFrame.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
html_content: HTML content from the FT Markets API
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
pandas.DataFrame with processed historical data
|
|
70
|
+
|
|
71
|
+
Note:
|
|
72
|
+
Returns empty DataFrame if no data is found
|
|
73
|
+
"""
|
|
74
|
+
if not html_content:
|
|
75
|
+
logger.warning("No HTML content provided")
|
|
76
|
+
return pd.DataFrame()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
80
|
+
rows = soup.find_all('tr')
|
|
81
|
+
|
|
82
|
+
historical_data = []
|
|
83
|
+
|
|
84
|
+
for row in rows:
|
|
85
|
+
cells = row.find_all('td')
|
|
86
|
+
if len(cells) >= 6: # Ensure we have all columns including volume
|
|
87
|
+
try:
|
|
88
|
+
# Extract date
|
|
89
|
+
date_cell = cells[0]
|
|
90
|
+
date_spans = date_cell.find_all('span')
|
|
91
|
+
if date_spans:
|
|
92
|
+
date_text = date_spans[0].get_text().strip()
|
|
93
|
+
else:
|
|
94
|
+
date_text = date_cell.get_text().strip()
|
|
95
|
+
|
|
96
|
+
# Parse date
|
|
97
|
+
try:
|
|
98
|
+
date_obj = datetime.strptime(date_text, '%A, %B %d, %Y')
|
|
99
|
+
except ValueError:
|
|
100
|
+
try:
|
|
101
|
+
date_obj = datetime.strptime(date_text, '%B %d, %Y')
|
|
102
|
+
except ValueError:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
formatted_date = date_obj.strftime('%Y-%m-%d')
|
|
106
|
+
|
|
107
|
+
# Extract OHLC prices
|
|
108
|
+
open_price = float(cells[1].get_text().strip().replace(',', ''))
|
|
109
|
+
high_price = float(cells[2].get_text().strip().replace(',', ''))
|
|
110
|
+
low_price = float(cells[3].get_text().strip().replace(',', ''))
|
|
111
|
+
close_price = float(cells[4].get_text().strip().replace(',', ''))
|
|
112
|
+
|
|
113
|
+
# Extract volume from 6th column
|
|
114
|
+
volume_cell = cells[5]
|
|
115
|
+
volume_spans = volume_cell.find_all('span')
|
|
116
|
+
if volume_spans:
|
|
117
|
+
# Use the first span with full number
|
|
118
|
+
volume_text = volume_spans[0].get_text().strip().replace(',', '')
|
|
119
|
+
else:
|
|
120
|
+
volume_text = volume_cell.get_text().strip().replace(',', '')
|
|
121
|
+
|
|
122
|
+
# Convert volume to integer
|
|
123
|
+
try:
|
|
124
|
+
volume = int(float(volume_text))
|
|
125
|
+
except ValueError:
|
|
126
|
+
volume = 0
|
|
127
|
+
|
|
128
|
+
historical_data.append({
|
|
129
|
+
'date': formatted_date,
|
|
130
|
+
'open': open_price,
|
|
131
|
+
'high': high_price,
|
|
132
|
+
'low': low_price,
|
|
133
|
+
'close': close_price,
|
|
134
|
+
'volume': volume
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
except (ValueError, IndexError) as e:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if historical_data:
|
|
141
|
+
df = pd.DataFrame(historical_data)
|
|
142
|
+
df['date'] = pd.to_datetime(df['date'])
|
|
143
|
+
df = df.sort_values('date').reset_index(drop=True)
|
|
144
|
+
return df
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Error processing HTML data: {e}")
|
|
148
|
+
|
|
149
|
+
return pd.DataFrame()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_historical_prices(xid: str, date_from: str, date_to: str) -> pd.DataFrame:
|
|
153
|
+
"""
|
|
154
|
+
Get historical price data for a security with full OHLCV data.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
xid: The FT Markets XID (use get_xid to find this)
|
|
158
|
+
date_from: Start date in DDMMYYYY format (e.g., "01012024")
|
|
159
|
+
date_to: End date in DDMMYYYY format (e.g., "31122024")
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
>>> xid = get_xid('AAPL')
|
|
163
|
+
>>> df = get_historical_prices(xid, "01012024", "31012024")
|
|
164
|
+
"""
|
|
165
|
+
if not xid:
|
|
166
|
+
raise ValueError("XID cannot be empty")
|
|
167
|
+
|
|
168
|
+
logger.info(f"Fetching historical data for XID {xid} from {date_from} to {date_to}")
|
|
169
|
+
|
|
170
|
+
json_data = fetch_historical_prices(xid, date_from, date_to)
|
|
171
|
+
|
|
172
|
+
if not json_data.get('html'):
|
|
173
|
+
logger.warning("No HTML data in API response")
|
|
174
|
+
return pd.DataFrame()
|
|
175
|
+
|
|
176
|
+
html_content = json_data['html']
|
|
177
|
+
if len(html_content) == 0:
|
|
178
|
+
logger.warning("Empty HTML content in API response")
|
|
179
|
+
return pd.DataFrame()
|
|
180
|
+
|
|
181
|
+
df = html_to_dataframe(html_content)
|
|
182
|
+
|
|
183
|
+
logger.info(f"Successfully retrieved {len(df)} data points for XID {xid}")
|
|
184
|
+
return df
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_multiple_historical_prices(
|
|
188
|
+
xids: List[str],
|
|
189
|
+
date_from: str,
|
|
190
|
+
date_to: str
|
|
191
|
+
) -> pd.DataFrame:
|
|
192
|
+
"""
|
|
193
|
+
Get historical data for multiple securities concurrently.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
xids: List of FT Markets XIDs
|
|
197
|
+
date_from: Start date in DDMMYYYY format (e.g., "01012024")
|
|
198
|
+
date_to: End date in DDMMYYYY format (e.g., "31122024")
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
pandas.DataFrame with data for all securities concatenated
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: If parameters are invalid
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
>>> from ftmarkets import get_xid, get_multiple_historical_prices
|
|
208
|
+
>>> xids = [get_xid('AAPL'), get_xid('MSFT')]
|
|
209
|
+
>>> df = get_multiple_historical_prices(xids, "01012024", "31012024")
|
|
210
|
+
"""
|
|
211
|
+
if not xids:
|
|
212
|
+
raise ValueError("XIDs list cannot be empty")
|
|
213
|
+
|
|
214
|
+
logger.info(f"Fetching data for {len(xids)} securities")
|
|
215
|
+
|
|
216
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
|
|
217
|
+
futures = [
|
|
218
|
+
executor.submit(get_historical_prices, xid, date_from, date_to)
|
|
219
|
+
for xid in xids
|
|
220
|
+
]
|
|
221
|
+
results = []
|
|
222
|
+
|
|
223
|
+
for future in concurrent.futures.as_completed(futures):
|
|
224
|
+
try:
|
|
225
|
+
result = future.result()
|
|
226
|
+
if not result.empty:
|
|
227
|
+
results.append(result)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"Error fetching data for security: {e}")
|
|
230
|
+
|
|
231
|
+
if results:
|
|
232
|
+
combined_df = pd.concat(results, axis=0, ignore_index=True)
|
|
233
|
+
logger.info(f"Successfully retrieved data for {len(results)} securities")
|
|
234
|
+
return combined_df
|
|
235
|
+
else:
|
|
236
|
+
logger.warning("No data retrieved for any securities")
|
|
237
|
+
return pd.DataFrame()
|
ftgo/holdings.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Holdings and allocation data functionality for FTMarkets.
|
|
3
|
+
|
|
4
|
+
This module provides functions to fetch ETF/fund holdings, asset allocation,
|
|
5
|
+
sector breakdown, and geographic allocation data from FT Markets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cloudscraper
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from bs4 import BeautifulSoup
|
|
11
|
+
from typing import Dict, Any, Tuple, Optional
|
|
12
|
+
from io import StringIO
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
# Set up logging
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Constants for holdings types
|
|
19
|
+
ASSET_ALLOCATION = "asset_allocation"
|
|
20
|
+
SECTOR_WEIGHTS = "sector_weights"
|
|
21
|
+
GEOGRAPHIC_ALLOCATION = "geographic_allocation"
|
|
22
|
+
TOP_HOLDINGS = "top_holdings"
|
|
23
|
+
ALL_TYPES = "all"
|
|
24
|
+
|
|
25
|
+
# Valid holdings types for validation
|
|
26
|
+
VALID_HOLDINGS_TYPES = {ASSET_ALLOCATION, SECTOR_WEIGHTS, GEOGRAPHIC_ALLOCATION, TOP_HOLDINGS, ALL_TYPES}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fetch_holdings_page(xid: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Fetch the holdings page HTML for a given XID from FT Markets.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
xid: The FT Markets XID
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
HTML content of the holdings page
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
requests.exceptions.HTTPError: If the API request fails
|
|
41
|
+
"""
|
|
42
|
+
scraper = cloudscraper.create_scraper(
|
|
43
|
+
browser={
|
|
44
|
+
'browser': 'chrome',
|
|
45
|
+
'platform': 'windows',
|
|
46
|
+
'desktop': True
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Construct holdings URL using XID
|
|
52
|
+
url = f"https://markets.ft.com/data/etfs/tearsheet/holdings?s={xid}"
|
|
53
|
+
|
|
54
|
+
response = scraper.get(url)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
return response.text
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logger.error(f"Failed to fetch holdings page for XID {xid}: {e}")
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_fund_name(soup: BeautifulSoup) -> str:
|
|
63
|
+
"""Extract fund name from the page"""
|
|
64
|
+
try:
|
|
65
|
+
name_element = soup.find('h1', {
|
|
66
|
+
'class': 'mod-tearsheet-overview__header__name mod-tearsheet-overview__header__name--large'
|
|
67
|
+
})
|
|
68
|
+
if name_element:
|
|
69
|
+
return name_element.get_text().strip()
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
return "Unknown Fund"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_asset_allocation(soup: BeautifulSoup, fund_name: str) -> pd.DataFrame:
|
|
76
|
+
"""Extract asset allocation data"""
|
|
77
|
+
try:
|
|
78
|
+
allocation_div = soup.find('div', {'class': 'mod-asset-allocation__table'})
|
|
79
|
+
if allocation_div:
|
|
80
|
+
html_string = str(allocation_div)
|
|
81
|
+
df_list = pd.read_html(StringIO(html_string))
|
|
82
|
+
if df_list:
|
|
83
|
+
df = df_list[0]
|
|
84
|
+
# Rename the percentage column to fund name
|
|
85
|
+
if len(df.columns) >= 2:
|
|
86
|
+
df = df.iloc[:, :2] # Keep only first two columns
|
|
87
|
+
df.columns = ['Asset Class', fund_name]
|
|
88
|
+
return df
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.warning(f"Could not extract asset allocation: {e}")
|
|
91
|
+
|
|
92
|
+
return pd.DataFrame(columns=['Asset Class', fund_name])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_sector_weights(soup: BeautifulSoup, fund_name: str) -> pd.DataFrame:
|
|
96
|
+
"""Extract sector weights data"""
|
|
97
|
+
try:
|
|
98
|
+
sectors_div = soup.find('div', {'class': 'mod-weightings__sectors__table'})
|
|
99
|
+
if sectors_div:
|
|
100
|
+
html_string = str(sectors_div)
|
|
101
|
+
df_list = pd.read_html(StringIO(html_string))
|
|
102
|
+
if df_list:
|
|
103
|
+
df = df_list[0]
|
|
104
|
+
# Rename the percentage column to fund name
|
|
105
|
+
if len(df.columns) >= 2:
|
|
106
|
+
df = df.iloc[:, :2] # Keep only first two columns
|
|
107
|
+
df.columns = ['Sector', fund_name]
|
|
108
|
+
return df
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.warning(f"Could not extract sector weights: {e}")
|
|
111
|
+
|
|
112
|
+
return pd.DataFrame(columns=['Sector', fund_name])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def extract_geographic_allocation(soup: BeautifulSoup, fund_name: str) -> pd.DataFrame:
|
|
116
|
+
"""Extract geographic allocation data"""
|
|
117
|
+
try:
|
|
118
|
+
geo_div = soup.find('div', {'class': 'mod-weightings__regions__table'})
|
|
119
|
+
if geo_div:
|
|
120
|
+
html_string = str(geo_div)
|
|
121
|
+
df_list = pd.read_html(StringIO(html_string))
|
|
122
|
+
if df_list:
|
|
123
|
+
df = df_list[0]
|
|
124
|
+
# Rename the percentage column to fund name
|
|
125
|
+
if len(df.columns) >= 2:
|
|
126
|
+
df = df.iloc[:, :2] # Keep only first two columns
|
|
127
|
+
df.columns = ['Region', fund_name]
|
|
128
|
+
return df
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning(f"Could not extract geographic allocation: {e}")
|
|
131
|
+
|
|
132
|
+
return pd.DataFrame(columns=['Region', fund_name])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_top_holdings(soup: BeautifulSoup) -> pd.DataFrame:
|
|
136
|
+
"""Extract top holdings data"""
|
|
137
|
+
try:
|
|
138
|
+
# Find all module content divs
|
|
139
|
+
module_divs = soup.find_all('div', {'class': 'mod-module__content'})
|
|
140
|
+
|
|
141
|
+
# Usually holdings are in the 3rd module (index 2)
|
|
142
|
+
if len(module_divs) >= 3:
|
|
143
|
+
holdings_html = str(module_divs[2]).replace('100%', '') # Remove 100% text that breaks parsing
|
|
144
|
+
df_list = pd.read_html(StringIO(holdings_html))
|
|
145
|
+
|
|
146
|
+
# Usually the second table contains the holdings
|
|
147
|
+
if len(df_list) >= 2:
|
|
148
|
+
df = df_list[1]
|
|
149
|
+
# Keep only first 3 columns and top 10 holdings
|
|
150
|
+
if len(df.columns) >= 3:
|
|
151
|
+
df = df.iloc[:10, :3]
|
|
152
|
+
# Standardize column names
|
|
153
|
+
df.columns = ['Holding', 'Weight', 'Shares'] if len(df.columns) >= 3 else df.columns
|
|
154
|
+
return df
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.warning(f"Could not extract top holdings: {e}")
|
|
157
|
+
|
|
158
|
+
return pd.DataFrame(columns=['Holding', 'Weight', 'Shares'])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def parse_holdings_data(html_content: str) -> Dict[str, pd.DataFrame]:
|
|
162
|
+
"""
|
|
163
|
+
Parse holdings data from HTML content into structured DataFrames.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
html_content: Raw HTML content from FT Markets holdings page
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dictionary containing DataFrames for different holdings types:
|
|
170
|
+
- asset_allocation: Asset class breakdown
|
|
171
|
+
- sector_weights: Sector allocation
|
|
172
|
+
- geographic_allocation: Geographic allocation
|
|
173
|
+
- top_holdings: Top holdings by weight
|
|
174
|
+
"""
|
|
175
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
176
|
+
|
|
177
|
+
# Extract fund name
|
|
178
|
+
fund_name = extract_fund_name(soup)
|
|
179
|
+
|
|
180
|
+
# Extract all data types
|
|
181
|
+
asset_allocation = extract_asset_allocation(soup, fund_name)
|
|
182
|
+
sector_weights = extract_sector_weights(soup, fund_name)
|
|
183
|
+
geographic_allocation = extract_geographic_allocation(soup, fund_name)
|
|
184
|
+
top_holdings = extract_top_holdings(soup)
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
ASSET_ALLOCATION: asset_allocation,
|
|
188
|
+
SECTOR_WEIGHTS: sector_weights,
|
|
189
|
+
GEOGRAPHIC_ALLOCATION: geographic_allocation,
|
|
190
|
+
TOP_HOLDINGS: top_holdings
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_holdings(
|
|
195
|
+
xid: str,
|
|
196
|
+
holdings_type: str = "all"
|
|
197
|
+
) -> pd.DataFrame | Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
|
198
|
+
"""
|
|
199
|
+
Get holdings and allocation data for ETFs and funds from FT Markets.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
xid: The FT Markets XID (use get_xid to find this)
|
|
203
|
+
holdings_type: Type of holdings data to retrieve:
|
|
204
|
+
- "asset_allocation": Breakdown by asset class (stocks, bonds, cash)
|
|
205
|
+
- "sector_weights": Sector allocation
|
|
206
|
+
- "geographic_allocation": Geographic allocation
|
|
207
|
+
- "top_holdings": Top holdings by weight percentage
|
|
208
|
+
- "all": All holdings data types as a tuple
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
- pandas.DataFrame for specific holdings type
|
|
212
|
+
- Tuple of DataFrames for "all": (asset_allocation, sector_weights, geographic_allocation, top_holdings)
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
ValueError: If xid is missing or holdings_type is invalid
|
|
216
|
+
|
|
217
|
+
Examples:
|
|
218
|
+
>>> from ftmarkets import get_xid, get_holdings
|
|
219
|
+
>>> xid = get_xid('SPY')
|
|
220
|
+
>>> asset_allocation = get_holdings(xid, "asset_allocation")
|
|
221
|
+
>>> print(asset_allocation)
|
|
222
|
+
|
|
223
|
+
>>> # Get all data
|
|
224
|
+
>>> all_data = get_holdings(xid, "all")
|
|
225
|
+
>>> asset_alloc, sectors, regions, holdings = all_data
|
|
226
|
+
"""
|
|
227
|
+
if not xid:
|
|
228
|
+
raise ValueError("Missing required parameter: xid")
|
|
229
|
+
|
|
230
|
+
if holdings_type not in VALID_HOLDINGS_TYPES:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Invalid holdings_type '{holdings_type}'. "
|
|
233
|
+
f"Choose from: {', '.join(sorted(VALID_HOLDINGS_TYPES))}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
logger.info(f"Fetching holdings data for XID {xid}, type: {holdings_type}")
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
html_content = fetch_holdings_page(xid)
|
|
240
|
+
holdings_data = parse_holdings_data(html_content)
|
|
241
|
+
|
|
242
|
+
if holdings_type == ALL_TYPES:
|
|
243
|
+
return (
|
|
244
|
+
holdings_data[ASSET_ALLOCATION],
|
|
245
|
+
holdings_data[SECTOR_WEIGHTS],
|
|
246
|
+
holdings_data[GEOGRAPHIC_ALLOCATION],
|
|
247
|
+
holdings_data[TOP_HOLDINGS]
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
return holdings_data[holdings_type]
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.error(f"Error retrieving holdings data: {e}")
|
|
254
|
+
raise
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_fund_breakdown(xid: str) -> Dict[str, pd.DataFrame]:
|
|
258
|
+
"""
|
|
259
|
+
Get complete fund breakdown with all allocation data.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
xid: The FT Markets XID
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Dictionary with all DataFrames:
|
|
266
|
+
- 'asset_allocation': Asset class breakdown
|
|
267
|
+
- 'sector_weights': Sector weights
|
|
268
|
+
- 'geographic_allocation': Geographic distribution
|
|
269
|
+
- 'top_holdings': Top 10 holdings
|
|
270
|
+
|
|
271
|
+
Examples:
|
|
272
|
+
>>> xid = get_xid('QQQ')
|
|
273
|
+
>>> breakdown = get_fund_breakdown(xid)
|
|
274
|
+
>>> print(breakdown['asset_allocation'])
|
|
275
|
+
>>> print(breakdown['top_holdings'])
|
|
276
|
+
"""
|
|
277
|
+
logger.info(f"Fetching complete fund breakdown for XID {xid}")
|
|
278
|
+
|
|
279
|
+
html_content = fetch_holdings_page(xid)
|
|
280
|
+
return parse_holdings_data(html_content)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Update __all__ for exports
|
|
284
|
+
__all__ = [
|
|
285
|
+
"get_holdings",
|
|
286
|
+
"get_fund_breakdown",
|
|
287
|
+
"ASSET_ALLOCATION",
|
|
288
|
+
"SECTOR_WEIGHTS",
|
|
289
|
+
"GEOGRAPHIC_ALLOCATION",
|
|
290
|
+
"TOP_HOLDINGS",
|
|
291
|
+
"ALL_TYPES"
|
|
292
|
+
]
|
ftgo/infos.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Profile and information data functionality for FTMarkets.
|
|
3
|
+
|
|
4
|
+
This module provides functions to fetch ETF/fund profile information,
|
|
5
|
+
key statistics, and investment details from FT Markets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cloudscraper
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from bs4 import BeautifulSoup
|
|
11
|
+
from typing import Dict, Any, Optional
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
# Set up logging
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fetch_profile_page(xid: str) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Fetch the profile/summary page HTML for a given XID from FT Markets.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
xid: The FT Markets XID
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
HTML content of the profile page
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
requests.exceptions.HTTPError: If the API request fails
|
|
30
|
+
"""
|
|
31
|
+
scraper = cloudscraper.create_scraper(
|
|
32
|
+
browser={
|
|
33
|
+
'browser': 'chrome',
|
|
34
|
+
'platform': 'windows',
|
|
35
|
+
'desktop': True
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Construct profile URL using XID
|
|
41
|
+
url = f"https://markets.ft.com/data/etfs/tearsheet/summary?s={xid}"
|
|
42
|
+
|
|
43
|
+
response = scraper.get(url)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
return response.text
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Failed to fetch profile page for XID {xid}: {e}")
|
|
48
|
+
raise
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def extract_profile_data(html_content: str) -> pd.DataFrame:
|
|
52
|
+
"""
|
|
53
|
+
Extract profile and investment data from FT Markets ETF page HTML.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
html_content: Raw HTML content from FT Markets profile page
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
pandas.DataFrame with Field and Value columns containing profile information
|
|
60
|
+
|
|
61
|
+
Note:
|
|
62
|
+
Returns empty DataFrame if no profile data is found
|
|
63
|
+
"""
|
|
64
|
+
if not html_content:
|
|
65
|
+
logger.warning("No HTML content provided")
|
|
66
|
+
return pd.DataFrame(columns=['Field', 'Value'])
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
70
|
+
|
|
71
|
+
# Find the Profile and Investment section
|
|
72
|
+
profile_section = soup.find('div', {'data-f2-app-id': 'mod-profile-and-investment-app'})
|
|
73
|
+
|
|
74
|
+
if not profile_section:
|
|
75
|
+
logger.warning("Profile and Investment section not found")
|
|
76
|
+
return pd.DataFrame(columns=['Field', 'Value'])
|
|
77
|
+
|
|
78
|
+
# Extract all table data
|
|
79
|
+
data = []
|
|
80
|
+
tables = profile_section.find_all('table')
|
|
81
|
+
|
|
82
|
+
for table in tables:
|
|
83
|
+
rows = table.find_all('tr')
|
|
84
|
+
for row in rows:
|
|
85
|
+
th = row.find('th')
|
|
86
|
+
td = row.find('td')
|
|
87
|
+
|
|
88
|
+
if th and td:
|
|
89
|
+
field = th.get_text(strip=True)
|
|
90
|
+
value = td.get_text(separator=' ', strip=True)
|
|
91
|
+
value = ' '.join(value.split()) # Clean whitespace
|
|
92
|
+
data.append({'Field': field, 'Value': value})
|
|
93
|
+
|
|
94
|
+
logger.info(f"Extracted {len(data)} profile data points")
|
|
95
|
+
return pd.DataFrame(data)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Error extracting profile data: {e}")
|
|
99
|
+
return pd.DataFrame(columns=['Field', 'Value'])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_fund_profile(xid: str) -> pd.DataFrame:
|
|
103
|
+
"""
|
|
104
|
+
Get profile and investment information for ETFs and funds from FT Markets.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
xid: The FT Markets XID (use get_xid to find this)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
pandas.DataFrame with Field and Value columns containing all available
|
|
111
|
+
fund information including details, investment data, and statistics
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If xid is missing
|
|
115
|
+
requests.exceptions.HTTPError: If API request fails
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
>>> from ftmarkets import get_xid, get_fund_profile
|
|
119
|
+
>>> xid = get_xid('SPY')
|
|
120
|
+
>>> profile = get_fund_profile(xid)
|
|
121
|
+
>>> print(profile)
|
|
122
|
+
|
|
123
|
+
>>> # Filter for specific information
|
|
124
|
+
>>> fees = profile[profile['Field'].str.contains('fee', case=False)]
|
|
125
|
+
>>> print(fees)
|
|
126
|
+
"""
|
|
127
|
+
if not xid:
|
|
128
|
+
raise ValueError("Missing required parameter: xid")
|
|
129
|
+
|
|
130
|
+
logger.info(f"Fetching profile data for XID {xid}")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
html_content = fetch_profile_page(xid)
|
|
134
|
+
profile_data = extract_profile_data(html_content)
|
|
135
|
+
|
|
136
|
+
logger.info(f"Successfully retrieved profile data for XID {xid}")
|
|
137
|
+
return profile_data
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Error retrieving profile data: {e}")
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_fund_stats(xid: str) -> Dict[str, str]:
|
|
145
|
+
"""
|
|
146
|
+
Get all fund profile data as a dictionary for easy access.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
xid: The FT Markets XID
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Dictionary with all available fund fields and values
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
>>> xid = get_xid('QQQ')
|
|
156
|
+
>>> stats = get_fund_stats(xid)
|
|
157
|
+
>>>
|
|
158
|
+
>>> # Access any available field safely
|
|
159
|
+
>>> for field, value in stats.items():
|
|
160
|
+
>>> print(f"{field}: {value}")
|
|
161
|
+
>>>
|
|
162
|
+
>>> # Check if specific fields exist before using
|
|
163
|
+
>>> inception = stats.get('Inception date', 'Not available')
|
|
164
|
+
>>> fees = stats.get('Ongoing charge', 'Not available')
|
|
165
|
+
"""
|
|
166
|
+
profile_df = get_fund_profile(xid)
|
|
167
|
+
|
|
168
|
+
if profile_df.empty:
|
|
169
|
+
return {}
|
|
170
|
+
|
|
171
|
+
# Convert DataFrame to dictionary for easy access
|
|
172
|
+
stats_dict = dict(zip(profile_df['Field'], profile_df['Value']))
|
|
173
|
+
|
|
174
|
+
logger.info(f"Converted profile data to dictionary with {len(stats_dict)} fields")
|
|
175
|
+
return stats_dict
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_available_fields(xid: str) -> list:
|
|
179
|
+
"""
|
|
180
|
+
Get list of all available profile fields for a fund.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
xid: The FT Markets XID
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of all field names available for this fund
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
>>> xid = get_xid('SPY')
|
|
190
|
+
>>> fields = get_available_fields(xid)
|
|
191
|
+
>>> print("Available fields:")
|
|
192
|
+
>>> for field in fields:
|
|
193
|
+
>>> print(f" - {field}")
|
|
194
|
+
"""
|
|
195
|
+
profile_df = get_fund_profile(xid)
|
|
196
|
+
|
|
197
|
+
if profile_df.empty:
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
field_list = profile_df['Field'].tolist()
|
|
201
|
+
logger.info(f"Found {len(field_list)} available fields")
|
|
202
|
+
return field_list
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def search_profile_field(xid: str, search_term: str) -> pd.DataFrame:
|
|
206
|
+
"""
|
|
207
|
+
Search for specific fields in the fund profile data.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
xid: The FT Markets XID
|
|
211
|
+
search_term: Term to search for in field names (case-insensitive)
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
pandas.DataFrame with matching fields and values
|
|
215
|
+
|
|
216
|
+
Examples:
|
|
217
|
+
>>> xid = get_xid('SPY')
|
|
218
|
+
>>> fees = search_profile_field(xid, 'fee')
|
|
219
|
+
>>> inception = search_profile_field(xid, 'inception')
|
|
220
|
+
"""
|
|
221
|
+
profile_df = get_fund_profile(xid)
|
|
222
|
+
|
|
223
|
+
if profile_df.empty:
|
|
224
|
+
return pd.DataFrame(columns=['Field', 'Value'])
|
|
225
|
+
|
|
226
|
+
# Filter for fields containing the search term
|
|
227
|
+
matches = profile_df[profile_df['Field'].str.contains(search_term, case=False, na=False)]
|
|
228
|
+
|
|
229
|
+
logger.info(f"Found {len(matches)} fields matching '{search_term}'")
|
|
230
|
+
return matches
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Export functions
|
|
234
|
+
__all__ = [
|
|
235
|
+
"get_fund_profile",
|
|
236
|
+
"get_fund_stats",
|
|
237
|
+
"get_available_fields",
|
|
238
|
+
"search_profile_field"
|
|
239
|
+
]
|
ftgo/search.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search functionality for FTMarkets library.
|
|
3
|
+
|
|
4
|
+
This module provides functions to search for financial instruments
|
|
5
|
+
and retrieve their XIDs from FT Markets.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import cloudscraper
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from urllib.parse import quote
|
|
11
|
+
from typing import Dict, Any, Union
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
# Set up logging
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fetch_search_data(search_string: str) -> Dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Fetch search data for a given search string from FT Markets API.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
search_string: The ticker symbol or name to search for
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
JSON response from the API
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
requests.exceptions.HTTPError: If the API request fails
|
|
30
|
+
"""
|
|
31
|
+
scraper = cloudscraper.create_scraper(
|
|
32
|
+
browser={
|
|
33
|
+
'browser': 'chrome',
|
|
34
|
+
'platform': 'windows',
|
|
35
|
+
'desktop': True
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
url = f"https://markets.ft.com/data/searchapi/searchsecurities?query={quote(search_string)}"
|
|
41
|
+
response = scraper.get(url)
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
return response.json()
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.error(f"Failed to fetch search data for {search_string}: {e}")
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def search_to_dataframe(json_data: Dict[str, Any]) -> pd.DataFrame:
|
|
50
|
+
"""
|
|
51
|
+
Convert search JSON response to a pandas DataFrame.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
json_data: JSON response from the FT Markets search API
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
pandas.DataFrame with columns: xid, name, symbol, asset_class, url
|
|
58
|
+
|
|
59
|
+
Note:
|
|
60
|
+
Returns empty DataFrame if no data is found
|
|
61
|
+
"""
|
|
62
|
+
if 'data' not in json_data or 'security' not in json_data['data']:
|
|
63
|
+
logger.warning("No search data found in JSON response")
|
|
64
|
+
return pd.DataFrame()
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
securities = json_data['data']['security']
|
|
68
|
+
|
|
69
|
+
# Process each security
|
|
70
|
+
processed_securities = []
|
|
71
|
+
for security in securities:
|
|
72
|
+
processed_securities.append({
|
|
73
|
+
'xid': security.get('xid'),
|
|
74
|
+
'name': security.get('name'),
|
|
75
|
+
'symbol': security.get('symbol'),
|
|
76
|
+
'asset_class': security.get('assetClass'),
|
|
77
|
+
'url': security.get('url')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return pd.DataFrame(processed_securities)
|
|
81
|
+
|
|
82
|
+
except (KeyError, IndexError) as e:
|
|
83
|
+
logger.error(f"Error processing search JSON data: {e}")
|
|
84
|
+
return pd.DataFrame()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def search_securities(query: str) -> pd.DataFrame:
|
|
88
|
+
"""
|
|
89
|
+
Search for securities on FT Markets.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
query: Search term for securities (ticker symbol or company name)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
pandas.DataFrame with search results containing:
|
|
96
|
+
- xid: FT Markets identifier
|
|
97
|
+
- name: Company/security name
|
|
98
|
+
- symbol: Trading symbol
|
|
99
|
+
- asset_class: Type of security (Equities, ETFs, etc.)
|
|
100
|
+
- url: FT Markets URL path
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If query is empty
|
|
104
|
+
requests.exceptions.HTTPError: If API request fails
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> results = search_securities('Apple')
|
|
108
|
+
>>> print(results.head())
|
|
109
|
+
"""
|
|
110
|
+
if not query:
|
|
111
|
+
raise ValueError("Query parameter cannot be empty")
|
|
112
|
+
|
|
113
|
+
logger.info(f"Searching for securities with query: {query}")
|
|
114
|
+
|
|
115
|
+
json_data = fetch_search_data(query)
|
|
116
|
+
df = search_to_dataframe(json_data)
|
|
117
|
+
|
|
118
|
+
logger.info(f"Found {len(df)} securities for query: {query}")
|
|
119
|
+
return df
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_xid(
|
|
123
|
+
ticker: str,
|
|
124
|
+
display_mode: str = "first"
|
|
125
|
+
) -> Union[str, pd.DataFrame]:
|
|
126
|
+
"""
|
|
127
|
+
Get FT Markets XID for given ticker symbol.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
ticker: Single ticker symbol
|
|
131
|
+
display_mode: "first" to return first match XID, "all" to return all matches
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
- If display_mode="first": String XID of first match
|
|
135
|
+
- If display_mode="all": pandas.DataFrame with all search results
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If parameters are invalid or no data is found
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
>>> xid = get_xid('AAPL')
|
|
142
|
+
>>> print(xid) # '36276'
|
|
143
|
+
|
|
144
|
+
>>> all_results = get_xid('AAPL', display_mode='all')
|
|
145
|
+
>>> print(all_results)
|
|
146
|
+
"""
|
|
147
|
+
if not ticker:
|
|
148
|
+
raise ValueError("Ticker parameter cannot be empty")
|
|
149
|
+
|
|
150
|
+
df = search_securities(ticker)
|
|
151
|
+
|
|
152
|
+
if df.empty:
|
|
153
|
+
raise ValueError(f"No data found for ticker: {ticker}")
|
|
154
|
+
|
|
155
|
+
if display_mode == "all":
|
|
156
|
+
return df
|
|
157
|
+
elif display_mode == "first":
|
|
158
|
+
return df.iloc[0]['xid']
|
|
159
|
+
else:
|
|
160
|
+
raise ValueError("Invalid display_mode. Choose 'first' or 'all'.")
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ftgo
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python library for fetching financial data from FT Markets
|
|
5
|
+
Home-page: https://github.com/gohibiki/ftgo
|
|
6
|
+
Author: gohibiki
|
|
7
|
+
Author-email: gohibiki <gohibiki@protonmail.com>
|
|
8
|
+
Maintainer-email: gohibiki <gohibiki@protonmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Project-URL: Homepage, https://github.com/gohibiki/ftgo
|
|
11
|
+
Project-URL: Repository, https://github.com/gohibiki/ftgo
|
|
12
|
+
Project-URL: Documentation, https://github.com/gohibiki/ftgo#readme
|
|
13
|
+
Project-URL: Bug Reports, https://github.com/gohibiki/ftgo/issues
|
|
14
|
+
Project-URL: Changelog, https://github.com/gohibiki/ftgo/blob/main/CHANGELOG.md
|
|
15
|
+
Keywords: finance,stocks,etf,historical-data,financial-analysis,market-data,ft-markets,financial-times
|
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
|
17
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
30
|
+
Requires-Python: >=3.7
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Requires-Dist: cloudscraper>=1.2.68
|
|
34
|
+
Requires-Dist: pandas>=1.3.0
|
|
35
|
+
Requires-Dist: beautifulsoup4>=4.11.0
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
43
|
+
Provides-Extra: examples
|
|
44
|
+
Requires-Dist: matplotlib>=3.6.0; extra == "examples"
|
|
45
|
+
Requires-Dist: jupyter>=1.0.0; extra == "examples"
|
|
46
|
+
Dynamic: author
|
|
47
|
+
Dynamic: home-page
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
Dynamic: requires-python
|
|
50
|
+
|
|
51
|
+
# FTMarkets
|
|
52
|
+
|
|
53
|
+
[](https://badge.fury.io/py/ftgo)
|
|
54
|
+
[](https://www.python.org/downloads/)
|
|
55
|
+
[](https://opensource.org/licenses/MIT)
|
|
56
|
+
|
|
57
|
+
A Python library for fetching financial data from Financial Times Markets, including historical stock prices, ETF holdings, fund profiles, and allocation breakdowns.
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- **Historical Data**: Fetch historical OHLCV data for stocks and ETFs
|
|
62
|
+
- **Holdings Data**: Get ETF/fund holdings, asset allocation, and sector breakdowns
|
|
63
|
+
- **Fund Profiles**: Access fund information, statistics, and investment details
|
|
64
|
+
- **Symbol Search**: Find FT Markets XIDs by ticker symbols
|
|
65
|
+
- **Concurrent Processing**: Fast data retrieval using multithreading
|
|
66
|
+
- **Pandas Integration**: Returns data as pandas DataFrames for easy analysis
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install ftgo
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quick Start
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from ftgo import search_securities, get_xid, get_historical_prices, get_holdings
|
|
78
|
+
|
|
79
|
+
# Search for a security
|
|
80
|
+
results = search_securities('AAPL')
|
|
81
|
+
print(results)
|
|
82
|
+
|
|
83
|
+
# Get XID for a ticker
|
|
84
|
+
xid = get_xid('AAPL')
|
|
85
|
+
|
|
86
|
+
# Fetch historical data
|
|
87
|
+
df = get_historical_prices(xid, "01012024", "31012024")
|
|
88
|
+
print(df.head())
|
|
89
|
+
|
|
90
|
+
# Get ETF holdings
|
|
91
|
+
spy_xid = get_xid('SPY')
|
|
92
|
+
holdings = get_holdings(spy_xid, "top_holdings")
|
|
93
|
+
print(holdings)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## API Reference
|
|
97
|
+
|
|
98
|
+
### Search Functions
|
|
99
|
+
|
|
100
|
+
#### `search_securities(query)`
|
|
101
|
+
|
|
102
|
+
Search for securities on FT Markets.
|
|
103
|
+
|
|
104
|
+
**Parameters:**
|
|
105
|
+
- `query` (str): Search term for securities (ticker symbol or company name)
|
|
106
|
+
|
|
107
|
+
**Returns:** pandas.DataFrame with search results containing xid, name, symbol, asset_class, url
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Search for Apple
|
|
111
|
+
results = search_securities('Apple')
|
|
112
|
+
print(results)
|
|
113
|
+
|
|
114
|
+
# Search by ticker
|
|
115
|
+
results = search_securities('AAPL')
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### `get_xid(ticker, display_mode="first")`
|
|
119
|
+
|
|
120
|
+
Get FT Markets XID for given ticker symbol.
|
|
121
|
+
|
|
122
|
+
**Parameters:**
|
|
123
|
+
- `ticker` (str): Ticker symbol
|
|
124
|
+
- `display_mode` (str): "first" to return first match XID, "all" to return all matches
|
|
125
|
+
|
|
126
|
+
**Returns:** String XID (if display_mode="first") or DataFrame (if display_mode="all")
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# Get XID for Apple
|
|
130
|
+
xid = get_xid('AAPL')
|
|
131
|
+
print(xid) # Returns XID string
|
|
132
|
+
|
|
133
|
+
# Get all matches
|
|
134
|
+
all_results = get_xid('AAPL', display_mode='all')
|
|
135
|
+
print(all_results)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Historical Data
|
|
139
|
+
|
|
140
|
+
#### `get_historical_prices(xid, date_from, date_to)`
|
|
141
|
+
|
|
142
|
+
Get historical price data for a security with full OHLCV data.
|
|
143
|
+
|
|
144
|
+
**Parameters:**
|
|
145
|
+
- `xid` (str): The FT Markets XID
|
|
146
|
+
- `date_from` (str): Start date in DDMMYYYY format (e.g., "01012024")
|
|
147
|
+
- `date_to` (str): End date in DDMMYYYY format (e.g., "31122024")
|
|
148
|
+
|
|
149
|
+
**Returns:** pandas.DataFrame with columns: date, open, high, low, close, volume
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
xid = get_xid('AAPL')
|
|
153
|
+
df = get_historical_prices(xid, "01012024", "31012024")
|
|
154
|
+
print(df.head())
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `get_multiple_historical_prices(xids, date_from, date_to)`
|
|
158
|
+
|
|
159
|
+
Get historical data for multiple securities concurrently.
|
|
160
|
+
|
|
161
|
+
**Parameters:**
|
|
162
|
+
- `xids` (list): List of FT Markets XIDs
|
|
163
|
+
- `date_from` (str): Start date in DDMMYYYY format
|
|
164
|
+
- `date_to` (str): End date in DDMMYYYY format
|
|
165
|
+
|
|
166
|
+
**Returns:** pandas.DataFrame with concatenated data for all securities
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
xids = [get_xid('AAPL'), get_xid('MSFT')]
|
|
170
|
+
df = get_multiple_historical_prices(xids, "01012024", "31012024")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Holdings Data
|
|
174
|
+
|
|
175
|
+
#### `get_holdings(xid, holdings_type="all")`
|
|
176
|
+
|
|
177
|
+
Get holdings and allocation data for ETFs and funds.
|
|
178
|
+
|
|
179
|
+
**Parameters:**
|
|
180
|
+
- `xid` (str): The FT Markets XID
|
|
181
|
+
- `holdings_type` (str): Type of holdings data:
|
|
182
|
+
- `"asset_allocation"`: Asset class breakdown (stocks, bonds, cash)
|
|
183
|
+
- `"sector_weights"`: Sector allocation
|
|
184
|
+
- `"geographic_allocation"`: Geographic allocation
|
|
185
|
+
- `"top_holdings"`: Top holdings by weight
|
|
186
|
+
- `"all"`: All holdings data types as a tuple
|
|
187
|
+
|
|
188
|
+
**Returns:** pandas.DataFrame or tuple of DataFrames
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
# Get top holdings for SPY ETF
|
|
192
|
+
spy_xid = get_xid('SPY')
|
|
193
|
+
top_holdings = get_holdings(spy_xid, "top_holdings")
|
|
194
|
+
|
|
195
|
+
# Get asset allocation
|
|
196
|
+
allocation = get_holdings(spy_xid, "asset_allocation")
|
|
197
|
+
|
|
198
|
+
# Get all holdings data
|
|
199
|
+
all_data = get_holdings(spy_xid, "all")
|
|
200
|
+
asset_alloc, sectors, regions, holdings = all_data
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### `get_fund_breakdown(xid)`
|
|
204
|
+
|
|
205
|
+
Get complete fund breakdown with all allocation data.
|
|
206
|
+
|
|
207
|
+
**Parameters:**
|
|
208
|
+
- `xid` (str): The FT Markets XID
|
|
209
|
+
|
|
210
|
+
**Returns:** Dictionary with all DataFrames
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
qqq_xid = get_xid('QQQ')
|
|
214
|
+
breakdown = get_fund_breakdown(qqq_xid)
|
|
215
|
+
print(breakdown['asset_allocation'])
|
|
216
|
+
print(breakdown['top_holdings'])
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Fund Profile Data
|
|
220
|
+
|
|
221
|
+
#### `get_fund_profile(xid)`
|
|
222
|
+
|
|
223
|
+
Get profile and investment information for ETFs and funds.
|
|
224
|
+
|
|
225
|
+
**Parameters:**
|
|
226
|
+
- `xid` (str): The FT Markets XID
|
|
227
|
+
|
|
228
|
+
**Returns:** pandas.DataFrame with Field and Value columns
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
xid = get_xid('SPY')
|
|
232
|
+
profile = get_fund_profile(xid)
|
|
233
|
+
print(profile)
|
|
234
|
+
|
|
235
|
+
# Filter for specific information
|
|
236
|
+
fees = profile[profile['Field'].str.contains('fee', case=False)]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `get_fund_stats(xid)`
|
|
240
|
+
|
|
241
|
+
Get fund profile data as a dictionary for easy access.
|
|
242
|
+
|
|
243
|
+
**Parameters:**
|
|
244
|
+
- `xid` (str): The FT Markets XID
|
|
245
|
+
|
|
246
|
+
**Returns:** Dictionary with all available fund fields and values
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
xid = get_xid('QQQ')
|
|
250
|
+
stats = get_fund_stats(xid)
|
|
251
|
+
|
|
252
|
+
# Access any available field safely
|
|
253
|
+
inception = stats.get('Inception date', 'Not available')
|
|
254
|
+
fees = stats.get('Ongoing charge', 'Not available')
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### `get_available_fields(xid)`
|
|
258
|
+
|
|
259
|
+
Get list of all available profile fields for a fund.
|
|
260
|
+
|
|
261
|
+
**Parameters:**
|
|
262
|
+
- `xid` (str): The FT Markets XID
|
|
263
|
+
|
|
264
|
+
**Returns:** List of all field names available
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
xid = get_xid('SPY')
|
|
268
|
+
fields = get_available_fields(xid)
|
|
269
|
+
print("Available fields:")
|
|
270
|
+
for field in fields:
|
|
271
|
+
print(f" - {field}")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### `search_profile_field(xid, search_term)`
|
|
275
|
+
|
|
276
|
+
Search for specific fields in the fund profile data.
|
|
277
|
+
|
|
278
|
+
**Parameters:**
|
|
279
|
+
- `xid` (str): The FT Markets XID
|
|
280
|
+
- `search_term` (str): Term to search for in field names (case-insensitive)
|
|
281
|
+
|
|
282
|
+
**Returns:** pandas.DataFrame with matching fields and values
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
xid = get_xid('SPY')
|
|
286
|
+
fees = search_profile_field(xid, 'fee')
|
|
287
|
+
inception = search_profile_field(xid, 'inception')
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Complete Example
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from ftgo import get_xid, get_historical_prices, get_holdings, get_fund_profile
|
|
294
|
+
import matplotlib.pyplot as plt
|
|
295
|
+
|
|
296
|
+
# Search for QQQ ETF
|
|
297
|
+
qqq_xid = get_xid('QQQ')
|
|
298
|
+
|
|
299
|
+
# Get 1 year of historical data
|
|
300
|
+
historical_data = get_historical_prices(qqq_xid, "01012023", "31122023")
|
|
301
|
+
|
|
302
|
+
# Get fund information
|
|
303
|
+
profile = get_fund_profile(qqq_xid)
|
|
304
|
+
top_holdings = get_holdings(qqq_xid, "top_holdings")
|
|
305
|
+
asset_allocation = get_holdings(qqq_xid, "asset_allocation")
|
|
306
|
+
|
|
307
|
+
# Plot price chart
|
|
308
|
+
historical_data.set_index('date')['close'].plot(title='QQQ Price History')
|
|
309
|
+
plt.show()
|
|
310
|
+
|
|
311
|
+
# Display fund information
|
|
312
|
+
print("Fund Profile:")
|
|
313
|
+
print(profile.head(10))
|
|
314
|
+
|
|
315
|
+
print("\nTop 10 Holdings:")
|
|
316
|
+
print(top_holdings.head(10))
|
|
317
|
+
|
|
318
|
+
print("\nAsset Allocation:")
|
|
319
|
+
print(asset_allocation)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Error Handling
|
|
323
|
+
|
|
324
|
+
The library includes logging and error handling, but you should wrap calls in try-except blocks for production use:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
try:
|
|
328
|
+
xid = get_xid('INVALID_TICKER')
|
|
329
|
+
data = get_historical_prices(xid, "01012024", "31012024")
|
|
330
|
+
except ValueError as e:
|
|
331
|
+
print(f"Error: {e}")
|
|
332
|
+
except Exception as e:
|
|
333
|
+
print(f"Unexpected error: {e}")
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Requirements
|
|
337
|
+
|
|
338
|
+
- Python 3.7+
|
|
339
|
+
- cloudscraper >= 1.2.68
|
|
340
|
+
- pandas >= 1.3.0
|
|
341
|
+
- beautifulsoup4 >= 4.11.0
|
|
342
|
+
|
|
343
|
+
## Contributing
|
|
344
|
+
|
|
345
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
346
|
+
|
|
347
|
+
## Disclaimer
|
|
348
|
+
|
|
349
|
+
This library is for educational and research purposes.
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ftgo/__init__.py,sha256=5sx2Wl-FnWfEtCF-m02vTaye3A_utwfsejmy9wTf_fM,937
|
|
2
|
+
ftgo/historical.py,sha256=nbTEnG4IMVwoRHx1HlrsePtMGlv_nLAsdAZ4fz0oChE,8209
|
|
3
|
+
ftgo/holdings.py,sha256=f-7BxyETvTfh-K0btptT8lIoePXXLQFTRL19SpwF-Lc,9990
|
|
4
|
+
ftgo/infos.py,sha256=MEvCOOe7ur0sz_Kr447bHINmSbSRgUhkAF9OHhBlrbk,7110
|
|
5
|
+
ftgo/search.py,sha256=io21frgPPQ1OX2Ye_aVT3tBE-UpHI6lXdpWGRapAnyg,4620
|
|
6
|
+
ftgo-1.0.0.dist-info/licenses/LICENSE,sha256=WArc03ET22jEfE5yYGtaFIZ2Jk1mV2P94R4bO8ryrVI,1065
|
|
7
|
+
ftgo-1.0.0.dist-info/METADATA,sha256=Wxar2b9idLGL82FuCBQLCNL9SPAi6fuKd1BsxINcMtw,9768
|
|
8
|
+
ftgo-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
ftgo-1.0.0.dist-info/entry_points.txt,sha256=eDQL8opHZgWyzkqYnG88hz6izwFkbHfWQ4cmu0iCmH4,61
|
|
10
|
+
ftgo-1.0.0.dist-info/top_level.txt,sha256=mNuQHBAj10xcs7AUQk0pAsXGMHGV3cwjxuGvTIurrP0,5
|
|
11
|
+
ftgo-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 gohibiki
|
|
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 @@
|
|
|
1
|
+
ftgo
|