financechatbotkit 2.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.
- financechatbotkit-2.0.0.dist-info/METADATA +11 -0
- financechatbotkit-2.0.0.dist-info/RECORD +39 -0
- financechatbotkit-2.0.0.dist-info/WHEEL +5 -0
- financechatbotkit-2.0.0.dist-info/entry_points.txt +2 -0
- financechatbotkit-2.0.0.dist-info/top_level.txt +2 -0
- orchestrator/__init__.py +29 -0
- orchestrator/bond/__init__.py +8 -0
- orchestrator/bond/base_reader.py +139 -0
- orchestrator/bond/getBondBasiInfo.py +84 -0
- orchestrator/bond/getBondWithOptiCallRede.py +83 -0
- orchestrator/bond/getEarlExerOpti.py +90 -0
- orchestrator/bond/getIssuIssuItemStat.py +85 -0
- orchestrator/bond/getOptiExer.py +83 -0
- orchestrator/bond/getOptiExerPricAdju.py +84 -0
- orchestrator/bond/workflow.py +252 -0
- orchestrator/exceptions.py +17 -0
- orchestrator/fnguide/__init__.py +21 -0
- orchestrator/fnguide/workflow.py +391 -0
- orchestrator/mapping/__init__.py +22 -0
- orchestrator/mapping/data/__init__.py +1 -0
- orchestrator/mapping/data/corp_codes_raw.json +693170 -0
- orchestrator/mapping/update_raw_data.py +96 -0
- orchestrator/mapping/workflow.py +303 -0
- orchestrator/price/__init__.py +15 -0
- orchestrator/price/workflow.py +250 -0
- telebotkit/__init__.py +51 -0
- telebotkit/bot/__init__.py +38 -0
- telebotkit/bot/client.py +217 -0
- telebotkit/bot/reply.py +36 -0
- telebotkit/bot/router.py +125 -0
- telebotkit/bot/safety.py +28 -0
- telebotkit/bot/telegram.py +41 -0
- telebotkit/firestore/__init__.py +45 -0
- telebotkit/firestore/client.py +141 -0
- telebotkit/firestore/documents.py +164 -0
- telebotkit/firestore/fetch.py +228 -0
- telebotkit/firestore/locks.py +74 -0
- telebotkit/firestore/upload.py +75 -0
- telebotkit/sheets.py +219 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
3
|
+
from .base_reader import get_datagokr_document
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Read https://www.data.go.kr/data/15059595/openapi.do
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_OptiExerPricAdju(
|
|
12
|
+
serviceKey: str,
|
|
13
|
+
isinCd: str,
|
|
14
|
+
timeout_seconds: float = 60.0,
|
|
15
|
+
) -> Any | None:
|
|
16
|
+
""" 행사가 변동내역 및 현재 행사가 다운로드. """
|
|
17
|
+
if not isinCd:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
service_url = "1160100/service/GetBondRedeInfoService/getOptiExerPricAdju"
|
|
21
|
+
params: dict[str, Any] = {
|
|
22
|
+
"serviceKey": serviceKey,
|
|
23
|
+
"isinCd": isinCd,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return get_datagokr_document(
|
|
27
|
+
serviceUrl=service_url,
|
|
28
|
+
params=params,
|
|
29
|
+
timeout_seconds=timeout_seconds
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_opti_exer_pric_adju(
|
|
34
|
+
serviceKey: str,
|
|
35
|
+
isinCd: str,
|
|
36
|
+
timeout_seconds: float = 60.0,
|
|
37
|
+
) -> list[dict[str, Any]] | None:
|
|
38
|
+
""" Parse get_OptiExerPricAdju results. """
|
|
39
|
+
raw = get_OptiExerPricAdju(
|
|
40
|
+
serviceKey=serviceKey,
|
|
41
|
+
isinCd=isinCd,
|
|
42
|
+
timeout_seconds=timeout_seconds,
|
|
43
|
+
)
|
|
44
|
+
if raw is None:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
body = raw["response"]["body"]
|
|
49
|
+
items_obj = body.get("items")
|
|
50
|
+
except Exception:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def extract_items(items: Any) -> list[dict[str, Any]]:
|
|
54
|
+
if items is None:
|
|
55
|
+
return []
|
|
56
|
+
if isinstance(items, dict) and "item" in items:
|
|
57
|
+
inner = items.get("item")
|
|
58
|
+
if inner is None:
|
|
59
|
+
return []
|
|
60
|
+
if isinstance(inner, list):
|
|
61
|
+
return [i for i in inner if isinstance(i, dict)]
|
|
62
|
+
return [inner] if isinstance(inner, dict) else []
|
|
63
|
+
if isinstance(items, list):
|
|
64
|
+
return [i for i in items if isinstance(i, dict)]
|
|
65
|
+
return [items] if isinstance(items, dict) else []
|
|
66
|
+
|
|
67
|
+
items = extract_items(items_obj)
|
|
68
|
+
if not items:
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
out: list[dict[str, Any]] = []
|
|
72
|
+
for it in items:
|
|
73
|
+
out.append(
|
|
74
|
+
{
|
|
75
|
+
"현재행사가": it.get("optnExertPrc", ""),
|
|
76
|
+
"변경일": it.get("rgtExertPricAdjDt"),
|
|
77
|
+
"이전행사가": it.get("rgtBchgExertPrc"),
|
|
78
|
+
"이후행사가": it.get("rgtAchgExertPrc"),
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# items 에 "rgtExertPricAdjDt": "00010101" 하나만 있으면 items 를 None 으로 처리
|
|
83
|
+
out = [x for x in out if (x.get("rgtExertPricAdjDt") != "00010101")]
|
|
84
|
+
return out
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Data.go.kr bond workflows included directly in FinanceChatbot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..exceptions import InvalidInputError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _empty_market_bond_payload(issue: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
13
|
+
return {
|
|
14
|
+
"issue": issue,
|
|
15
|
+
"optionRedemptions": [],
|
|
16
|
+
"optionExercises": [],
|
|
17
|
+
"priceAdjustments": [],
|
|
18
|
+
"optionSchedules": [],
|
|
19
|
+
"bondBasicInfo": [],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _compact_dict(value: dict[str, Any]) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
key: item
|
|
26
|
+
for key, item in value.items()
|
|
27
|
+
if item is not None and item != "" and item != [] and item != {} and item != "-"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _coerce_list(value: Any) -> list[dict[str, Any]]:
|
|
32
|
+
if not isinstance(value, list):
|
|
33
|
+
return []
|
|
34
|
+
return [item for item in value if isinstance(item, dict)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize_items(items: Any, normalizer) -> list[dict[str, Any]]:
|
|
38
|
+
normalized_items: list[dict[str, Any]] = []
|
|
39
|
+
for item in _coerce_list(items):
|
|
40
|
+
normalized = normalizer(item)
|
|
41
|
+
if normalized:
|
|
42
|
+
normalized_items.append(normalized)
|
|
43
|
+
return normalized_items
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _normalize_issue(value: Any) -> dict[str, Any] | None:
|
|
47
|
+
if not isinstance(value, dict):
|
|
48
|
+
return None
|
|
49
|
+
normalized = _compact_dict(
|
|
50
|
+
{
|
|
51
|
+
"isinCdNm": value.get("isinCdNm"),
|
|
52
|
+
"isinCd": value.get("isinCd"),
|
|
53
|
+
"basDt": value.get("basDt") or value.get("기준일자"),
|
|
54
|
+
"bondIssuDt": value.get("bondIssuDt") or value.get("발행일"),
|
|
55
|
+
"bondExprDt": value.get("bondExprDt") or value.get("만기일"),
|
|
56
|
+
"bondIssuAmt": value.get("bondIssuAmt") or value.get("최초발행권면"),
|
|
57
|
+
"bondPymtAmt": value.get("bondPymtAmt") or value.get("최초납입금액"),
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
return normalized or None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _normalize_redemption_item(item: dict[str, Any]) -> dict[str, Any] | None:
|
|
64
|
+
normalized = _compact_dict(
|
|
65
|
+
{
|
|
66
|
+
"optnTcdNm": item.get("optnTcdNm") or item.get("옵션분류"),
|
|
67
|
+
"opbdClrdDt": item.get("opbdClrdDt") or item.get("행사일자"),
|
|
68
|
+
"opbdPamtPayAmt": item.get("opbdPamtPayAmt") or item.get("행사원금"),
|
|
69
|
+
"opbdIntPayAmt": item.get("opbdIntPayAmt") or item.get("행사이자"),
|
|
70
|
+
"bondIssuAmt": item.get("bondIssuAmt") or item.get("발행잔액"),
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
return normalized or None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _normalize_exercise_item(item: dict[str, Any]) -> dict[str, Any] | None:
|
|
77
|
+
normalized = _compact_dict(
|
|
78
|
+
{
|
|
79
|
+
"rgtOccrBasDt": item.get("rgtOccrBasDt") or item.get("행사일"),
|
|
80
|
+
"optnExertPrc": item.get("optnExertPrc") or item.get("현재행사단가"),
|
|
81
|
+
"exertPric": item.get("exertPric") or item.get("행사단가"),
|
|
82
|
+
"exertAmt": item.get("exertAmt") or item.get("행사금액"),
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
if normalized.get("rgtOccrBasDt") in {"00000000", "00010101"}:
|
|
86
|
+
return None
|
|
87
|
+
return normalized or None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _normalize_adjustment_item(item: dict[str, Any]) -> dict[str, Any] | None:
|
|
91
|
+
normalized = _compact_dict(
|
|
92
|
+
{
|
|
93
|
+
"optnExertPrc": item.get("optnExertPrc") or item.get("현재행사가"),
|
|
94
|
+
"rgtExertPricAdjDt": item.get("rgtExertPricAdjDt") or item.get("변경일"),
|
|
95
|
+
"rgtBchgExertPrc": item.get("rgtBchgExertPrc") or item.get("이전행사가"),
|
|
96
|
+
"rgtAchgExertPrc": item.get("rgtAchgExertPrc") or item.get("이후행사가"),
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
if normalized.get("rgtExertPricAdjDt") in {"00000000", "00010101"}:
|
|
100
|
+
return None
|
|
101
|
+
return normalized or None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _normalize_schedule_item(item: dict[str, Any]) -> dict[str, Any] | None:
|
|
105
|
+
dates = item.get("dates") or item.get("행사일정") or []
|
|
106
|
+
normalized = _compact_dict(
|
|
107
|
+
{
|
|
108
|
+
"type": item.get("type") or item.get("옵션분류"),
|
|
109
|
+
"dates": dates if isinstance(dates, list) else [],
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
return normalized or None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _normalize_basic_info_item(item: dict[str, Any]) -> dict[str, Any] | None:
|
|
116
|
+
normalized = _compact_dict(
|
|
117
|
+
{
|
|
118
|
+
"bondBal": item.get("bondBal") or item.get("발행잔액"),
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
return normalized or None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _orchestrate_bond_api_flow(
|
|
125
|
+
*,
|
|
126
|
+
api_key: str,
|
|
127
|
+
query: str,
|
|
128
|
+
parse_issue,
|
|
129
|
+
parse_redemptions,
|
|
130
|
+
parse_exercises,
|
|
131
|
+
parse_adjustments,
|
|
132
|
+
parse_schedules,
|
|
133
|
+
parse_basic_info,
|
|
134
|
+
issuer_name_from_bond_name,
|
|
135
|
+
normalize_name,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
normalized_query = normalize_name(query)
|
|
138
|
+
issuer_name = issuer_name_from_bond_name(query)
|
|
139
|
+
|
|
140
|
+
issue = parse_issue(
|
|
141
|
+
serviceKey=api_key,
|
|
142
|
+
bondIsurNm=issuer_name,
|
|
143
|
+
bondNm=normalized_query,
|
|
144
|
+
)
|
|
145
|
+
if not isinstance(issue, dict):
|
|
146
|
+
return _empty_market_bond_payload()
|
|
147
|
+
|
|
148
|
+
isin_cd = str(issue.get("isinCd") or "").strip()
|
|
149
|
+
bas_dt = str(issue.get("기준일자") or issue.get("basDt") or "").strip()
|
|
150
|
+
if not isin_cd or not bas_dt:
|
|
151
|
+
return _empty_market_bond_payload(_normalize_issue(issue))
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"issue": _normalize_issue(issue),
|
|
155
|
+
"optionRedemptions": _normalize_items(
|
|
156
|
+
parse_redemptions(serviceKey=api_key, isinCd=isin_cd),
|
|
157
|
+
_normalize_redemption_item,
|
|
158
|
+
),
|
|
159
|
+
"optionExercises": _normalize_items(
|
|
160
|
+
parse_exercises(serviceKey=api_key, isinCd=isin_cd),
|
|
161
|
+
_normalize_exercise_item,
|
|
162
|
+
),
|
|
163
|
+
"priceAdjustments": _normalize_items(
|
|
164
|
+
parse_adjustments(serviceKey=api_key, isinCd=isin_cd),
|
|
165
|
+
_normalize_adjustment_item,
|
|
166
|
+
),
|
|
167
|
+
"optionSchedules": _normalize_items(
|
|
168
|
+
parse_schedules(serviceKey=api_key, basDt=bas_dt, isinCd=isin_cd),
|
|
169
|
+
_normalize_schedule_item,
|
|
170
|
+
),
|
|
171
|
+
"bondBasicInfo": _normalize_items(
|
|
172
|
+
parse_basic_info(serviceKey=api_key, basDt=bas_dt, isinCd=isin_cd),
|
|
173
|
+
_normalize_basic_info_item,
|
|
174
|
+
),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _run_bond_api_flow(api_key: str, query: str) -> dict[str, Any]:
|
|
179
|
+
from .getBondBasiInfo import parse_bond_basi_info # noqa: PLC0415
|
|
180
|
+
from .getBondWithOptiCallRede import parse_bond_with_opti_call_rede # noqa: PLC0415
|
|
181
|
+
from .getEarlExerOpti import parse_earl_exer_opti # noqa: PLC0415
|
|
182
|
+
from .getIssuIssuItemStat import parse_issu_issu_item_stat # noqa: PLC0415
|
|
183
|
+
from .getOptiExer import parse_opti_exer # noqa: PLC0415
|
|
184
|
+
from .getOptiExerPricAdju import parse_opti_exer_pric_adju # noqa: PLC0415
|
|
185
|
+
from .base_reader import issuer_name_from_bond_name, normalize_name # noqa: PLC0415
|
|
186
|
+
|
|
187
|
+
return _orchestrate_bond_api_flow(
|
|
188
|
+
api_key=api_key,
|
|
189
|
+
query=query,
|
|
190
|
+
parse_issue=parse_issu_issu_item_stat,
|
|
191
|
+
parse_redemptions=parse_bond_with_opti_call_rede,
|
|
192
|
+
parse_exercises=parse_opti_exer,
|
|
193
|
+
parse_adjustments=parse_opti_exer_pric_adju,
|
|
194
|
+
parse_schedules=parse_earl_exer_opti,
|
|
195
|
+
parse_basic_info=parse_bond_basi_info,
|
|
196
|
+
issuer_name_from_bond_name=issuer_name_from_bond_name,
|
|
197
|
+
normalize_name=normalize_name,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class MarketBondWorkflow:
|
|
202
|
+
"""Return normalized Data.go.kr bond payloads."""
|
|
203
|
+
|
|
204
|
+
def __init__(self, *, workflow_runner=None) -> None:
|
|
205
|
+
self._workflow_runner = workflow_runner or _run_bond_api_flow
|
|
206
|
+
|
|
207
|
+
def run(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
query: str,
|
|
211
|
+
api_key: str,
|
|
212
|
+
save_path: str | None = None,
|
|
213
|
+
) -> dict[str, Any]:
|
|
214
|
+
normalized_query = str(query or "").strip()
|
|
215
|
+
normalized_api_key = str(api_key or "").strip()
|
|
216
|
+
if not normalized_query:
|
|
217
|
+
raise InvalidInputError("query is required.")
|
|
218
|
+
if not normalized_api_key:
|
|
219
|
+
raise InvalidInputError("api_key is required.")
|
|
220
|
+
|
|
221
|
+
payload = self._workflow_runner(normalized_api_key, normalized_query)
|
|
222
|
+
if not isinstance(payload, dict):
|
|
223
|
+
raise InvalidInputError("bond workflow payload must be a JSON object.")
|
|
224
|
+
|
|
225
|
+
if save_path:
|
|
226
|
+
path = Path(save_path)
|
|
227
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"input": {
|
|
232
|
+
"query": normalized_query,
|
|
233
|
+
"save_path": save_path,
|
|
234
|
+
},
|
|
235
|
+
"data": payload,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
_DEFAULT_WORKFLOW = MarketBondWorkflow()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def run_market_bond_workflow(
|
|
243
|
+
*,
|
|
244
|
+
query: str,
|
|
245
|
+
api_key: str,
|
|
246
|
+
save_path: str | None = None,
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
return _DEFAULT_WORKFLOW.run(
|
|
249
|
+
query=query,
|
|
250
|
+
api_key=api_key,
|
|
251
|
+
save_path=save_path,
|
|
252
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Exceptions for the mapping workflow library."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MappingWorkflowError(Exception):
|
|
5
|
+
"""Base exception for mapping workflow failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvalidInputError(MappingWorkflowError):
|
|
9
|
+
"""Raised when the workflow input contract is violated."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotFoundError(MappingWorkflowError):
|
|
13
|
+
"""Raised when no mapping matches the requested lookup."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DownloadError(MappingWorkflowError):
|
|
17
|
+
"""Raised when latest OpenDart corp-code data cannot be downloaded."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""FnGuide workflows for FinanceChatbot."""
|
|
2
|
+
|
|
3
|
+
from .workflow import (
|
|
4
|
+
FnGuideWorkflow,
|
|
5
|
+
build_finance_ratio_params,
|
|
6
|
+
build_finance_statement_params,
|
|
7
|
+
fetch_fnguide_page,
|
|
8
|
+
parse_fnguide_finance_ratio_html,
|
|
9
|
+
parse_fnguide_finance_statement_html,
|
|
10
|
+
run_fnguide_workflow,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FnGuideWorkflow",
|
|
15
|
+
"build_finance_ratio_params",
|
|
16
|
+
"build_finance_statement_params",
|
|
17
|
+
"fetch_fnguide_page",
|
|
18
|
+
"parse_fnguide_finance_ratio_html",
|
|
19
|
+
"parse_fnguide_finance_statement_html",
|
|
20
|
+
"run_fnguide_workflow",
|
|
21
|
+
]
|