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.
- openstockapi/__init__.py +32 -0
- openstockapi/api/financial.py +48 -0
- openstockapi/api/fund.py +32 -0
- openstockapi/api/macro.py +35 -0
- openstockapi/api/news.py +60 -0
- openstockapi/api/orderbook.py +15 -0
- openstockapi/api/stock.py +116 -0
- openstockapi/api/trading.py +79 -0
- openstockapi/config/settings.py +13 -0
- openstockapi/core/base_provider.py +54 -0
- openstockapi/core/exceptions.py +29 -0
- openstockapi/core/http_client.py +62 -0
- openstockapi/core/models.py +67 -0
- openstockapi/core/models_fund.py +22 -0
- openstockapi/core/models_macro.py +12 -0
- openstockapi/core/models_news.py +20 -0
- openstockapi/core/models_trading.py +39 -0
- openstockapi/core/rate_limiter.py +36 -0
- openstockapi/core/security.py +19 -0
- openstockapi/core/types.py +6 -0
- openstockapi/core/utils.py +23 -0
- openstockapi/license/session.py +36 -0
- openstockapi/providers/__init__.py +21 -0
- openstockapi/providers/dnse.py +104 -0
- openstockapi/providers/fmarket.py +58 -0
- openstockapi/providers/kbs.py +69 -0
- openstockapi/providers/mas.py +92 -0
- openstockapi/providers/mbk.py +49 -0
- openstockapi/providers/vci.py +108 -0
- openstockapi/providers/vndirect.py +49 -0
- openstockapi-0.1.0.dist-info/METADATA +46 -0
- openstockapi-0.1.0.dist-info/RECORD +36 -0
- openstockapi-0.1.0.dist-info/WHEEL +5 -0
- openstockapi-0.1.0.dist-info/licenses/LICENSE +176 -0
- openstockapi-0.1.0.dist-info/licenses/NOTICE +5 -0
- openstockapi-0.1.0.dist-info/top_level.txt +1 -0
openstockapi/__init__.py
ADDED
|
@@ -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)
|
openstockapi/api/fund.py
ADDED
|
@@ -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}")
|
openstockapi/api/news.py
ADDED
|
@@ -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
|