Yahoo Finance에서 주식 정보 가져오기 (4)

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 chrono crate을 이용한 format_timestamp 함수와 tokio main 매크로에 대해 알아보겠습니다.

Ⅰ. format_timestamp 함수에 대해 알아보기

fn format_timestamp(timestamp: i64) -> String {
match Utc.timestamp_opt(timestamp, 0) {
chrono::LocalResult::Single(datetime) => datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
_ => "Invalid timestamp".to_string(),
}
}

1. 입력값

timestamp: i64 : 이 값은 유닉스 타임스탬프(1970-01-01 00:00:00 UTC 기준의 초 단위 정수) 입니다.

2. Utc.timestamp_opt(timestamp, 0)

  • chrono::Utc는 UTC(협정 세계시) 타임존을 나타냅니다.
  • timestamp_opt(secs, nsecs)는 주어진 초(secs)와 나노초(nsecs)를 UTC 시각으로 변환하려고 시도합니다.
  • 반환값은 -chrono::LocalResult> enum인데, 이에는 세 가지 경우가 있습니다.
    – Single(datetime) → 정상적으로 변환됨
    – None → 변환 불가
    – Ambiguous(, ) → 모호한 시간 (주로 로컬 타임존에서 섬머타임 전환 시 발생, UTC에서는 거의 없음)

3. match 표현식

  • chrono::LocalResult::Single(datetime)일 경우에는 변환된 datetime을 format(“%Y-%m-%d %H:%M:%S”)로 지정한 문자열 포맷(연-월-일 시:분:초)으로 변환하고,
  • 그 밖의 경우(None, Ambiguous)는 “Invalid timestamp”라는 문자열을 반환합니다.

Ⅱ. tokio main 매크로에 대해 알아보기

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let symbols = [
"005930.KS", // 삼성전자
"AAPL", // Apple
"TSLA", // Tesla
"LIT", // Global X Lithium & Battery Tech ETF
"BRK-B", // Berkshire Hathaway Class B
"AMZN", // Amazon
"O", // Realty Income Corporation
"TQQQ", // ProShares UltraPro QQQ
"XOM", // Exxon Mobil
"WMT", // Walmart
];

println!("Fetching stock data...\n");

let results = fetch_multiple_stocks(&symbols).await;

// 테이블 헤더 출력
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
"No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
println!("{}", "-".repeat(95));

for (i, result) in results.iter().enumerate() {
let row_number = i + 1;
match result {
Ok(stock) => {
println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}",
row_number,
stock.symbol,
if stock.long_name.len() > 35 {
format!("{}...", &stock.long_name[..32])
} else {
stock.long_name.clone()
},
stock.regular_market_price,
stock.currency,
format_timestamp(stock.regular_market_time)
);
}
Err(e) => {
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
row_number,
symbols[i],
"Error fetching data",
"N/A",
"N/A",
"N/A"
);
}
}
}

Ok(())
}

1. Tokio 런타임

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
  • #[tokio::main]은 Tokio 비동기 런타임을 사용하는 매크로.
  • async fn main() → 메인 함수 자체가 비동기 함수로 실행됨.
  • 반환 타입이 Result<(), Box<dyn std::error::Error + Send + Sync>>으로서 성공했을 때는 (), 다시 말해 빈 튜플을 반환하므로 아무 값도 안돌려주고, 오류가 발생했을 때 다양한 에러 타입을 동적으로 담아 반환합니다.

2. 주식 심볼 배열

let symbols = [
"005930.KS", // 삼성전자
"AAPL", // Apple
"TSLA", // Tesla
"LIT", // Global X Lithium & Battery Tech ETF
"BRK-B", // Berkshire Hathaway Class B
"AMZN", // Amazon
"O", // Realty Income Corporation
"TQQQ", // ProShares UltraPro QQQ
"XOM", // Exxon Mobil
"WMT", // Walmart
];
  • fetch_multiple_stocks함수의 인수로 전달하기 위해 여러 주식의 티커(symbol)를 배열로 정의합니다.

3. 주식 데이터 가져오기

println!("Fetching stock data...\n");

let results = fetch_multiple_stocks(&symbols).await;
  • “Fetching stock data…” → 데이터 가져오기 시작 알림 출력.
  • fetch_multiple_stocks 함수는 비동기 함수로, 각 심볼에 대해 API 호출을 시도하여 Vec<Result<StockData, Error>> 를 반환함

4. 표 헤더 출력

println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
"No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
println!("{}", "-".repeat(95));
  • {:<n} 포맷은 왼쪽 정렬(Left Align) 하여 n 자리 확보.
  • 따라서 표 형태로 깔끔하게 정렬됨.
  • 이어서 “-“.repeat(95)로 구분선 출력.

5. 각 결과 출력

for (i, result) in results.iter().enumerate() {
let row_number = i + 1;
match result {
Ok(stock) => {
println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}",
row_number,
stock.symbol,
if stock.long_name.len() > 35 {
format!("{}...", &stock.long_name[..32])
} else {
stock.long_name.clone()
},
stock.regular_market_price,
stock.currency,
format_timestamp(stock.regular_market_time)
);
}
Err(e) => {
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
row_number,
symbols[i],
"Error fetching data",
"N/A",
"N/A",
"N/A"
);
}
}
}
  • enumerate()를 쓰면 (index, result) 튜플이 넘어옴. row_number = i + 1로 1씩 증가하는 번호를 출력.
    • match result:
      ① 성공(Ok(stock))하면
      – stock(StockData 구조체)를 이용해 주식 데이터를 출력하는데, stock은 result가 성공했을 때 값입니다.
      – stock.symbol 값을 symbol로 출력하고,
      – stock.long_name은 너무 길면 앞의 32자만 출력하고, + “…” 붙여서 가독성을 유지하고,
      – stock.regular_market_price는 소수점 둘째 자리까지 표시하고,({:.2}),
      – 화폐 단위(currency)를 출력하고,
      – stock.regular_market_time은 format_timestamp 함수로 변환하여 표시함

      ② 실패(Err)하면
      – 주어진 symbols[i] 심볼과 함께 “Error fetching data”, “N/A” 값들을 표시.

6. 종료

Ok(())
  • main 함수가 정상 종료됨을 의미.

Yahoo Finance에서 주식 정보 가져오기 (3)

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 fetch_stock_data과 fetch_multiple_stocks 함수에 대해 알아보겠습니다.

Ⅰ. fetch_stock_data 함수에 대해 알아보기

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);

let client = reqwest::Client::new();
let response = client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.send()
.await?;
let text = response.text().await?;
let yahoo_response: YahooResponse = serde_json::from_str(&text)?;
if let Some(result) = yahoo_response.chart.result.first() {
let meta = &result.meta;

Ok(StockData {
symbol: symbol.to_string(),
long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
regular_market_price: meta.regular_market_price.unwrap_or(0.0),
currency: meta.currency.clone(),
regular_market_time: meta.regular_market_time.unwrap_or(0),
})
} else {
Err(format!("No data found for symbol: {}", symbol).into())
}
}

1. 함수 시그니처

async fn fetch_stock_data(symbol: &str) 
-> Result<StockData, Box<dyn std::error::Error + Send + Sync>>
  • async fn → 비동기 함수, await를 사용할 수 있음
  • 입력값: symbol → “AAPL”, “TSLA” 같은 종목 코드
  • 반환값은 성공 시는 StockData 구조체, 실패 시는 에러(Box<dyn std::error::Error + Send + Sync>를 반환하는데, 멀티스레드 환경이라 Box<dyn std::error::Error >에 Send(스레드간 이동)와 Sync(동시 접근) trait를 추가한 것임

2. 함수 동작 흐름

가. API URL 만들기

let url = format!(
"https://query1.finance.yahoo.com/v8/finance/chart/{}",
symbol
);
  • symbol을 이용해 Yahoo Finance의 차트 데이터 API 주소 구성
    예) https://query1.finance.yahoo.com/v8/finance/chart/AAPL

나. HTTP 클라이언트 준비

let client = reqwest::Client::new();
  • reqwest 라이브러리의 비동기 HTTP 클라이언트를 생성

다. GET 요청 보내기

let response = client
.get(&url)
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
.send()
.await?;
  • Yahoo Finance API 서버에 GET 요청
  • 브라우저처럼보이게 하려고 User-Agent 헤더를 추가 (봇 차단 방지 목적)
  • .await? → 요청 완료를 기다리고, 실패 시 에러 전파

라. 응답 본문(JSON) 텍스트 추출

let text = response.text().await?;
  • HTTP 응답을 문자열 형태로 읽어옴

마. JSON 파싱

let yahoo_response: YahooResponse = serde_json::from_str(&text)?;
  • serde_json을 사용해 JSON 문자열을 YahooResponse구조체로 변환
  • 여기서 YahooResponse와 내부 구조(chart.result, meta 등)는 미리 serde를 이용해 파싱할 수 있도록 정의되어 있어야 함

바. 데이터 꺼내서 StockData 만들기

if let Some(result) = yahoo_response.chart.result.first() {
let meta = &result.meta;

Ok(StockData {
symbol: symbol.to_string(),
long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
regular_market_price: meta.regular_market_price.unwrap_or(0.0),
currency: meta.currency.clone(),
regular_market_time: meta.regular_market_time.unwrap_or(0),
})
} else {
Err(format!("No data found for symbol: {}", symbol).into())
}
  • chart.result의 첫 번째 요소를 가져옴
  • 거기서 meta 정보를 추출
  • 회사명, 현재 시장 가격, 통화 단위, 마지막 거래 시간 등을 꺼내 StockData에 담음
  • 데이터가 없으면 Err로 반환
위 예에서, unwrap_or는 Option이 None일 때 값을 0.0 또는 0으로 지정하는데,
unwrap_or_else는 Option이 None일 때 클로저가 실행되고, clone()이 추가된 차이점이 있음

Ⅱ. fetch_multiple_stocks 함수에 대해 알아보기

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
let mut handles = Vec::new();

for &symbol in symbols {
let symbol_owned = symbol.to_string();
let handle = tokio::spawn(async move {
fetch_stock_data(&symbol_owned).await
});
handles.push(handle);
}

let mut results = Vec::new();
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => results.push(Err(e.into())),
}
}

results
}

이 함수는 여러 주식 종목(symbol)을 동시에 비동기로 조회(fetch) 하기 위해 tokio::spawn을 사용하는 구조입니다.

1. 함수 시그니처

async fn fetch_multiple_stocks(
symbols: &[&str]
) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>>
  • async fn → 비동기 함수이므로 호출 시 .await 필요.
  • 입력값은 &[&str]로 &str 슬라이스 (예: &[“AAPL”, “GOOG”, “TSLA”])
  • 반환값: Result의 벡터
    – 각 종목(symbol)에 대해 Ok(StockData) 또는 Err(에러)가 담긴 리스트.
    – 즉, 한 종목 실패해도 다른 종목은 결과를 받을 수 있음.

2. 주요 동작 흐름

가. handles 벡터 생성

let mut handles = Vec::new();
  • 비동기 작업(태스크) 핸들을 저장해 둘 벡터.

나. 종목별 비동기 작업 생성

for &symbol in symbols {
let symbol_owned = symbol.to_string(); // 소유권 있는 String으로 변환
let handle = tokio::spawn(async move {
fetch_stock_data(&symbol_owned).await
});
handles.push(handle);
}
  • for 루프를 돌면서 각 종목 기호(&str)를 String으로 복사(to_string)
    → 이유: tokio::spawn의 async move 블록은 ‘static 라이프타임을 요구하기 때문.
    원본 &str는 반복문이 끝나면 사라질 수 있으니, 안전하게 소유권 있는 String 사용.
  • tokio::spawn(…) → 배경(백그라운드)에서 새로운 비동기 태스크 생성
  • async move → 클로저에 캡처되는 값(symbol_owned)을 이동(move)시켜 사용.
  • 결과: 각 종목을 조회하는 여러 비동기 태스크가 동시에 실행됨.

다. 모든 태스크 완료 대기

let mut results = Vec::new();
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => results.push(Err(e.into())),
}
}
  • handle.await → 해당 비동기 태스크가 끝날 때까지 대기.
  • handle.await의 반환값:
    – Ok(result) → 작업이 정상 종료 → result(= Result)를 results에 저장.
    – Err(e) → 태스크 자체가 패닉 또는 취소 → 에러를 Box로 변환해 저장.

라. 결과 반환

results
  • 벡터에는 각 종목별 Result<StockData, Error>가 순서대로 저장됨.

마. 주의점

종목 수가 매우 많으면 동시에 많은 태스크가 실행되어 서버나 네트워크에 부하 발생 가능 → tokio::task::JoinSet이나 futures::stream::FuturesUnordered로 동시 실행 수를 제한하는 방법 고려 가능.