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 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
+ [![PyPI version](https://badge.fury.io/py/ftgo.svg)](https://badge.fury.io/py/ftgo)
54
+ [![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)
55
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ftgo-demo = ftgo.examples.basic_usage:main
@@ -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