Reduce output duplication and add install scripts

This commit is contained in:
2026-06-09 05:44:42 +00:00
parent 9f29755f0a
commit 70e3dd3cc5
10 changed files with 149 additions and 47 deletions
+50 -16
View File
@@ -4,6 +4,40 @@
이 프로그램은 투자 판단을 대신하지 않습니다. `order buy`, `order sell`은 실전 주문을 전송할 수 있으므로 실행 전 입력값과 계좌 상태를 반드시 확인해야 합니다.
## Install
로컬 repo에서 release 바이너리를 빌드해 `~/.local/bin/openstock`에 설치합니다.
```bash
./scripts/install.sh
```
원라인 설치:
```bash
curl -fsSL https://git.hananakick.cc/Autotrade/openstock/raw/branch/main/scripts/install.sh | sh
```
원라인 설치 스크립트는 현재 디렉터리에 openstock source tree가 있으면 그대로 빌드하고, 없으면 main branch archive를 임시 디렉터리에 내려받아 빌드합니다. 설치 위치는 `OPENSTOCK_INSTALL_DIR`, 바이너리 이름은 `OPENSTOCK_BIN_NAME`, archive URL은 `OPENSTOCK_ARCHIVE_URL`로 바꿀 수 있습니다.
```bash
OPENSTOCK_INSTALL_DIR=/usr/local/bin ./scripts/install.sh
```
삭제:
```bash
./scripts/uninstall.sh
```
원라인 삭제:
```bash
curl -fsSL https://git.hananakick.cc/Autotrade/openstock/raw/branch/main/scripts/uninstall.sh | sh
```
삭제 스크립트는 설치된 바이너리만 제거하고 `~/.config/openstock`의 설정/캐시는 보존합니다.
## Architecture
| Layer | Path | Role |
@@ -28,7 +62,7 @@
"value": "실제 값"
}
],
"raw": "원본 또는 상세 데이터"
"raw": "직접 API 호출 원본 또는 null"
}
```
@@ -58,7 +92,7 @@
| `fields[].name` | string | 출력 필드 이름입니다. |
| `fields[].description` | string | 해당 필드의 의미입니다. CLI에서 key 의미를 해석할 때 이 값을 우선 사용합니다. |
| `fields[].value` | any | 실제 값입니다. 원본 API 숫자 문자열은 정밀도 보존을 위해 문자열일 수 있습니다. |
| `raw` | any/null | 원본 API 응답 또는 명령별 상세 payload입니다. 재처리/디버깅/AI 분석용입니다. |
| `raw` | any/null | 중복 출력을 줄이기 위해 typed command는 기본적으로 `null`입니다. `api call`처럼 원본 응답 자체가 목적인 명령만 원본 응답을 넣습니다. |
오류 출력의 `fields`는 다음 고정 키를 사용합니다.
@@ -213,7 +247,7 @@
| `price` | object | KIS 현재가/호가/시세/투자주의 관련 값입니다. |
| `company` | object | KIS 종목 및 기업 기본 정보입니다. |
`price``company`는 KIS 원본 field name을 보존합니다. 자세한 원본은 `raw.price`, `raw.company`를 봅니다.
`price``company`는 KIS field name을 보존한 조회 결과입니다.
#### `market history`
@@ -332,7 +366,7 @@
| `balance` | array | KIS 잔고/평가금/예수금 요약 목록입니다. |
| `holdings` | array | 보유종목 상세 목록입니다. |
`balance``holdings`는 KIS 원본 field name을 보존합니다. 전체 원본은 `raw`를 봅니다.
`balance``holdings`는 KIS field name을 보존한 조회 결과입니다.
#### `order buy`, `order sell`
@@ -498,7 +532,7 @@ KIS endpoint를 직접 GET 호출합니다. `tr_id` 파라미터는 HTTP header
| External IO | KIS endpoint GET |
| File IO | `~/.config/openstock/.env` read |
| Output fields | `broker`, `endpoint`, `params`, `request_semantics`, `response`, `response_semantics` |
| Raw | parsed KIS response |
| Raw | parsed KIS response. `api call`은 직접 호출 명령이므로 원본 응답을 `raw`에도 유지합니다. |
| Side effect | endpoint에 따라 다름. typed command 사용 권장 |
### `openstock search <query>`
@@ -511,7 +545,7 @@ Naver 모바일 주식 검색 API로 종목 후보를 검색합니다.
| External IO | `GET https://m.stock.naver.com/front-api/search/autoComplete` |
| File IO | 없음 |
| Output fields | `provider`, `query`, `stocks` |
| Raw | Naver 원본 JSON |
| Raw | `null` |
| Side effect | 없음 |
`stocks` 항목은 가능한 경우 `code`, `name`, `market`, `market_code`, `nation_code`, `category`, `reuters_code`, `url`을 포함합니다.
@@ -526,7 +560,7 @@ KIND 상장법인목록을 받아 stock universe cache를 갱신합니다.
| External IO | `GET https://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13` |
| File write | `~/.config/openstock/cache/universe/kind/latest.json`, `meta.json`, `YYYY-MM-DD.json` |
| Output fields | `source`, `cache_date`, `refreshed_at`, `refreshed`, `stock_count`, `counts_by_market` |
| Raw | universe metadata |
| Raw | `null` |
| Side effect | writes/updates local cache; prunes old snapshots |
### `openstock universe status`
@@ -581,7 +615,7 @@ KIS 현재가와 종목/기업 기본 정보를 조회합니다.
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET` |
| External IO | KIS `GET /uapi/domestic-stock/v1/quotations/inquire-price`, TR ID `FHKST01010100`; KIS `GET /uapi/domestic-stock/v1/quotations/search-stock-info`, TR ID `CTPF1002R` |
| Output fields | `broker`, `symbol`, `price`, `company` |
| Raw | KIS price/company 원본 응답 |
| Raw | `null` |
| Side effect | 없음 |
### `openstock market history <symbol> --from YYYYMMDD --to YYYYMMDD [--period D|W|M|Y] [--raw-price]`
@@ -594,7 +628,7 @@ KIS 기간별 OHLCV를 조회합니다.
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET` |
| External IO | `GET /uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice`, TR ID `FHKST03010100` |
| Output fields | `broker`, `symbol`, `period`, `date_range`, `adjusted`, `count`, `candles`, `summary` |
| Raw | normalized payload plus KIS raw response |
| Raw | `null` |
| Side effect | 없음 |
`candles`는 날짜 오름차순이며 각 항목은 `date`, `open`, `high`, `low`, `close`, `volume`, `trading_value`, `change`, `change_sign`, `change_rate`를 포함합니다. 숫자는 원본 정밀도 보존을 위해 문자열로 반환합니다.
@@ -644,7 +678,7 @@ KRX 종목코드를 OpenDART 고유번호로 변환합니다.
| File read/write | `symbol` 사용 시 corp code cache read; cache refresh 가능 |
| External IO | `GET https://opendart.fss.or.kr/api/list.json` |
| Output fields | `corp_code`, `from`, `to`, `page`, `resolved`, `disclosures` |
| Raw | OpenDART list response |
| Raw | `null` |
| Side effect | cache refresh 가능 |
`disclosures` 항목은 OpenDART가 반환하는 `corp_cls`, `corp_code`, `corp_name`, `stock_code`, `rcept_no`, `report_nm`, `rcept_dt`, `flr_nm`, `rm` 등을 포함합니다.
@@ -660,7 +694,7 @@ KRX 종목코드를 OpenDART 고유번호로 변환합니다.
| External IO | `GET https://opendart.fss.or.kr/api/document.xml` |
| File read/write | `~/.config/openstock/cache/opendart/documents/{rcept_no}.zip` |
| Output fields | `rcept_no`, `source`, `viewer_url`, `cached`, `zip_path`, `zip_bytes`, `files`, `text`, `text_chars`, `truncated` |
| Raw | same document object |
| Raw | `null` |
| Side effect | writes ZIP cache; prunes old document ZIP files |
`text`는 XML tag, style, script를 제거한 본문입니다. `--max-chars`를 넘으면 앞부분만 출력하고 `truncated=true`를 반환합니다.
@@ -676,7 +710,7 @@ KRX 종목코드를 OpenDART 고유번호로 변환합니다.
| File read/write | corp code cache, document ZIP cache |
| External IO | OpenDART `list.json`; selected document cache miss or `--force`이면 `document.xml` |
| Output fields | `symbol`, `resolved`, `date_range`, `selected_index`, `selected_disclosure`, `disclosures`, `document` |
| Raw | query, resolved corp, selected disclosure, list response, document object |
| Raw | `null` |
| Side effect | cache refresh/write 가능 |
### `openstock account status`
@@ -689,7 +723,7 @@ KIS 계좌 잔고와 보유종목을 조회합니다.
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCOUNT` |
| External IO | KIS `GET /uapi/domestic-stock/v1/trading/inquire-balance` |
| Output fields | `broker`, `account`, `balance`, `holdings` |
| Raw | KIS balance response |
| Raw | `null` |
| Side effect | 없음 |
### `openstock order buy <symbol> --qty QTY (--price PRICE|--market)`
@@ -702,7 +736,7 @@ KIS 계좌 잔고와 보유종목을 조회합니다.
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCOUNT` |
| External IO | KIS `POST /uapi/domestic-stock/v1/trading/order-cash`, TR ID `TTTC0802U` |
| Output fields | `broker`, `side`, `symbol`, `qty`, `order_type`, `price`, `order` |
| Raw | KIS order response |
| Raw | `null` |
| Side effect | financial_order: 실전 매수 주문 |
### `openstock order sell <symbol> --qty QTY (--price PRICE|--market)`
@@ -715,7 +749,7 @@ KIS 계좌 잔고와 보유종목을 조회합니다.
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCOUNT` |
| External IO | KIS `POST /uapi/domestic-stock/v1/trading/order-cash`, TR ID `TTTC0801U` |
| Output fields | `broker`, `side`, `symbol`, `qty`, `order_type`, `price`, `order` |
| Raw | KIS order response |
| Raw | `null` |
| Side effect | financial_order: 실전 매도 주문 |
### `openstock order status [order_no] [--from YYYYMMDD] [--to YYYYMMDD]`
@@ -728,7 +762,7 @@ KIS 계좌 잔고와 보유종목을 조회합니다.
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCOUNT` |
| External IO | KIS `GET /uapi/domestic-stock/v1/trading/inquire-daily-ccld` |
| Output fields | `broker`, `account`, `order_no`, `orders`, `summary` |
| Raw | KIS order status response |
| Raw | `null` |
| Side effect | 없음 |
### `openstock cache status`
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env sh
set -eu
INSTALL_DIR="${OPENSTOCK_INSTALL_DIR:-$HOME/.local/bin}"
BIN_NAME="${OPENSTOCK_BIN_NAME:-openstock}"
ARCHIVE_URL="${OPENSTOCK_ARCHIVE_URL:-https://git.hananakick.cc/Autotrade/openstock/archive/main.tar.gz}"
SOURCE_DIR=""
TMP_DIR=""
cleanup() {
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
rm -rf "$TMP_DIR"
fi
}
trap cleanup EXIT
is_openstock_source() {
[ -f "$1/Cargo.toml" ] && grep -q 'name = "openstock"' "$1/Cargo.toml"
}
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd || pwd)"
if is_openstock_source "$PWD"; then
SOURCE_DIR="$PWD"
elif is_openstock_source "$SCRIPT_DIR/.."; then
SOURCE_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
else
if ! command -v curl >/dev/null 2>&1; then
echo "curl is required when installing outside an openstock source tree" >&2
exit 1
fi
if ! command -v tar >/dev/null 2>&1; then
echo "tar is required when installing outside an openstock source tree" >&2
exit 1
fi
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openstock-install.XXXXXX")"
curl -fsSL "$ARCHIVE_URL" | tar -xz -C "$TMP_DIR"
SOURCE_DIR="$(find "$TMP_DIR" -mindepth 1 -maxdepth 2 -name Cargo.toml -print | head -n 1 | xargs dirname)"
if ! is_openstock_source "$SOURCE_DIR"; then
echo "failed to locate openstock source in downloaded archive" >&2
exit 1
fi
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "cargo is required to build openstock" >&2
exit 1
fi
mkdir -p "$INSTALL_DIR"
cd "$SOURCE_DIR"
cargo build --release
cp "$SOURCE_DIR/target/release/openstock" "$INSTALL_DIR/$BIN_NAME"
chmod 755 "$INSTALL_DIR/$BIN_NAME"
echo "installed: $INSTALL_DIR/$BIN_NAME"
echo "config: ${OPENSTOCK_CONFIG_DIR:-$HOME/.config/openstock}"
case ":$PATH:" in
*":$INSTALL_DIR:"*) ;;
*)
echo "warning: $INSTALL_DIR is not in PATH" >&2
echo "add this to your shell profile: export PATH=\"$INSTALL_DIR:\$PATH\"" >&2
;;
esac
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env sh
set -eu
INSTALL_DIR="${OPENSTOCK_INSTALL_DIR:-$HOME/.local/bin}"
BIN_NAME="${OPENSTOCK_BIN_NAME:-openstock}"
TARGET="$INSTALL_DIR/$BIN_NAME"
if [ -e "$TARGET" ]; then
rm -f "$TARGET"
echo "removed: $TARGET"
else
echo "not installed: $TARGET"
fi
echo "config and cache are preserved at: ${OPENSTOCK_CONFIG_DIR:-$HOME/.config/openstock}"
echo "remove them manually only if you no longer need credentials or cached data."
+1 -1
View File
@@ -54,7 +54,7 @@ pub fn handle_account(sub: &AccountCommands) {
.unwrap_or_else(|| serde_json::json!([])),
),
],
value,
serde_json::Value::Null,
)
);
}
+2 -2
View File
@@ -52,7 +52,7 @@ pub fn handle_cache(sub: &CacheCommands) {
serde_json::json!(status.namespaces),
),
],
serde_json::json!(status),
serde_json::Value::Null,
)
),
Err(err) => eprintln!(
@@ -106,7 +106,7 @@ pub fn handle_cache(sub: &CacheCommands) {
serde_json::json!(reports),
),
],
serde_json::json!(reports),
serde_json::Value::Null,
)
);
}
+4 -10
View File
@@ -344,7 +344,7 @@ fn print_corp_snapshot(
serde_json::json!(snapshot.meta.listed_count),
),
],
serde_json::json!(snapshot.meta),
serde_json::Value::Null,
)
);
}
@@ -505,7 +505,7 @@ fn print_disclosures(
),
crate::core::output::field("disclosures", "조회된 공시 목록", list),
],
value,
serde_json::Value::Null,
)
);
}
@@ -568,7 +568,7 @@ fn print_document(value: document::DartDocument) {
serde_json::json!(value.truncated),
),
],
serde_json::json!(value),
serde_json::Value::Null,
)
);
}
@@ -630,13 +630,7 @@ fn print_show(
serde_json::json!(document),
),
],
serde_json::json!({
"query": query,
"resolved": resolved,
"selected_disclosure": selected,
"disclosures": disclosures_value,
"document": document,
}),
serde_json::Value::Null,
)
);
}
+2 -2
View File
@@ -100,7 +100,7 @@ fn handle_market_overview(symbol: &str) {
.unwrap_or_else(|| serde_json::json!({})),
),
],
value,
serde_json::Value::Null,
)
);
}
@@ -194,7 +194,7 @@ fn handle_market_history(command: &MarketHistoryCommand) {
.unwrap_or_else(|| serde_json::json!({})),
),
],
value,
serde_json::Value::Null,
)
);
}
+2 -2
View File
@@ -145,7 +145,7 @@ fn print_order_result(command: &str, description: &str, json: &str) {
.unwrap_or_else(|| serde_json::json!({})),
),
],
value,
serde_json::Value::Null,
)
);
}
@@ -199,7 +199,7 @@ fn print_order_status_result(json: &str) {
.unwrap_or_else(|| serde_json::json!({})),
),
],
value,
serde_json::Value::Null,
)
);
}
+1 -1
View File
@@ -35,7 +35,7 @@ pub fn handle_search(query: &str) {
.unwrap_or_else(|| serde_json::json!([])),
),
],
value,
serde_json::Value::Null,
)
);
}
+4 -13
View File
@@ -171,10 +171,7 @@ pub fn handle_universe(sub: &UniverseCommands) {
serde_json::json!(page),
),
],
serde_json::json!({
"meta": snapshot.meta,
"stocks": page,
}),
serde_json::Value::Null,
)
);
}
@@ -237,10 +234,7 @@ pub fn handle_universe(sub: &UniverseCommands) {
serde_json::json!(chunks),
),
],
serde_json::json!({
"meta": snapshot.meta,
"chunks": chunks,
}),
serde_json::Value::Null,
)
);
}
@@ -296,10 +290,7 @@ pub fn handle_universe(sub: &UniverseCommands) {
serde_json::json!(report.errors),
),
],
serde_json::json!({
"meta": snapshot.meta,
"report": report,
}),
serde_json::Value::Null,
)
),
Err(err) => eprintln!(
@@ -362,7 +353,7 @@ fn print_snapshot(command: &str, description: &str, snapshot: &kind::universe::U
serde_json::json!(snapshot.meta.counts_by_market),
),
],
serde_json::json!(snapshot.meta),
serde_json::Value::Null,
)
);
}