@@ -268,6 +268,19 @@ docker compose logs --tail=120 openstock-runner
|
|||||||
| `reuters_code` | string | Reuters style code입니다. 없으면 빈 문자열일 수 있습니다. |
|
| `reuters_code` | string | Reuters style code입니다. 없으면 빈 문자열일 수 있습니다. |
|
||||||
| `url` | string | Naver 모바일 종목 페이지 URL입니다. |
|
| `url` | string | Naver 모바일 종목 페이지 URL입니다. |
|
||||||
|
|
||||||
|
#### `score set`, `score get`, `score list`, `score delete`
|
||||||
|
|
||||||
|
| Field | Type | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `path` | string | 종목 평가 점수를 저장하는 파일 경로입니다. 기본값은 `~/.config/openstock/scores.json`입니다. |
|
||||||
|
| `symbol` | string | 점수를 매긴 종목코드 또는 종목 ID입니다. 영문자는 대문자로 정규화됩니다. |
|
||||||
|
| `score` | number | 종목 평가 점수입니다. 0은 최저, 100은 최고입니다. |
|
||||||
|
| `updated_at_unix` | number | 점수를 저장하거나 갱신한 Unix timestamp(초)입니다. |
|
||||||
|
| `count` | number | `score list`가 반환한 저장 점수 개수입니다. |
|
||||||
|
| `scores` | array | `score list`가 반환한 점수 목록입니다. 점수 내림차순, 종목 ID 오름차순으로 정렬됩니다. |
|
||||||
|
| `deleted` | boolean | `score delete`에서 저장된 점수가 실제로 삭제되었는지 여부입니다. |
|
||||||
|
| `removed` | object/null | `score delete`에서 삭제된 기존 점수 기록입니다. |
|
||||||
|
|
||||||
#### `universe sync`, `universe status`
|
#### `universe sync`, `universe status`
|
||||||
|
|
||||||
| Field | Type | Meaning |
|
| Field | Type | Meaning |
|
||||||
@@ -566,6 +579,7 @@ cp -R .openstock/* ~/.config/openstock/cache/
|
|||||||
|
|
||||||
| Path | Writer | Reader | Retention | Description |
|
| Path | Writer | Reader | Retention | Description |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `~/.config/openstock/scores.json` | `score set`, `score delete` | `score get`, `score list` | protected | 종목 ID별 0~100 평가 점수와 갱신시각입니다. `.env`와 같은 설정 디렉터리에 저장됩니다. |
|
||||||
| `~/.config/openstock/cache/universe/kind/latest.json` | `universe sync` | `universe status/list/chunks/validate` | protected | KIND 상장법인목록 최신 normalized stock list입니다. |
|
| `~/.config/openstock/cache/universe/kind/latest.json` | `universe sync` | `universe status/list/chunks/validate` | protected | KIND 상장법인목록 최신 normalized stock list입니다. |
|
||||||
| `~/.config/openstock/cache/universe/kind/meta.json` | `universe sync` | `universe status/*` | protected | universe cache metadata입니다. |
|
| `~/.config/openstock/cache/universe/kind/meta.json` | `universe sync` | `universe status/*` | protected | universe cache metadata입니다. |
|
||||||
| `~/.config/openstock/cache/universe/kind/YYYY-MM-DD.json` | `universe sync` | audit/manual use | max 7 files or 25MB | 날짜별 universe snapshot입니다. |
|
| `~/.config/openstock/cache/universe/kind/YYYY-MM-DD.json` | `universe sync` | audit/manual use | max 7 files or 25MB | 날짜별 universe snapshot입니다. |
|
||||||
@@ -661,6 +675,54 @@ Naver 모바일 주식 검색 API로 종목 후보를 검색합니다.
|
|||||||
|
|
||||||
`stocks` 항목은 가능한 경우 `code`, `name`, `market`, `market_code`, `nation_code`, `category`, `reuters_code`, `url`을 포함합니다.
|
`stocks` 항목은 가능한 경우 `code`, `name`, `market`, `market_code`, `nation_code`, `category`, `reuters_code`, `url`을 포함합니다.
|
||||||
|
|
||||||
|
### `openstock score set <symbol> <score>`
|
||||||
|
|
||||||
|
종목 ID에 0~100점 평가 점수를 저장하거나 갱신합니다.
|
||||||
|
|
||||||
|
| Direction | Data |
|
||||||
|
| --- | --- |
|
||||||
|
| Input | `symbol`, `score` |
|
||||||
|
| File read/write | `~/.config/openstock/scores.json` |
|
||||||
|
| Output fields | `path`, `symbol`, `score`, `updated_at_unix` |
|
||||||
|
| Raw | `null` |
|
||||||
|
| Side effect | 종목 평가 점수 파일을 생성하거나 갱신합니다. |
|
||||||
|
|
||||||
|
### `openstock score get <symbol>`
|
||||||
|
|
||||||
|
종목 ID의 저장된 평가 점수를 조회합니다.
|
||||||
|
|
||||||
|
| Direction | Data |
|
||||||
|
| --- | --- |
|
||||||
|
| Input | `symbol` |
|
||||||
|
| File read | `~/.config/openstock/scores.json` |
|
||||||
|
| Output fields | `path`, `symbol`, `score`, `updated_at_unix` |
|
||||||
|
| Raw | `null` |
|
||||||
|
| Side effect | 없음 |
|
||||||
|
|
||||||
|
### `openstock score list`
|
||||||
|
|
||||||
|
저장된 종목 평가 점수 목록을 점수 내림차순으로 조회합니다.
|
||||||
|
|
||||||
|
| Direction | Data |
|
||||||
|
| --- | --- |
|
||||||
|
| Input | 없음 |
|
||||||
|
| File read | `~/.config/openstock/scores.json` |
|
||||||
|
| Output fields | `path`, `count`, `scores` |
|
||||||
|
| Raw | `null` |
|
||||||
|
| Side effect | 없음 |
|
||||||
|
|
||||||
|
### `openstock score delete <symbol>`
|
||||||
|
|
||||||
|
종목 ID의 저장된 평가 점수를 삭제합니다.
|
||||||
|
|
||||||
|
| Direction | Data |
|
||||||
|
| --- | --- |
|
||||||
|
| Input | `symbol` |
|
||||||
|
| File read/write | `~/.config/openstock/scores.json` |
|
||||||
|
| Output fields | `path`, `symbol`, `deleted`, `removed` |
|
||||||
|
| Raw | `null` |
|
||||||
|
| Side effect | 저장된 점수가 있으면 해당 기록을 삭제합니다. |
|
||||||
|
|
||||||
### `openstock universe sync [--force]`
|
### `openstock universe sync [--force]`
|
||||||
|
|
||||||
KIND 상장법인목록을 받아 stock universe cache를 갱신합니다.
|
KIND 상장법인목록을 받아 stock universe cache를 갱신합니다.
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ Commands are documented by CLI depth under `references/commands`.
|
|||||||
| `openstock version` | `references/commands/version/guide.md` |
|
| `openstock version` | `references/commands/version/guide.md` |
|
||||||
| `openstock update` | `references/commands/update/guide.md` |
|
| `openstock update` | `references/commands/update/guide.md` |
|
||||||
| `openstock search` | `references/commands/search/guide.md` |
|
| `openstock search` | `references/commands/search/guide.md` |
|
||||||
|
| `openstock score *` | `references/commands/score/guide.md` |
|
||||||
| `openstock api *` | `references/commands/api/guide.md` |
|
| `openstock api *` | `references/commands/api/guide.md` |
|
||||||
| `openstock account *` | `references/commands/account/guide.md` |
|
| `openstock account *` | `references/commands/account/guide.md` |
|
||||||
| `openstock market *` | `references/commands/market/guide.md` |
|
| `openstock market *` | `references/commands/market/guide.md` |
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ This directory mirrors the `openstock` CLI command depth for agent execution.
|
|||||||
| `openstock version` | `version/` |
|
| `openstock version` | `version/` |
|
||||||
| `openstock update` | `update/` |
|
| `openstock update` | `update/` |
|
||||||
| `openstock search` | `search/` |
|
| `openstock search` | `search/` |
|
||||||
|
| `openstock score ...` | `score/` |
|
||||||
| `openstock api ...` | `api/` |
|
| `openstock api ...` | `api/` |
|
||||||
| `openstock account ...` | `account/` |
|
| `openstock account ...` | `account/` |
|
||||||
| `openstock market ...` | `market/` |
|
| `openstock market ...` | `market/` |
|
||||||
@@ -20,6 +21,7 @@ Subcommands also have leaf folders matching CLI depth, for example:
|
|||||||
| CLI Depth | Folder |
|
| CLI Depth | Folder |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `openstock api login` | `api/login/` |
|
| `openstock api login` | `api/login/` |
|
||||||
|
| `openstock score set` | `score/set/` |
|
||||||
| `openstock account status` | `account/status/` |
|
| `openstock account status` | `account/status/` |
|
||||||
| `openstock market history` | `market/history/` |
|
| `openstock market history` | `market/history/` |
|
||||||
| `openstock universe chunks` | `universe/chunks/` |
|
| `openstock universe chunks` | `universe/chunks/` |
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# `openstock score delete`
|
||||||
|
|
||||||
|
Purpose: delete a saved stock evaluation score.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openstock score delete 005930
|
||||||
|
```
|
||||||
|
|
||||||
|
Inputs: `symbol`.
|
||||||
|
|
||||||
|
Writes: `~/.config/openstock/scores.json` when a record exists.
|
||||||
|
|
||||||
|
Output fields: `path`, `symbol`, `deleted`, `removed`.
|
||||||
|
|
||||||
|
Agent rule: deletion changes local evaluation state; run only when requested.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# `openstock score get`
|
||||||
|
|
||||||
|
Purpose: read a saved stock evaluation score.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openstock score get 005930
|
||||||
|
```
|
||||||
|
|
||||||
|
Inputs: `symbol`.
|
||||||
|
|
||||||
|
Reads: `~/.config/openstock/scores.json`.
|
||||||
|
|
||||||
|
Output fields: `path`, `symbol`, `score`, `updated_at_unix`.
|
||||||
|
|
||||||
|
Agent rule: if the command fails with no saved score, do not infer a score of 0.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# `openstock score`
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Store and query local stock evaluation scores by stock ID.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Scores are stored beside `.env` under the OpenStock config directory:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/openstock/scores.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Each score is an integer from 0 to 100. `0` is the lowest evaluation and `100` is the highest.
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `openstock score set <symbol> <score>` | Save or update a stock score. |
|
||||||
|
| `openstock score get <symbol>` | Read one stock score. |
|
||||||
|
| `openstock score list` | List all saved scores. |
|
||||||
|
| `openstock score delete <symbol>` | Delete one stock score. |
|
||||||
|
|
||||||
|
## Output Fields
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `path` | Score file path. |
|
||||||
|
| `symbol` | Normalized stock ID. |
|
||||||
|
| `score` | 0 to 100 score. |
|
||||||
|
| `updated_at_unix` | Update Unix timestamp in seconds. |
|
||||||
|
| `count` | Number of saved scores. |
|
||||||
|
| `scores` | Score records sorted by score descending. |
|
||||||
|
| `deleted` | Whether a record was deleted. |
|
||||||
|
| `removed` | Deleted record, if any. |
|
||||||
|
|
||||||
|
## Agent Notes
|
||||||
|
|
||||||
|
Use this for user- or strategy-defined evaluation state. Do not treat the score as market evidence unless the workflow defines how it was produced.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# `openstock score list`
|
||||||
|
|
||||||
|
Purpose: list saved stock evaluation scores.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openstock score list
|
||||||
|
```
|
||||||
|
|
||||||
|
Reads: `~/.config/openstock/scores.json`.
|
||||||
|
|
||||||
|
Output fields: `path`, `count`, `scores`.
|
||||||
|
|
||||||
|
Agent rule: scores are sorted by score descending, then symbol ascending.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# `openstock score set`
|
||||||
|
|
||||||
|
Purpose: save or update a stock evaluation score.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openstock score set 005930 87
|
||||||
|
```
|
||||||
|
|
||||||
|
Inputs: `symbol`, `score` from 0 to 100.
|
||||||
|
|
||||||
|
Writes: `~/.config/openstock/scores.json`.
|
||||||
|
|
||||||
|
Output fields: `path`, `symbol`, `score`, `updated_at_unix`.
|
||||||
|
|
||||||
|
Agent rule: use only when the user or scoring workflow has produced a concrete score.
|
||||||
@@ -4,6 +4,7 @@ pub mod cache;
|
|||||||
pub mod dart;
|
pub mod dart;
|
||||||
pub mod market;
|
pub mod market;
|
||||||
pub mod order;
|
pub mod order;
|
||||||
|
pub mod score;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod universe;
|
pub mod universe;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
@@ -15,6 +16,7 @@ pub use cache::handle_cache;
|
|||||||
pub use dart::handle_dart;
|
pub use dart::handle_dart;
|
||||||
pub use market::handle_market;
|
pub use market::handle_market;
|
||||||
pub use order::handle_order;
|
pub use order::handle_order;
|
||||||
|
pub use score::handle_score;
|
||||||
pub use search::handle_search;
|
pub use search::handle_search;
|
||||||
pub use universe::handle_universe;
|
pub use universe::handle_universe;
|
||||||
pub use update::handle_update;
|
pub use update::handle_update;
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum ScoreCommands {
|
||||||
|
/// 종목 평가 점수 저장 또는 갱신
|
||||||
|
Set(ScoreSetCommand),
|
||||||
|
|
||||||
|
/// 종목 평가 점수 조회
|
||||||
|
Get(ScoreSymbolCommand),
|
||||||
|
|
||||||
|
/// 저장된 종목 평가 점수 목록 조회
|
||||||
|
List,
|
||||||
|
|
||||||
|
/// 종목 평가 점수 삭제
|
||||||
|
Delete(ScoreSymbolCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ScoreSetCommand {
|
||||||
|
/// 종목코드 또는 종목 ID
|
||||||
|
pub symbol: String,
|
||||||
|
|
||||||
|
/// 종목 평가 점수 (0~100)
|
||||||
|
pub score: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ScoreSymbolCommand {
|
||||||
|
/// 종목코드 또는 종목 ID
|
||||||
|
pub symbol: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_score(sub: &ScoreCommands) {
|
||||||
|
match sub {
|
||||||
|
ScoreCommands::Set(command) => {
|
||||||
|
match crate::core::scores::set(&command.symbol, command.score) {
|
||||||
|
Ok(record) => print_record("score set", "종목 평가 점수 저장 결과", record),
|
||||||
|
Err(err) => eprintln!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::error("score set", "종목 평가 점수 저장 실패", &err)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScoreCommands::Get(command) => match crate::core::scores::get(&command.symbol) {
|
||||||
|
Ok(Some(record)) => print_record("score get", "종목 평가 점수 조회 결과", record),
|
||||||
|
Ok(None) => eprintln!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::error(
|
||||||
|
"score get",
|
||||||
|
"종목 평가 점수 조회 실패",
|
||||||
|
"해당 종목의 저장된 점수가 없습니다.",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Err(err) => eprintln!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::error("score get", "종목 평가 점수 조회 실패", &err)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ScoreCommands::List => match crate::core::scores::list() {
|
||||||
|
Ok(scores) => println!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::explained_with_raw(
|
||||||
|
"score list",
|
||||||
|
"저장된 종목 평가 점수 목록",
|
||||||
|
vec![
|
||||||
|
crate::core::output::field(
|
||||||
|
"path",
|
||||||
|
"종목 평가 점수를 저장하는 파일 경로",
|
||||||
|
serde_json::json!(crate::core::scores::path()),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"count",
|
||||||
|
"저장된 종목 평가 점수 개수",
|
||||||
|
serde_json::json!(scores.len()),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"scores",
|
||||||
|
"점수 내림차순으로 정렬한 종목 평가 목록",
|
||||||
|
serde_json::json!(scores),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
serde_json::Value::Null,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Err(err) => eprintln!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::error("score list", "종목 평가 점수 목록 조회 실패", &err)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ScoreCommands::Delete(command) => match crate::core::scores::delete(&command.symbol) {
|
||||||
|
Ok(removed) => println!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::explained_with_raw(
|
||||||
|
"score delete",
|
||||||
|
"종목 평가 점수 삭제 결과",
|
||||||
|
vec![
|
||||||
|
crate::core::output::field(
|
||||||
|
"path",
|
||||||
|
"종목 평가 점수를 저장하는 파일 경로",
|
||||||
|
serde_json::json!(crate::core::scores::path()),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"symbol",
|
||||||
|
"삭제 요청한 종목코드 또는 종목 ID",
|
||||||
|
serde_json::json!(command.symbol.trim().to_ascii_uppercase()),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"deleted",
|
||||||
|
"저장된 점수가 존재해서 삭제되었는지 여부",
|
||||||
|
serde_json::json!(removed.is_some()),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"removed",
|
||||||
|
"삭제된 기존 점수 기록",
|
||||||
|
serde_json::json!(removed),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
serde_json::Value::Null,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Err(err) => eprintln!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::error("score delete", "종목 평가 점수 삭제 실패", &err)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_record(command: &str, description: &str, record: crate::core::scores::StockScore) {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
crate::core::output::explained_with_raw(
|
||||||
|
command,
|
||||||
|
description,
|
||||||
|
vec![
|
||||||
|
crate::core::output::field(
|
||||||
|
"path",
|
||||||
|
"종목 평가 점수를 저장하는 파일 경로",
|
||||||
|
serde_json::json!(crate::core::scores::path()),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"symbol",
|
||||||
|
"점수를 매긴 종목코드 또는 종목 ID",
|
||||||
|
serde_json::json!(record.symbol),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"score",
|
||||||
|
"종목 평가 점수. 0은 최저, 100은 최고",
|
||||||
|
serde_json::json!(record.score),
|
||||||
|
),
|
||||||
|
crate::core::output::field(
|
||||||
|
"updated_at_unix",
|
||||||
|
"점수를 저장하거나 갱신한 Unix timestamp(초)",
|
||||||
|
serde_json::json!(record.updated_at_unix),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
serde_json::Value::Null,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ pub mod http;
|
|||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
|
pub mod scores;
|
||||||
pub mod stock;
|
pub mod stock;
|
||||||
pub mod trader;
|
pub mod trader;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ pub fn env_file() -> PathBuf {
|
|||||||
config_dir().join(".env")
|
config_dir().join(".env")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn score_file() -> PathBuf {
|
||||||
|
config_dir().join("scores.json")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cache_dir() -> PathBuf {
|
pub fn cache_dir() -> PathBuf {
|
||||||
config_dir().join("cache")
|
config_dir().join("cache")
|
||||||
}
|
}
|
||||||
@@ -49,4 +53,10 @@ mod tests {
|
|||||||
let path = cache_namespace("universe/kind");
|
let path = cache_namespace("universe/kind");
|
||||||
assert!(path.ends_with("cache/universe/kind"));
|
assert!(path.ends_with("cache/universe/kind"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn score_file_lives_under_config_dir() {
|
||||||
|
let path = score_file();
|
||||||
|
assert!(path.ends_with("scores.json"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StockScoreStore {
|
||||||
|
pub version: u32,
|
||||||
|
pub scores: BTreeMap<String, StockScore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StockScore {
|
||||||
|
pub symbol: String,
|
||||||
|
pub score: u8,
|
||||||
|
pub updated_at_unix: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StockScoreStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: 1,
|
||||||
|
scores: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(symbol: &str, score: u16) -> Result<StockScore, String> {
|
||||||
|
validate_score(score)?;
|
||||||
|
let symbol = normalize_symbol(symbol)?;
|
||||||
|
let mut store = load()?;
|
||||||
|
let record = StockScore {
|
||||||
|
symbol: symbol.clone(),
|
||||||
|
score: score as u8,
|
||||||
|
updated_at_unix: current_unix_timestamp(),
|
||||||
|
};
|
||||||
|
store.scores.insert(symbol, record.clone());
|
||||||
|
save(&store)?;
|
||||||
|
Ok(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(symbol: &str) -> Result<Option<StockScore>, String> {
|
||||||
|
let symbol = normalize_symbol(symbol)?;
|
||||||
|
Ok(load()?.scores.get(&symbol).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list() -> Result<Vec<StockScore>, String> {
|
||||||
|
let mut scores = load()?.scores.into_values().collect::<Vec<_>>();
|
||||||
|
scores.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.symbol.cmp(&b.symbol)));
|
||||||
|
Ok(scores)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(symbol: &str) -> Result<Option<StockScore>, String> {
|
||||||
|
let symbol = normalize_symbol(symbol)?;
|
||||||
|
let mut store = load()?;
|
||||||
|
let removed = store.scores.remove(&symbol);
|
||||||
|
if removed.is_some() {
|
||||||
|
save(&store)?;
|
||||||
|
}
|
||||||
|
Ok(removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path() -> std::path::PathBuf {
|
||||||
|
crate::core::paths::score_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load() -> Result<StockScoreStore, String> {
|
||||||
|
let path = path();
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(StockScoreStore::default());
|
||||||
|
}
|
||||||
|
let text = fs::read_to_string(&path)
|
||||||
|
.map_err(|err| format!("종목 점수 파일 읽기 실패 ({}): {}", path.display(), err))?;
|
||||||
|
serde_json::from_str(&text).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"종목 점수 파일 JSON 파싱 실패 ({}): {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(store: &StockScoreStore) -> Result<(), String> {
|
||||||
|
let path = path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|err| format!("설정 디렉터리 생성 실패 ({}): {}", parent.display(), err))?;
|
||||||
|
}
|
||||||
|
let text = serde_json::to_string_pretty(store)
|
||||||
|
.map_err(|err| format!("종목 점수 JSON 직렬화 실패: {}", err))?;
|
||||||
|
write_atomic(&path, &text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_atomic(path: &Path, text: &str) -> Result<(), String> {
|
||||||
|
let tmp_path = path.with_extension("json.tmp");
|
||||||
|
fs::write(&tmp_path, text).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"종목 점수 임시 파일 쓰기 실패 ({}): {}",
|
||||||
|
tmp_path.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
fs::rename(&tmp_path, path).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"종목 점수 파일 교체 실패 ({} -> {}): {}",
|
||||||
|
tmp_path.display(),
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_symbol(symbol: &str) -> Result<String, String> {
|
||||||
|
let symbol = symbol.trim().to_ascii_uppercase();
|
||||||
|
crate::core::stock::validate_symbol(&symbol)?;
|
||||||
|
Ok(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_score(score: u16) -> Result<(), String> {
|
||||||
|
if score <= 100 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("종목 점수는 0 이상 100 이하이어야 합니다.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_unix_timestamp() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalizes_symbol_to_uppercase() {
|
||||||
|
assert_eq!(normalize_symbol(" a123 ").unwrap(), "A123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_score_bounds() {
|
||||||
|
assert!(validate_score(0).is_ok());
|
||||||
|
assert!(validate_score(100).is_ok());
|
||||||
|
assert!(validate_score(101).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use commands::cache::CacheCommands;
|
|||||||
use commands::dart::DartCommands;
|
use commands::dart::DartCommands;
|
||||||
use commands::market::MarketCommand;
|
use commands::market::MarketCommand;
|
||||||
use commands::order::OrderCommands;
|
use commands::order::OrderCommands;
|
||||||
|
use commands::score::ScoreCommands;
|
||||||
use commands::universe::UniverseCommands;
|
use commands::universe::UniverseCommands;
|
||||||
|
|
||||||
mod apis;
|
mod apis;
|
||||||
@@ -74,6 +75,12 @@ enum Commands {
|
|||||||
query: String,
|
query: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// 종목 평가 점수 저장 및 조회
|
||||||
|
Score {
|
||||||
|
#[command(subcommand)]
|
||||||
|
sub: ScoreCommands,
|
||||||
|
},
|
||||||
|
|
||||||
/// 종목 정보 및 기업정보 조회
|
/// 종목 정보 및 기업정보 조회
|
||||||
Market(MarketCommand),
|
Market(MarketCommand),
|
||||||
}
|
}
|
||||||
@@ -91,6 +98,7 @@ fn main() {
|
|||||||
Some(Commands::Order { sub }) => commands::handle_order(sub),
|
Some(Commands::Order { sub }) => commands::handle_order(sub),
|
||||||
Some(Commands::Universe { sub }) => commands::handle_universe(sub),
|
Some(Commands::Universe { sub }) => commands::handle_universe(sub),
|
||||||
Some(Commands::Search { query }) => commands::handle_search(query),
|
Some(Commands::Search { query }) => commands::handle_search(query),
|
||||||
|
Some(Commands::Score { sub }) => commands::handle_score(sub),
|
||||||
Some(Commands::Market(command)) => commands::handle_market(command),
|
Some(Commands::Market(command)) => commands::handle_market(command),
|
||||||
None => {
|
None => {
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
Reference in New Issue
Block a user