insighta-sdk 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.
- insighta_sdk/__init__.py +41 -0
- insighta_sdk/client.py +144 -0
- insighta_sdk/models.py +212 -0
- insighta_sdk/utils.py +148 -0
- insighta_sdk-0.1.0.dist-info/METADATA +103 -0
- insighta_sdk-0.1.0.dist-info/RECORD +8 -0
- insighta_sdk-0.1.0.dist-info/WHEEL +4 -0
- insighta_sdk-0.1.0.dist-info/licenses/LICENSE +20 -0
insighta_sdk/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Insighta Cloud SDK - API client and data models."""
|
|
2
|
+
|
|
3
|
+
from .client import InsightaClient
|
|
4
|
+
from .models import (
|
|
5
|
+
Credentials,
|
|
6
|
+
UploadConfig,
|
|
7
|
+
OrderGroup,
|
|
8
|
+
CashDeposit,
|
|
9
|
+
Trade,
|
|
10
|
+
Holding,
|
|
11
|
+
Deposit,
|
|
12
|
+
RateEntry,
|
|
13
|
+
Dirs,
|
|
14
|
+
)
|
|
15
|
+
from .utils import (
|
|
16
|
+
load_order_groups,
|
|
17
|
+
load_cash_deposits,
|
|
18
|
+
merge_and_sort_groups,
|
|
19
|
+
fetch_ticker_info,
|
|
20
|
+
load_rate_file,
|
|
21
|
+
lookup_rate,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"InsightaClient",
|
|
26
|
+
"Credentials",
|
|
27
|
+
"UploadConfig",
|
|
28
|
+
"OrderGroup",
|
|
29
|
+
"CashDeposit",
|
|
30
|
+
"Trade",
|
|
31
|
+
"Holding",
|
|
32
|
+
"Deposit",
|
|
33
|
+
"RateEntry",
|
|
34
|
+
"Dirs",
|
|
35
|
+
"load_order_groups",
|
|
36
|
+
"load_cash_deposits",
|
|
37
|
+
"merge_and_sort_groups",
|
|
38
|
+
"fetch_ticker_info",
|
|
39
|
+
"load_rate_file",
|
|
40
|
+
"lookup_rate",
|
|
41
|
+
]
|
insighta_sdk/client.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Insighta OpenAPI client."""
|
|
2
|
+
|
|
3
|
+
import json as _json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .models import Credentials, OrderGroup, UploadConfig
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InsightaClient:
|
|
15
|
+
"""Insighta OpenAPI client."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, credentials: Credentials, output_dir: str = "output"):
|
|
18
|
+
self.endpoint = credentials.endpoint
|
|
19
|
+
self.output_dir = output_dir
|
|
20
|
+
self.headers = {
|
|
21
|
+
"Authorization": f"Bearer {credentials.api_key}",
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def _request(self, method: str, path: str, **kwargs) -> requests.Response:
|
|
26
|
+
url = f"{self.endpoint}{path}"
|
|
27
|
+
payload = kwargs.get("json")
|
|
28
|
+
if payload is not None:
|
|
29
|
+
self._last_payload = payload
|
|
30
|
+
payload_str = _json.dumps(payload, indent=2, ensure_ascii=False)
|
|
31
|
+
log.debug("%s %s\n%s", method, url, payload_str)
|
|
32
|
+
log_path = os.path.join(self.output_dir, "request_payload.log")
|
|
33
|
+
with open(log_path, "a", encoding="utf-8") as lf:
|
|
34
|
+
lf.write(f"=== {method} {url} ===\n{payload_str}\n\n")
|
|
35
|
+
else:
|
|
36
|
+
log.debug("%s %s", method, url)
|
|
37
|
+
resp = requests.request(method, url, headers=self.headers, timeout=30, **kwargs)
|
|
38
|
+
log.debug("Response %s: %s", resp.status_code, resp.text)
|
|
39
|
+
resp.raise_for_status()
|
|
40
|
+
return resp
|
|
41
|
+
|
|
42
|
+
def create_portfolio(self, config: UploadConfig) -> str:
|
|
43
|
+
"""POST /portfolios → portfolio_id 반환."""
|
|
44
|
+
items = [
|
|
45
|
+
{
|
|
46
|
+
"ticker": str(item["ticker"]),
|
|
47
|
+
"type": str(item.get("type", "stock")),
|
|
48
|
+
"quantity": float(item.get("quantity", 0)),
|
|
49
|
+
"ratio": float(item.get("ratio", 0)),
|
|
50
|
+
"price": float(item.get("price", 0)),
|
|
51
|
+
"sector": str(item.get("sector", "N/A")),
|
|
52
|
+
"industry": str(item.get("industry", "N/A")),
|
|
53
|
+
}
|
|
54
|
+
for item in config.items
|
|
55
|
+
]
|
|
56
|
+
body = {
|
|
57
|
+
"name": config.name,
|
|
58
|
+
"description": config.description,
|
|
59
|
+
"type": config.portfolio_type,
|
|
60
|
+
"currency": config.currency,
|
|
61
|
+
"budget": float(config.budget),
|
|
62
|
+
"target_return": config.target_return,
|
|
63
|
+
"start_date": config.start_date,
|
|
64
|
+
"target_date": config.target_date,
|
|
65
|
+
"items": items,
|
|
66
|
+
}
|
|
67
|
+
if config.settings:
|
|
68
|
+
body["settings"] = config.settings
|
|
69
|
+
resp = self._request("POST", "/portfolios", json=body)
|
|
70
|
+
return resp.json()["portfolio_id"]
|
|
71
|
+
|
|
72
|
+
def get_portfolios(self) -> list[dict]:
|
|
73
|
+
"""GET /portfolios → return caller's own portfolios."""
|
|
74
|
+
resp = self._request("GET", "/portfolios")
|
|
75
|
+
return resp.json()
|
|
76
|
+
|
|
77
|
+
def search_portfolios(
|
|
78
|
+
self,
|
|
79
|
+
search: str | None = None,
|
|
80
|
+
country: str | None = None,
|
|
81
|
+
sort_by: str | None = None,
|
|
82
|
+
last_item: str | None = None,
|
|
83
|
+
) -> dict:
|
|
84
|
+
"""GET /portfolios with search params."""
|
|
85
|
+
params = {k: v for k, v in {
|
|
86
|
+
"search": search, "country": country,
|
|
87
|
+
"sort_by": sort_by, "last_item": last_item,
|
|
88
|
+
}.items() if v is not None}
|
|
89
|
+
resp = self._request("GET", "/portfolios", params=params)
|
|
90
|
+
return resp.json()
|
|
91
|
+
|
|
92
|
+
def delete_portfolio(self, portfolio_id: str) -> None:
|
|
93
|
+
"""DELETE /portfolios/{portfolio_id}."""
|
|
94
|
+
self._request("DELETE", f"/portfolios/{portfolio_id}")
|
|
95
|
+
|
|
96
|
+
def get_nav_history(self, portfolio_id: str) -> dict:
|
|
97
|
+
"""GET /portfolios/{portfolio_id}/nav-history."""
|
|
98
|
+
resp = self._request("GET", f"/portfolios/{portfolio_id}/nav-history")
|
|
99
|
+
return resp.json()
|
|
100
|
+
|
|
101
|
+
def get_metrics_history(
|
|
102
|
+
self,
|
|
103
|
+
portfolio_id: str,
|
|
104
|
+
metrics: str = "twr",
|
|
105
|
+
from_t: int | None = None,
|
|
106
|
+
to_t: int | None = None,
|
|
107
|
+
) -> dict:
|
|
108
|
+
"""GET /portfolios/{portfolio_id}/metrics-history."""
|
|
109
|
+
params: dict = {"metrics": metrics}
|
|
110
|
+
if from_t is not None:
|
|
111
|
+
params["from_t"] = str(from_t)
|
|
112
|
+
if to_t is not None:
|
|
113
|
+
params["to_t"] = str(to_t)
|
|
114
|
+
resp = self._request(
|
|
115
|
+
"GET", f"/portfolios/{portfolio_id}/metrics-history",
|
|
116
|
+
params=params)
|
|
117
|
+
return resp.json()
|
|
118
|
+
|
|
119
|
+
def send_order(self, portfolio_id: str, order_group: OrderGroup, portfolio_currency: str) -> dict:
|
|
120
|
+
"""POST /orders → 주문 그룹 하나 전송."""
|
|
121
|
+
body = {
|
|
122
|
+
"portfolio_id": portfolio_id,
|
|
123
|
+
"currency": portfolio_currency,
|
|
124
|
+
"payment_currency": order_group.currency,
|
|
125
|
+
"items": order_group.items,
|
|
126
|
+
}
|
|
127
|
+
if order_group.memo:
|
|
128
|
+
body["memo"] = order_group.memo
|
|
129
|
+
if order_group.exchange_rate:
|
|
130
|
+
body["custom_exchange_rate"] = order_group.exchange_rate
|
|
131
|
+
body["is_custom_exchange_rate"] = True
|
|
132
|
+
if order_group.cash_deposits:
|
|
133
|
+
body["cash_deposits"] = [
|
|
134
|
+
{k: v for k, v in {
|
|
135
|
+
"type": d.type,
|
|
136
|
+
"amount": d.amount,
|
|
137
|
+
"currency": d.currency,
|
|
138
|
+
"ticker": d.ticker,
|
|
139
|
+
"timestamp": d.timestamp,
|
|
140
|
+
}.items() if v is not None}
|
|
141
|
+
for d in order_group.cash_deposits
|
|
142
|
+
]
|
|
143
|
+
resp = self._request("POST", "/orders", json=body)
|
|
144
|
+
return resp.json() if resp.text else {}
|
insighta_sdk/models.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Data models for Insighta SDK."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone, timedelta
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
WORKSPACES_DIR = "workspaces"
|
|
12
|
+
JST = timezone(timedelta(hours=9))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Credentials:
|
|
17
|
+
api_key: str
|
|
18
|
+
endpoint: str
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def masked_key(self) -> str:
|
|
22
|
+
if len(self.api_key) <= 8:
|
|
23
|
+
return "****"
|
|
24
|
+
return self.api_key[:4] + "****" + self.api_key[-4:]
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_file(cls, path: str) -> "Credentials":
|
|
28
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
29
|
+
data = yaml.safe_load(f)
|
|
30
|
+
return cls(api_key=data["api_key"], endpoint=data["endpoint"].rstrip("/"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class UploadConfig:
|
|
35
|
+
name: str
|
|
36
|
+
description: str
|
|
37
|
+
portfolio_type: str
|
|
38
|
+
currency: str
|
|
39
|
+
budget: Decimal
|
|
40
|
+
balance: Decimal
|
|
41
|
+
order_file: str
|
|
42
|
+
target_return: float = 0.0
|
|
43
|
+
start_date: str = ""
|
|
44
|
+
target_date: str = ""
|
|
45
|
+
items: list = field(default_factory=list)
|
|
46
|
+
cash_deposits_file: str | None = None
|
|
47
|
+
memo_file: str | None = None
|
|
48
|
+
settings: dict | None = None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_file(cls, path: str) -> "UploadConfig":
|
|
52
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
53
|
+
data = yaml.safe_load(f)
|
|
54
|
+
p = data["portfolio"]
|
|
55
|
+
files = data.get("files", {})
|
|
56
|
+
return cls(
|
|
57
|
+
name=p["name"],
|
|
58
|
+
description=p.get("description", ""),
|
|
59
|
+
portfolio_type=p["type"],
|
|
60
|
+
currency=p["currency"],
|
|
61
|
+
budget=Decimal(str(p["budget"])),
|
|
62
|
+
balance=Decimal(str(p["budget"])),
|
|
63
|
+
target_return=float(p.get("target_return", 0)),
|
|
64
|
+
start_date=p.get("start_date", ""),
|
|
65
|
+
target_date=p.get("target_date", ""),
|
|
66
|
+
items=p.get("items", []),
|
|
67
|
+
order_file=files["order"],
|
|
68
|
+
cash_deposits_file=files.get("cash_deposits"),
|
|
69
|
+
memo_file=files.get("memo"),
|
|
70
|
+
settings=p.get("settings"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class CashDeposit:
|
|
76
|
+
type: str # budget | dividend
|
|
77
|
+
amount: float
|
|
78
|
+
currency: str | None = None
|
|
79
|
+
ticker: str | None = None
|
|
80
|
+
timestamp: int | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class OrderGroup:
|
|
85
|
+
group_id: str
|
|
86
|
+
currency: str
|
|
87
|
+
items: list = field(default_factory=list)
|
|
88
|
+
cash_deposits: list[CashDeposit] = field(default_factory=list)
|
|
89
|
+
exchange_rate: float | None = None
|
|
90
|
+
memo: str = ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Trade:
|
|
95
|
+
dt: str # ISO 8601 JST
|
|
96
|
+
ticker: str
|
|
97
|
+
qty: int
|
|
98
|
+
acct: str
|
|
99
|
+
price: Decimal
|
|
100
|
+
avg: Decimal
|
|
101
|
+
cur: str # 決済通貨 (JPY or USD)
|
|
102
|
+
base: str = "USD" # 銘柄の基準通貨
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class Holding:
|
|
107
|
+
ticker: str
|
|
108
|
+
acct: str
|
|
109
|
+
qty: int
|
|
110
|
+
cost: Decimal = Decimal("0")
|
|
111
|
+
price: Decimal = Decimal("0")
|
|
112
|
+
pnl: Decimal = Decimal("0")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class Deposit:
|
|
117
|
+
dt: str # ISO 8601 JST or raw datetime
|
|
118
|
+
amount: Decimal
|
|
119
|
+
cur: str
|
|
120
|
+
type: str = "budget" # budget | dividend
|
|
121
|
+
ticker: str = ""
|
|
122
|
+
rate: Decimal | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class RateEntry:
|
|
127
|
+
start: str
|
|
128
|
+
end: str
|
|
129
|
+
pair: str
|
|
130
|
+
rate: Decimal
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class Dirs:
|
|
135
|
+
"""作業ディレクトリ設定。--work オプションで切り替え可能。"""
|
|
136
|
+
work: str = ""
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_work(cls, work: str = "") -> "Dirs":
|
|
140
|
+
return cls(work=work)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def _base(self) -> str:
|
|
144
|
+
return os.path.join(WORKSPACES_DIR, self.work) if self.work else ""
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def input(self) -> str:
|
|
148
|
+
return os.path.join(self._base, "input") if self._base else "input"
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def output(self) -> str:
|
|
152
|
+
return os.path.join(self._base, "output") if self._base else "output"
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def history(self) -> str:
|
|
156
|
+
return os.path.join(self.input, "history")
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def summary(self) -> str:
|
|
160
|
+
return os.path.join(self.input, "summary")
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def seed(self) -> str:
|
|
164
|
+
return os.path.join(self.input, "seed")
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def deposit(self) -> str:
|
|
168
|
+
return os.path.join(self.input, "deposit")
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def exchange(self) -> str:
|
|
172
|
+
return os.path.join(self.input, "currency_exchange")
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def manual(self) -> str:
|
|
176
|
+
return os.path.join(self.input, "manual")
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def rate_csv(self) -> str:
|
|
180
|
+
return os.path.join(self.input, "rate.csv")
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def ratio_csv(self) -> str:
|
|
184
|
+
return os.path.join(self.input, "ratio.csv")
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def history_csv(self) -> str:
|
|
188
|
+
return os.path.join(self.output, "history.csv")
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def order_csv(self) -> str:
|
|
192
|
+
return os.path.join(self.output, "order.csv")
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def upload_yaml(self) -> str:
|
|
196
|
+
return os.path.join(self.output, "upload.yaml")
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def memo_csv(self) -> str:
|
|
200
|
+
return os.path.join(self.output, "memo.csv")
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def cash_deposits_csv(self) -> str:
|
|
204
|
+
return os.path.join(self.output, "cash_deposits.csv")
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def request_payload_log(self) -> str:
|
|
208
|
+
return os.path.join(self.output, "request_payload.log")
|
|
209
|
+
|
|
210
|
+
def ensure_output(self):
|
|
211
|
+
"""output ディレクトリを作成する。"""
|
|
212
|
+
os.makedirs(self.output, exist_ok=True)
|
insighta_sdk/utils.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Utility functions for Insighta SDK."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .models import CashDeposit, OrderGroup, RateEntry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_timestamp(val: str) -> int | None:
|
|
14
|
+
if not val:
|
|
15
|
+
return None
|
|
16
|
+
try:
|
|
17
|
+
return int(val)
|
|
18
|
+
except ValueError:
|
|
19
|
+
pass
|
|
20
|
+
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
|
|
21
|
+
try:
|
|
22
|
+
return int(datetime.strptime(val, fmt).replace(tzinfo=timezone.utc).timestamp() * 1000)
|
|
23
|
+
except ValueError:
|
|
24
|
+
continue
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_order_groups(filepath: str) -> list[OrderGroup]:
|
|
29
|
+
"""order.csv를 읽어서 group_dt별로 묶어 반환."""
|
|
30
|
+
groups: OrderedDict[str, OrderGroup] = OrderedDict()
|
|
31
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
32
|
+
for row in csv.DictReader(f):
|
|
33
|
+
gdt = row["group_dt"]
|
|
34
|
+
rate_val = row.get("rate", "").strip() if row.get("rate") else ""
|
|
35
|
+
if gdt not in groups:
|
|
36
|
+
groups[gdt] = OrderGroup(
|
|
37
|
+
group_id=gdt,
|
|
38
|
+
currency=row.get("settle_currency", row["currency"]),
|
|
39
|
+
exchange_rate=float(rate_val) if rate_val else None,
|
|
40
|
+
)
|
|
41
|
+
groups[gdt].items.append({
|
|
42
|
+
"id": row["ticker"],
|
|
43
|
+
"ticker": row["ticker"],
|
|
44
|
+
"quantity": float(row["quantity"]),
|
|
45
|
+
"price": float(row["price"]),
|
|
46
|
+
"currency": row["currency"],
|
|
47
|
+
"price_type": row["price_type"],
|
|
48
|
+
"timestamp": _parse_timestamp(row.get("timestamp", "")),
|
|
49
|
+
})
|
|
50
|
+
return list(groups.values())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_cash_deposits(filepath: str) -> dict[str, list[CashDeposit]]:
|
|
54
|
+
"""cash_deposits.csv를 읽어서 group_dt별로 묶어 반환."""
|
|
55
|
+
groups: dict[str, list[CashDeposit]] = {}
|
|
56
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
57
|
+
for row in csv.DictReader(f):
|
|
58
|
+
gdt = row["group_dt"]
|
|
59
|
+
groups.setdefault(gdt, []).append(CashDeposit(
|
|
60
|
+
type=row["type"],
|
|
61
|
+
amount=float(row["amount"]),
|
|
62
|
+
currency=row.get("currency") or None,
|
|
63
|
+
ticker=row.get("ticker") or None,
|
|
64
|
+
timestamp=_parse_timestamp(row.get("timestamp", "")),
|
|
65
|
+
))
|
|
66
|
+
return groups
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def merge_and_sort_groups(
|
|
70
|
+
orders: list[OrderGroup],
|
|
71
|
+
deposits_by_gdt: dict[str, list[CashDeposit]],
|
|
72
|
+
memos: dict[str, str],
|
|
73
|
+
) -> list[OrderGroup]:
|
|
74
|
+
"""order + deposit을 group_dt 기준으로 머지하고 시간순 정렬."""
|
|
75
|
+
existing_gdts = {g.group_id for g in orders}
|
|
76
|
+
for gdt, deps in deposits_by_gdt.items():
|
|
77
|
+
if gdt not in existing_gdts:
|
|
78
|
+
cur = deps[0].currency or "USD"
|
|
79
|
+
orders.append(OrderGroup(group_id=gdt, currency=cur))
|
|
80
|
+
deps_map = dict(deposits_by_gdt)
|
|
81
|
+
for g in orders:
|
|
82
|
+
if g.group_id in deps_map:
|
|
83
|
+
g.cash_deposits = deps_map[g.group_id]
|
|
84
|
+
|
|
85
|
+
def _sort_key(g: OrderGroup):
|
|
86
|
+
ts = _parse_timestamp(g.group_id)
|
|
87
|
+
return ts if ts is not None else float("inf")
|
|
88
|
+
orders.sort(key=_sort_key)
|
|
89
|
+
for i, g in enumerate(orders, 1):
|
|
90
|
+
g.group_id = str(i)
|
|
91
|
+
for g in orders:
|
|
92
|
+
if g.group_id in memos:
|
|
93
|
+
g.memo = memos[g.group_id]
|
|
94
|
+
return orders
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def fetch_ticker_info(tickers: list[str]) -> dict[str, dict]:
|
|
98
|
+
"""Insighta /tickers/info API로 sector/industry/type 조회."""
|
|
99
|
+
if not tickers:
|
|
100
|
+
return {}
|
|
101
|
+
resp = requests.get(
|
|
102
|
+
"https://api.insighta.cloud/tickers/info",
|
|
103
|
+
params={"tickers": ",".join(tickers), "conditions": "sector,industry,type"},
|
|
104
|
+
timeout=10,
|
|
105
|
+
)
|
|
106
|
+
resp.raise_for_status()
|
|
107
|
+
return resp.json()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load_rate_file(filepath: str) -> list[RateEntry]:
|
|
111
|
+
"""為替レートCSVを読み込む。
|
|
112
|
+
|
|
113
|
+
CSV format:
|
|
114
|
+
from,to,pair,rate
|
|
115
|
+
2024/01/01,2024/12/31,USD/JPY,155.50
|
|
116
|
+
"""
|
|
117
|
+
entries: list[RateEntry] = []
|
|
118
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
119
|
+
for row in csv.DictReader(f):
|
|
120
|
+
entries.append(RateEntry(
|
|
121
|
+
start=row["from"].strip(),
|
|
122
|
+
end=row["to"].strip(),
|
|
123
|
+
pair=row["pair"].strip(),
|
|
124
|
+
rate=Decimal(row["rate"].strip()),
|
|
125
|
+
))
|
|
126
|
+
return entries
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _normalize_dt(val: str) -> str:
|
|
130
|
+
return val if " " in val else f"{val} 00:00"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _normalize_dt_end(val: str) -> str:
|
|
134
|
+
return val if " " in val else f"{val} 23:59"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def lookup_rate(entries: list[RateEntry], dt: str, cur: str, base: str) -> Decimal | None:
|
|
138
|
+
"""決済通貨と基準通貨が異なる場合のみ該当期間のレートを返す。"""
|
|
139
|
+
if cur == base:
|
|
140
|
+
return None
|
|
141
|
+
trade_dt = dt[:16].replace("-", "/").replace("T", " ") if dt else ""
|
|
142
|
+
pair = f"{base}/{cur}"
|
|
143
|
+
for e in entries:
|
|
144
|
+
start = _normalize_dt(e.start)
|
|
145
|
+
end = _normalize_dt_end(e.end)
|
|
146
|
+
if e.pair == pair and start <= trade_dt <= end:
|
|
147
|
+
return e.rate
|
|
148
|
+
return None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: insighta-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Insighta Cloud SDK - API client and data models
|
|
5
|
+
Project-URL: Homepage, https://insighta.cloud
|
|
6
|
+
Author: insighta cloud Inc.
|
|
7
|
+
License-Expression: CC-BY-NC-4.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: api-client,insighta,investment,portfolio,sdk
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: requests>=2.28.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# insighta-sdk
|
|
25
|
+
|
|
26
|
+
Insighta Cloud SDK — API client and data models for portfolio management.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install insighta-sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from insighta_sdk import Credentials, InsightaClient
|
|
38
|
+
|
|
39
|
+
creds = Credentials.from_file("credentials.yaml")
|
|
40
|
+
client = InsightaClient(creds)
|
|
41
|
+
|
|
42
|
+
# List portfolios
|
|
43
|
+
portfolios = client.get_portfolios()
|
|
44
|
+
|
|
45
|
+
# Create a portfolio
|
|
46
|
+
from insighta_sdk import UploadConfig
|
|
47
|
+
config = UploadConfig.from_file("upload.yaml")
|
|
48
|
+
portfolio_id = client.create_portfolio(config)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- **API Client** — Full coverage of Insighta OpenAPI (portfolios, orders, metrics)
|
|
54
|
+
- **Data Models** — `Trade`, `Holding`, `Deposit`, `OrderGroup`, `CashDeposit`, `RateEntry`
|
|
55
|
+
- **Utilities** — Rate lookup, order grouping, deposit merging
|
|
56
|
+
- **Workspace Management** — `Dirs` class for consistent file path resolution
|
|
57
|
+
|
|
58
|
+
## API Reference
|
|
59
|
+
|
|
60
|
+
### Client
|
|
61
|
+
|
|
62
|
+
| Method | Description |
|
|
63
|
+
|--------|-------------|
|
|
64
|
+
| `create_portfolio(config)` | Create a new portfolio |
|
|
65
|
+
| `get_portfolios()` | List own portfolios |
|
|
66
|
+
| `search_portfolios(...)` | Search public portfolios |
|
|
67
|
+
| `delete_portfolio(id)` | Delete a portfolio |
|
|
68
|
+
| `send_order(portfolio_id, group, currency)` | Submit an order group |
|
|
69
|
+
| `get_nav_history(id)` | Get NAV history |
|
|
70
|
+
| `get_metrics_history(id, ...)` | Get metrics (TWR, etc.) |
|
|
71
|
+
|
|
72
|
+
### Utilities
|
|
73
|
+
|
|
74
|
+
| Function | Description |
|
|
75
|
+
|----------|-------------|
|
|
76
|
+
| `load_order_groups(path)` | Parse order.csv into OrderGroup list |
|
|
77
|
+
| `load_cash_deposits(path)` | Parse cash_deposits.csv |
|
|
78
|
+
| `merge_and_sort_groups(orders, deposits, memos)` | Merge and sequence order groups |
|
|
79
|
+
| `load_rate_file(path)` | Load exchange rate CSV |
|
|
80
|
+
| `lookup_rate(entries, dt, cur, base)` | Find applicable rate for a trade |
|
|
81
|
+
| `fetch_ticker_info(tickers)` | Query ticker metadata from API |
|
|
82
|
+
|
|
83
|
+
## API Endpoints
|
|
84
|
+
|
|
85
|
+
| Environment | Base URL |
|
|
86
|
+
|-------------|----------|
|
|
87
|
+
| Production | `https://openapi.insighta.cloud` |
|
|
88
|
+
| Development | `https://dev.openapi.insighta.cloud` |
|
|
89
|
+
|
|
90
|
+
OpenAPI spec: [`insighta-app/openapi-docs/`](https://github.com/insighta-cloud/insighta/tree/main/insighta-app/openapi-docs)
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install -e .
|
|
96
|
+
pytest
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
CC-BY-NC-4.0 — See [LICENSE](LICENSE)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
insighta_sdk/__init__.py,sha256=O-Mb_GJmDk0gyP6gFsM6S0_c1kS5AkPCJluYdwjMsy0,733
|
|
2
|
+
insighta_sdk/client.py,sha256=juCH5LDHkdUYGh-KBuX1nRPnsFwB9o5OuiHoO11loos,5410
|
|
3
|
+
insighta_sdk/models.py,sha256=7zLmqyDiJROf2pqrXFK3cWXUYBxlnFIajImcjeBBIfo,5308
|
|
4
|
+
insighta_sdk/utils.py,sha256=QaQU1K962Yo3vCbXf4A1Rx9_0q12X_1AXaXtL85jJe0,5015
|
|
5
|
+
insighta_sdk-0.1.0.dist-info/METADATA,sha256=HuulJU140OjxAaDztFmesd-51rEXtt08gGK73B2bL7g,3142
|
|
6
|
+
insighta_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
insighta_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=WPu_gaVv4rw7ucpTiIFHcubHh0VS_s5PI6RQJAz98l0,712
|
|
8
|
+
insighta_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Creative Commons Attribution-NonCommercial 4.0 International
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Insighta Cloud Inc.
|
|
4
|
+
|
|
5
|
+
You are free to:
|
|
6
|
+
|
|
7
|
+
Share — copy and redistribute the material in any medium or format
|
|
8
|
+
Adapt — remix, transform, and build upon the material
|
|
9
|
+
|
|
10
|
+
Under the following terms:
|
|
11
|
+
|
|
12
|
+
Attribution — You must give appropriate credit, provide a link to the
|
|
13
|
+
license, and indicate if changes were made.
|
|
14
|
+
|
|
15
|
+
NonCommercial — You may not use the material for commercial purposes.
|
|
16
|
+
|
|
17
|
+
No additional restrictions — You may not apply legal terms or technological
|
|
18
|
+
measures that legally restrict others from doing anything the license permits.
|
|
19
|
+
|
|
20
|
+
Full license text: https://creativecommons.org/licenses/by-nc/4.0/legalcode
|