openstockapi 0.1.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.
@@ -0,0 +1,32 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from openstockapi.api.stock import ohlcv, profile, quote, async_ohlcv
4
+ from openstockapi.api.financial import income_statement, balance_sheet, cashflow, ratios
5
+ from openstockapi.api.trading import foreign, insider, prop_trade
6
+ from openstockapi.api.orderbook import bid_ask, depth
7
+ from openstockapi.api.macro import indicators
8
+ from openstockapi.api.fund import details as fund_details
9
+ from openstockapi.api.news import news as company_news, events as company_events
10
+
11
+ from openstockapi.core import exceptions
12
+ from openstockapi.license import session
13
+
14
+ __all__ = [
15
+ "ohlcv",
16
+ "async_ohlcv",
17
+ "profile",
18
+ "quote",
19
+ "income_statement",
20
+ "balance_sheet",
21
+ "cashflow",
22
+ "ratios",
23
+ "foreign",
24
+ "insider",
25
+ "prop_trade",
26
+ "bid_ask",
27
+ "depth",
28
+ "indicators",
29
+ "fund_details",
30
+ "company_news",
31
+ "company_events",
32
+ ]
@@ -0,0 +1,48 @@
1
+ from typing import List, Optional, Union, Any
2
+ from openstockapi.core.types import DataTier
3
+ from openstockapi.core.security import enforce_tier_and_rate_limit
4
+ from openstockapi.core.utils import clean_symbol
5
+ from openstockapi.config.settings import DEFAULT_PROVIDER_PRIORITY
6
+ from openstockapi.providers import get_provider
7
+ from openstockapi.core.exceptions import ProviderUnavailableError
8
+
9
+ try:
10
+ import pandas as pd
11
+ HAS_PANDAS = True
12
+ except ImportError:
13
+ HAS_PANDAS = False
14
+
15
+ def _fetch_financial(symbol: str, stmt_type: str, period: str, provider: Optional[str]) -> Union[List[dict], Any]:
16
+ enforce_tier_and_rate_limit(DataTier.FREE, f"financial.{stmt_type}")
17
+ symbol = clean_symbol(symbol)
18
+
19
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("financials", ["mas"])
20
+
21
+ last_err = None
22
+ for p_name in providers_to_try:
23
+ p_inst = get_provider(p_name)
24
+ if not p_inst:
25
+ continue
26
+ try:
27
+ reports = p_inst.get_financial_statements(symbol, stmt_type, period)
28
+ data_list = [rep.model_dump() for rep in reports]
29
+ if HAS_PANDAS:
30
+ return pd.DataFrame(data_list)
31
+ return data_list
32
+ except Exception as e:
33
+ last_err = e
34
+ continue
35
+
36
+ raise ProviderUnavailableError(f"No provider succeeded in fetching financial statement '{stmt_type}' for '{symbol}': {last_err}")
37
+
38
+ def income_statement(symbol: str, period: str = "Q", provider: Optional[str] = None) -> Union[List[dict], Any]:
39
+ return _fetch_financial(symbol, "income", period, provider)
40
+
41
+ def balance_sheet(symbol: str, period: str = "Q", provider: Optional[str] = None) -> Union[List[dict], Any]:
42
+ return _fetch_financial(symbol, "balance", period, provider)
43
+
44
+ def cashflow(symbol: str, period: str = "Q", provider: Optional[str] = None) -> Union[List[dict], Any]:
45
+ return _fetch_financial(symbol, "cashflow", period, provider)
46
+
47
+ def ratios(symbol: str, period: str = "Q", provider: Optional[str] = None) -> Union[List[dict], Any]:
48
+ return _fetch_financial(symbol, "ratios", period, provider)
@@ -0,0 +1,32 @@
1
+ from typing import Optional, Union, Any
2
+ from openstockapi.core.types import DataTier
3
+ from openstockapi.core.security import enforce_tier_and_rate_limit
4
+ from openstockapi.config.settings import DEFAULT_PROVIDER_PRIORITY
5
+ from openstockapi.providers import get_provider
6
+ from openstockapi.core.exceptions import ProviderUnavailableError
7
+
8
+ try:
9
+ import pandas as pd
10
+ HAS_PANDAS = True
11
+ except ImportError:
12
+ HAS_PANDAS = False
13
+
14
+ def details(fund_id: int, provider: Optional[str] = None) -> Union[dict, Any]:
15
+ # Mutual fund data is a FREE endpoint
16
+ enforce_tier_and_rate_limit(DataTier.FREE, "fund.details")
17
+
18
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("fund", ["fmarket"])
19
+
20
+ last_err = None
21
+ for p_name in providers_to_try:
22
+ p_inst = get_provider(p_name)
23
+ if not p_inst:
24
+ continue
25
+ try:
26
+ fund_data = p_inst.get_fund_details(fund_id)
27
+ return fund_data.model_dump()
28
+ except Exception as e:
29
+ last_err = e
30
+ continue
31
+
32
+ raise ProviderUnavailableError(f"No provider succeeded in fetching details for fund_id '{fund_id}': {last_err}")
@@ -0,0 +1,35 @@
1
+ from typing import List, Optional, Union, Any
2
+ from openstockapi.core.types import DataTier
3
+ from openstockapi.core.security import enforce_tier_and_rate_limit
4
+ from openstockapi.config.settings import DEFAULT_PROVIDER_PRIORITY
5
+ from openstockapi.providers import get_provider
6
+ from openstockapi.core.exceptions import ProviderUnavailableError
7
+
8
+ try:
9
+ import pandas as pd
10
+ HAS_PANDAS = True
11
+ except ImportError:
12
+ HAS_PANDAS = False
13
+
14
+ def indicators(provider: Optional[str] = None) -> Union[List[dict], Any]:
15
+ # Macro indicators is a FREE endpoint
16
+ enforce_tier_and_rate_limit(DataTier.FREE, "macro.indicators")
17
+
18
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("macro", ["mbk"])
19
+
20
+ last_err = None
21
+ for p_name in providers_to_try:
22
+ p_inst = get_provider(p_name)
23
+ if not p_inst:
24
+ continue
25
+ try:
26
+ records = p_inst.get_macro_indicators()
27
+ data_list = [rec.model_dump() for rec in records]
28
+ if HAS_PANDAS:
29
+ return pd.DataFrame(data_list)
30
+ return data_list
31
+ except Exception as e:
32
+ last_err = e
33
+ continue
34
+
35
+ raise ProviderUnavailableError(f"No provider succeeded in fetching macro indicators: {last_err}")
@@ -0,0 +1,60 @@
1
+ from typing import List, Optional, Union, Any
2
+ from openstockapi.core.types import DataTier
3
+ from openstockapi.core.security import enforce_tier_and_rate_limit
4
+ from openstockapi.core.utils import clean_symbol
5
+ from openstockapi.config.settings import DEFAULT_PROVIDER_PRIORITY
6
+ from openstockapi.providers import get_provider
7
+ from openstockapi.core.exceptions import ProviderUnavailableError
8
+
9
+ try:
10
+ import pandas as pd
11
+ HAS_PANDAS = True
12
+ except ImportError:
13
+ HAS_PANDAS = False
14
+
15
+ def news(symbol: str, limit: int = 10, provider: Optional[str] = None) -> Union[List[dict], Any]:
16
+ # News is a FREE endpoint
17
+ enforce_tier_and_rate_limit(DataTier.FREE, "news")
18
+ symbol = clean_symbol(symbol)
19
+
20
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("news", ["kbs"])
21
+
22
+ last_err = None
23
+ for p_name in providers_to_try:
24
+ p_inst = get_provider(p_name)
25
+ if not p_inst:
26
+ continue
27
+ try:
28
+ items = p_inst.get_company_news(symbol, limit)
29
+ data_list = [item.model_dump() for item in items]
30
+ if HAS_PANDAS:
31
+ return pd.DataFrame(data_list)
32
+ return data_list
33
+ except Exception as e:
34
+ last_err = e
35
+ continue
36
+
37
+ raise ProviderUnavailableError(f"No provider succeeded in fetching news for symbol '{symbol}': {last_err}")
38
+
39
+ def events(symbol: str, limit: int = 10, provider: Optional[str] = None) -> Union[List[dict], Any]:
40
+ enforce_tier_and_rate_limit(DataTier.FREE, "events")
41
+ symbol = clean_symbol(symbol)
42
+
43
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("news", ["kbs"])
44
+
45
+ last_err = None
46
+ for p_name in providers_to_try:
47
+ p_inst = get_provider(p_name)
48
+ if not p_inst:
49
+ continue
50
+ try:
51
+ items = p_inst.get_company_events(symbol, limit)
52
+ data_list = [item.model_dump() for item in items]
53
+ if HAS_PANDAS:
54
+ return pd.DataFrame(data_list)
55
+ return data_list
56
+ except Exception as e:
57
+ last_err = e
58
+ continue
59
+
60
+ raise ProviderUnavailableError(f"No provider succeeded in fetching events for symbol '{symbol}': {last_err}")
@@ -0,0 +1,15 @@
1
+ from typing import Optional, Union, Any
2
+ from openstockapi.core.types import DataTier
3
+ from openstockapi.core.security import enforce_tier_and_rate_limit
4
+ from openstockapi.core.utils import clean_symbol
5
+
6
+ def bid_ask(symbol: str) -> Union[dict, Any]:
7
+ # PRO or PREMIUM Tier required
8
+ enforce_tier_and_rate_limit(DataTier.PRO, "orderbook.bid_ask")
9
+ symbol = clean_symbol(symbol)
10
+ raise NotImplementedError("Real-time Order Book APIs are premium only.")
11
+
12
+ def depth(symbol: str) -> Union[dict, Any]:
13
+ enforce_tier_and_rate_limit(DataTier.PRO, "orderbook.depth")
14
+ symbol = clean_symbol(symbol)
15
+ raise NotImplementedError("Real-time Order Book Depth APIs are premium only.")
@@ -0,0 +1,116 @@
1
+ from typing import List, Union, Optional, Any
2
+ from openstockapi.core.types import DataTier
3
+ from openstockapi.core.security import enforce_tier_and_rate_limit
4
+ from openstockapi.core.utils import clean_symbol
5
+ from openstockapi.config.settings import DEFAULT_PROVIDER_PRIORITY
6
+ from openstockapi.providers import get_provider
7
+ from openstockapi.core.exceptions import ProviderUnavailableError
8
+ from openstockapi.core.models import OHLCVBar, CompanyProfile, RealtimeQuote
9
+
10
+ # Optional import for Pandas support
11
+ try:
12
+ import pandas as pd
13
+ HAS_PANDAS = True
14
+ except ImportError:
15
+ HAS_PANDAS = False
16
+
17
+ def ohlcv(symbol: str, resolution: str = "1D", start: Optional[str] = None, end: Optional[str] = None, provider: Optional[str] = None) -> Union[List[dict], Any]:
18
+ # 1. Enforce safety checks
19
+ enforce_tier_and_rate_limit(DataTier.FREE, "stock.ohlcv")
20
+
21
+ # 2. Standardize inputs
22
+ symbol = clean_symbol(symbol)
23
+ if start is None:
24
+ start = "2020-01-01"
25
+ if end is None:
26
+ import datetime
27
+ end = datetime.datetime.now().strftime("%Y-%m-%d")
28
+
29
+ # 3. Resolve provider priority list
30
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("ohlcv", ["dnse"])
31
+
32
+ last_err = None
33
+ for p_name in providers_to_try:
34
+ p_inst = get_provider(p_name)
35
+ if not p_inst:
36
+ continue
37
+ try:
38
+ bars = p_inst.get_ohlcv(symbol, resolution, start, end)
39
+ data_list = [bar.model_dump() for bar in bars]
40
+ if HAS_PANDAS:
41
+ return pd.DataFrame(data_list)
42
+ return data_list
43
+ except Exception as e:
44
+ last_err = e
45
+ continue
46
+
47
+ raise ProviderUnavailableError(f"No provider succeeded in fetching OHLCV for '{symbol}': {last_err}")
48
+
49
+ async def async_ohlcv(symbol: str, resolution: str = "1D", start: Optional[str] = None, end: Optional[str] = None, provider: Optional[str] = None) -> Union[List[dict], Any]:
50
+ enforce_tier_and_rate_limit(DataTier.FREE, "stock.ohlcv")
51
+ symbol = clean_symbol(symbol)
52
+ if start is None:
53
+ start = "2020-01-01"
54
+ if end is None:
55
+ import datetime
56
+ end = datetime.datetime.now().strftime("%Y-%m-%d")
57
+
58
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("ohlcv", ["dnse"])
59
+
60
+ last_err = None
61
+ for p_name in providers_to_try:
62
+ p_inst = get_provider(p_name)
63
+ if not p_inst:
64
+ continue
65
+ try:
66
+ bars = await p_inst.async_get_ohlcv(symbol, resolution, start, end)
67
+ data_list = [bar.model_dump() for bar in bars]
68
+ if HAS_PANDAS:
69
+ return pd.DataFrame(data_list)
70
+ return data_list
71
+ except Exception as e:
72
+ last_err = e
73
+ continue
74
+
75
+ raise ProviderUnavailableError(f"No provider succeeded in fetching async OHLCV for '{symbol}': {last_err}")
76
+
77
+ def profile(symbol: str, provider: Optional[str] = None) -> Union[dict, Any]:
78
+ enforce_tier_and_rate_limit(DataTier.FREE, "stock.profile")
79
+ symbol = clean_symbol(symbol)
80
+
81
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("profile", ["vndirect"])
82
+
83
+ last_err = None
84
+ for p_name in providers_to_try:
85
+ p_inst = get_provider(p_name)
86
+ if not p_inst:
87
+ continue
88
+ try:
89
+ prof = p_inst.get_company_profile(symbol)
90
+ return prof.model_dump()
91
+ except Exception as e:
92
+ last_err = e
93
+ continue
94
+
95
+ raise ProviderUnavailableError(f"No provider succeeded in fetching profile for '{symbol}': {last_err}")
96
+
97
+ def quote(symbol: str, provider: Optional[str] = None) -> Union[dict, Any]:
98
+ # PREMIUM / PRO Tier endpoint
99
+ enforce_tier_and_rate_limit(DataTier.PRO, "stock.quote")
100
+ symbol = clean_symbol(symbol)
101
+
102
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("quote", ["dnse"])
103
+
104
+ last_err = None
105
+ for p_name in providers_to_try:
106
+ p_inst = get_provider(p_name)
107
+ if not p_inst:
108
+ continue
109
+ try:
110
+ q = p_inst.get_realtime_quote(symbol)
111
+ return q.model_dump()
112
+ except Exception as e:
113
+ last_err = e
114
+ continue
115
+
116
+ raise ProviderUnavailableError(f"No provider succeeded in fetching quote for '{symbol}': {last_err}")
@@ -0,0 +1,79 @@
1
+ from typing import List, Optional, Union, Any
2
+ from openstockapi.core.types import DataTier
3
+ from openstockapi.core.security import enforce_tier_and_rate_limit
4
+ from openstockapi.core.utils import clean_symbol
5
+ from openstockapi.config.settings import DEFAULT_PROVIDER_PRIORITY
6
+ from openstockapi.providers import get_provider
7
+ from openstockapi.core.exceptions import ProviderUnavailableError
8
+
9
+ try:
10
+ import pandas as pd
11
+ HAS_PANDAS = True
12
+ except ImportError:
13
+ HAS_PANDAS = False
14
+
15
+ def foreign(symbol: str, limit: int = 10, provider: Optional[str] = None) -> Union[List[dict], Any]:
16
+ enforce_tier_and_rate_limit(DataTier.PRO, "trading.foreign")
17
+ symbol = clean_symbol(symbol)
18
+
19
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("trading", ["vci"])
20
+
21
+ last_err = None
22
+ for p_name in providers_to_try:
23
+ p_inst = get_provider(p_name)
24
+ if not p_inst:
25
+ continue
26
+ try:
27
+ records = p_inst.get_foreign_trading(symbol, limit)
28
+ data_list = [rec.model_dump() for rec in records]
29
+ if HAS_PANDAS:
30
+ return pd.DataFrame(data_list)
31
+ return data_list
32
+ except Exception as e:
33
+ last_err = e
34
+ continue
35
+ raise ProviderUnavailableError(f"No provider succeeded in fetching foreign trading for '{symbol}': {last_err}")
36
+
37
+ def insider(symbol: str, limit: int = 10, provider: Optional[str] = None) -> Union[List[dict], Any]:
38
+ enforce_tier_and_rate_limit(DataTier.PRO, "trading.insider")
39
+ symbol = clean_symbol(symbol)
40
+
41
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("trading", ["vci"])
42
+
43
+ last_err = None
44
+ for p_name in providers_to_try:
45
+ p_inst = get_provider(p_name)
46
+ if not p_inst:
47
+ continue
48
+ try:
49
+ records = p_inst.get_insider_trading(symbol, limit)
50
+ data_list = [rec.model_dump() for rec in records]
51
+ if HAS_PANDAS:
52
+ return pd.DataFrame(data_list)
53
+ return data_list
54
+ except Exception as e:
55
+ last_err = e
56
+ continue
57
+ raise ProviderUnavailableError(f"No provider succeeded in fetching insider transactions for '{symbol}': {last_err}")
58
+
59
+ def prop_trade(symbol: str, limit: int = 10, provider: Optional[str] = None) -> Union[List[dict], Any]:
60
+ enforce_tier_and_rate_limit(DataTier.PRO, "trading.prop_trade")
61
+ symbol = clean_symbol(symbol)
62
+
63
+ providers_to_try = [provider] if provider else DEFAULT_PROVIDER_PRIORITY.get("trading", ["vci"])
64
+
65
+ last_err = None
66
+ for p_name in providers_to_try:
67
+ p_inst = get_provider(p_name)
68
+ if not p_inst:
69
+ continue
70
+ try:
71
+ records = p_inst.get_prop_trading(symbol, limit)
72
+ data_list = [rec.model_dump() for rec in records]
73
+ if HAS_PANDAS:
74
+ return pd.DataFrame(data_list)
75
+ return data_list
76
+ except Exception as e:
77
+ last_err = e
78
+ continue
79
+ raise ProviderUnavailableError(f"No provider succeeded in fetching proprietary trading for '{symbol}': {last_err}")
@@ -0,0 +1,13 @@
1
+ from typing import Dict, List
2
+
3
+ DEFAULT_PROVIDER_PRIORITY: Dict[str, List[str]] = {
4
+ "ohlcv": ["dnse"],
5
+ "financials": ["mas"],
6
+ "profile": ["vndirect"],
7
+ "quote": ["dnse", "vci"],
8
+ "orderbook": ["vci", "dnse"],
9
+ "trading": ["vci"],
10
+ "macro": ["mbk"],
11
+ "fund": ["fmarket"],
12
+ "news": ["kbs"],
13
+ }
@@ -0,0 +1,54 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Dict, Any, Optional
3
+ from openstockapi.core.types import DataTier
4
+ from openstockapi.core.models import OHLCVBar, FinancialItem, CompanyProfile, RealtimeQuote, OrderBook
5
+
6
+ from openstockapi.core.models_trading import ForeignTradingEntry, PropTradingEntry, InsiderTradingEntry
7
+
8
+ class BaseProvider(ABC):
9
+ name: str = "base"
10
+ required_tier: DataTier = DataTier.FREE
11
+
12
+ @abstractmethod
13
+ def get_ohlcv(self, symbol: str, resolution: str, from_date: str, to_date: str) -> List[OHLCVBar]:
14
+ pass
15
+
16
+ @abstractmethod
17
+ def get_financial_statements(self, symbol: str, stmt_type: str, period: str) -> List[FinancialItem]:
18
+ pass
19
+
20
+ def get_company_profile(self, symbol: str) -> CompanyProfile:
21
+ raise NotImplementedError("This provider does not support company profiles")
22
+
23
+ def get_realtime_quote(self, symbol: str) -> RealtimeQuote:
24
+ raise NotImplementedError("This provider does not support realtime quotes")
25
+
26
+ def get_order_book(self, symbol: str) -> OrderBook:
27
+ raise NotImplementedError("This provider does not support order book depth")
28
+
29
+ def get_foreign_trading(self, symbol: str, limit: int = 10) -> List[ForeignTradingEntry]:
30
+ raise NotImplementedError("This provider does not support foreign trading data")
31
+
32
+ def get_prop_trading(self, symbol: str, limit: int = 10) -> List[PropTradingEntry]:
33
+ raise NotImplementedError("This provider does not support proprietary trading data")
34
+
35
+ def get_insider_trading(self, symbol: str, limit: int = 10) -> List[InsiderTradingEntry]:
36
+ raise NotImplementedError("This provider does not support insider trading transactions")
37
+
38
+ def get_macro_indicators(self) -> List[Any]:
39
+ raise NotImplementedError("This provider does not support macro indicators")
40
+
41
+ def get_fund_details(self, fund_id: int) -> Any:
42
+ raise NotImplementedError("This provider does not support fund details data")
43
+
44
+ def get_company_news(self, symbol: str, limit: int = 10) -> List[Any]:
45
+ raise NotImplementedError("This provider does not support company news data")
46
+
47
+ def get_company_events(self, symbol: str, limit: int = 10) -> List[Any]:
48
+ raise NotImplementedError("This provider does not support company events data")
49
+
50
+ async def async_get_ohlcv(self, symbol: str, resolution: str, from_date: str, to_date: str) -> List[OHLCVBar]:
51
+ raise NotImplementedError("This provider does not support async OHLCV")
52
+
53
+ async def async_get_financial_statements(self, symbol: str, stmt_type: str, period: str) -> List[FinancialItem]:
54
+ raise NotImplementedError("This provider does not support async financial statements")
@@ -0,0 +1,29 @@
1
+ class OpenStockAPIError(Exception):
2
+ """Base exception for all OpenStockAPI errors."""
3
+ pass
4
+
5
+ class ProviderUnavailableError(OpenStockAPIError):
6
+ """Raised when a data provider is down or fails to respond."""
7
+ pass
8
+
9
+ class RateLimitError(OpenStockAPIError):
10
+ """Raised when the rate limit for the tier is exceeded."""
11
+ pass
12
+
13
+ class SymbolNotFoundError(OpenStockAPIError):
14
+ """Raised when a ticker symbol is not found."""
15
+ pass
16
+
17
+ class DataParseError(OpenStockAPIError):
18
+ """Raised when parsing data from provider fails."""
19
+ pass
20
+
21
+ class TierUpgradeRequiredError(OpenStockAPIError):
22
+ """Raised when a free user calls a paid endpoint."""
23
+ def __init__(self, required_tier: str, endpoint: str):
24
+ self.required_tier = required_tier
25
+ self.endpoint = endpoint
26
+ super().__init__(
27
+ f"Endpoint '{endpoint}' requires '{required_tier}' tier or above. "
28
+ f"Please upgrade your tier to access this data."
29
+ )
@@ -0,0 +1,62 @@
1
+ import httpx
2
+ import random
3
+ import time
4
+ from typing import Any, Dict, Optional
5
+
6
+ USER_AGENTS = [
7
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
8
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
9
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
10
+ ]
11
+
12
+ class RobustHTTPClient:
13
+ def __init__(self) -> None:
14
+ self.client = httpx.Client(
15
+ timeout=httpx.Timeout(15.0, connect=5.0),
16
+ follow_redirects=True
17
+ )
18
+ self.async_client = httpx.AsyncClient(
19
+ timeout=httpx.Timeout(15.0, connect=5.0),
20
+ follow_redirects=True
21
+ )
22
+
23
+ def request(self, method: str, url: str, retries: int = 3, backoff_factor: float = 0.5, **kwargs: Any) -> httpx.Response:
24
+ headers = kwargs.get("headers", {})
25
+ if "User-Agent" not in headers:
26
+ headers["User-Agent"] = random.choice(USER_AGENTS)
27
+ kwargs["headers"] = headers
28
+
29
+ last_err = None
30
+ for attempt in range(retries):
31
+ try:
32
+ response = self.client.request(method, url, **kwargs)
33
+ response.raise_for_status()
34
+ return response
35
+ except (httpx.HTTPError, httpx.NetworkError) as e:
36
+ last_err = e
37
+ if attempt < retries - 1:
38
+ time.sleep(backoff_factor * (2 ** attempt))
39
+ continue
40
+ raise last_err or httpx.HTTPError("Request failed after retries")
41
+
42
+ async def async_request(self, method: str, url: str, retries: int = 3, backoff_factor: float = 0.5, **kwargs: Any) -> httpx.Response:
43
+ import asyncio
44
+ headers = kwargs.get("headers", {})
45
+ if "User-Agent" not in headers:
46
+ headers["User-Agent"] = random.choice(USER_AGENTS)
47
+ kwargs["headers"] = headers
48
+
49
+ last_err = None
50
+ for attempt in range(retries):
51
+ try:
52
+ response = await self.async_client.request(method, url, **kwargs)
53
+ response.raise_for_status()
54
+ return response
55
+ except (httpx.HTTPError, httpx.NetworkError) as e:
56
+ last_err = e
57
+ if attempt < retries - 1:
58
+ await asyncio.sleep(backoff_factor * (2 ** attempt))
59
+ continue
60
+ raise last_err or httpx.HTTPError("Async request failed after retries")
61
+
62
+ http_client = RobustHTTPClient()
@@ -0,0 +1,67 @@
1
+ from datetime import datetime
2
+ from pydantic import BaseModel
3
+ from typing import Dict, List, Optional, Any
4
+
5
+ class OHLCVBar(BaseModel):
6
+ symbol: str
7
+ timestamp: datetime
8
+ open: float
9
+ high: float
10
+ low: float
11
+ close: float
12
+ volume: int
13
+ provider: str
14
+
15
+ class FinancialItem(BaseModel):
16
+ symbol: str
17
+ year: int
18
+ quarter: Optional[int] = None
19
+ statement_type: str # "income", "balance", "cashflow", "ratios"
20
+ items: Dict[str, Optional[float]]
21
+ provider: str
22
+
23
+ class CompanyProfile(BaseModel):
24
+ symbol: str
25
+ full_name: str
26
+ en_name: Optional[str] = None
27
+ exchange: str
28
+ industry: Optional[str] = None
29
+ website: Optional[str] = None
30
+ employees: Optional[int] = None
31
+ description: Optional[str] = None
32
+ provider: str
33
+
34
+ class RealtimeQuote(BaseModel):
35
+ symbol: str
36
+ price: float
37
+ change: float
38
+ pct_change: float
39
+ volume: int
40
+ timestamp: datetime
41
+ provider: str
42
+
43
+ class OrderBookEntry(BaseModel):
44
+ price: float
45
+ volume: int
46
+
47
+ class OrderBook(BaseModel):
48
+ symbol: str
49
+ bids: List[OrderBookEntry]
50
+ asks: List[OrderBookEntry]
51
+ timestamp: datetime
52
+ provider: str
53
+
54
+ class NewsItem(BaseModel):
55
+ symbol: str
56
+ title: str
57
+ url: Optional[str] = None
58
+ source: Optional[str] = None
59
+ publish_date: datetime
60
+ summary: Optional[str] = None
61
+ provider: str
62
+
63
+ class FundItem(BaseModel):
64
+ fund_code: str
65
+ name: str
66
+ nav: float
67
+ provider: str
@@ -0,0 +1,22 @@
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional
3
+
4
+ class FundHolding(BaseModel):
5
+ ticker: str
6
+ name: Optional[str] = None
7
+ net_asset_percent: float
8
+ asset_value: Optional[float] = None
9
+ volume: Optional[float] = None
10
+
11
+ class FundDetails(BaseModel):
12
+ fund_id: int
13
+ name: str
14
+ short_name: str
15
+ code: str
16
+ price: float
17
+ nav: float
18
+ expected_return: Optional[float] = None
19
+ management_fee: Optional[float] = None
20
+ description: Optional[str] = None
21
+ holdings: List[FundHolding]
22
+ provider: str