Update app.py
Browse files
app.py
CHANGED
|
@@ -6,12 +6,144 @@ import pandas as pd
|
|
| 6 |
import plotly.express as px
|
| 7 |
from openai import OpenAI
|
| 8 |
|
| 9 |
-
# --- Streamlit 설정
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
if "OPENAI_API_KEY" not in os.environ:
|
| 13 |
-
st.error(
|
|
|
|
|
|
|
| 14 |
st.stop()
|
|
|
|
| 15 |
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
| 16 |
|
| 17 |
# --- 세션 상태 초기화 ---
|
|
@@ -20,229 +152,193 @@ if "chat_session" not in st.session_state:
|
|
| 20 |
if "portfolio" not in st.session_state:
|
| 21 |
st.session_state["portfolio"] = {"cash": 10000000, "stocks": {}}
|
| 22 |
if "stocks" not in st.session_state:
|
| 23 |
-
st.session_state["stocks"] = {} #
|
| 24 |
if "news_analysis_results" not in st.session_state:
|
| 25 |
st.session_state["news_analysis_results"] = {}
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
"
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
"
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
"
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
"
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
"
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
"
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
"
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
"
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
"
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
"
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
"
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
"
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
"
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
"
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
}
|
| 175 |
-
"오리온": {
|
| 176 |
-
"current_price": random.randint(150000, 180000),
|
| 177 |
-
"price_history": [],
|
| 178 |
-
"description": "초코파이, 오!감자, 포카칩 등 인기 과자 제조 기업. 글로벌 제과 시장 확대.",
|
| 179 |
-
"sector": "식품",
|
| 180 |
-
},
|
| 181 |
-
"아모레퍼시픽": {
|
| 182 |
-
"current_price": random.randint(150000, 170000),
|
| 183 |
-
"price_history": [],
|
| 184 |
-
"description": "설화수, 라네즈, 이니스프리 등 화장품 브랜드 보유. K-뷰티 Leading 기업. 글로벌 화장품 시장 진출.",
|
| 185 |
-
"sector": "화장품",
|
| 186 |
-
},
|
| 187 |
-
"LG생활건강": {
|
| 188 |
-
"current_price": random.randint(700000, 800000),
|
| 189 |
-
"price_history": [],
|
| 190 |
-
"description": "화장품, 생활용품, 음료 등 다양한 소비재 제조 기업. '숨', '후' 등 프리미엄 화장품 브랜드.",
|
| 191 |
-
"sector": "생활용품",
|
| 192 |
-
},
|
| 193 |
-
"이마트": {
|
| 194 |
-
"current_price": random.randint(120000, 140000),
|
| 195 |
-
"price_history": [],
|
| 196 |
-
"description": "대한민국 대표 대형 할인 마트. 신선 식품, 가공 식품, 생활 용품 등 판매. 온라인 쇼핑몰 운영.",
|
| 197 |
-
"sector": "유통",
|
| 198 |
-
},
|
| 199 |
-
"롯데쇼핑": {
|
| 200 |
-
"current_price": random.randint(180000, 200000),
|
| 201 |
-
"price_history": [],
|
| 202 |
-
"description": "백화점, 마트, 아울렛, 영화관 등 운영하는 유통 기업. 롯데백화점, 롯데마트 Leading 브랜드.",
|
| 203 |
-
"sector": "유통",
|
| 204 |
-
},
|
| 205 |
-
"두산에너빌리티": {
|
| 206 |
-
"current_price": random.randint(18000, 22000),
|
| 207 |
-
"price_history": [],
|
| 208 |
-
"description": "발전 설비, 해수 담수화 플랜트 등 에너지 인프라 기업. 친환경 에너지 기술 개발.",
|
| 209 |
-
"sector": "에너지",
|
| 210 |
-
},
|
| 211 |
-
"HD현대": {
|
| 212 |
-
"current_price": random.randint(50000, 60000),
|
| 213 |
-
"price_history": [],
|
| 214 |
-
"description": "조선, 건설기계, 에너지 등 종합 중공업 기업. 글로벌 조선 시장 Leading. 친환경 선박 기술 개발.",
|
| 215 |
-
"sector": "중공업",
|
| 216 |
-
},
|
| 217 |
-
"GS건설": {
|
| 218 |
-
"current_price": random.randint(40000, 50000),
|
| 219 |
-
"price_history": [],
|
| 220 |
-
"description": "자이 아파트 브랜드로 유명한 건설 기업. 국내외 건설 프로젝트 수행. 플랜트, 인프라 사업.",
|
| 221 |
-
"sector": "건설",
|
| 222 |
-
},
|
| 223 |
-
}
|
| 224 |
-
for stock_name in st.session_state["stocks"]:
|
| 225 |
-
st.session_state["stocks"][stock_name]["price_history"].append(
|
| 226 |
-
st.session_state["stocks"][stock_name]["current_price"]
|
| 227 |
-
)
|
| 228 |
|
| 229 |
-
# ---
|
| 230 |
def generate_news():
|
| 231 |
decade_count = st.session_state["decade_count"]
|
| 232 |
difficulty_level = st.session_state['difficulty_level']
|
| 233 |
|
| 234 |
-
difficulty_prompt_map = {
|
| 235 |
-
"초등학생": {
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
}
|
|
|
|
| 239 |
level_config = difficulty_prompt_map.get(difficulty_level, difficulty_prompt_map["중학생"])
|
|
|
|
| 240 |
level_prompt = level_config["level_desc"]
|
| 241 |
sentence_count = level_config["sentence_count"]
|
| 242 |
vocabulary_level = level_config["vocabulary_level"]
|
| 243 |
inference_level = level_config["inference_level"]
|
| 244 |
|
| 245 |
-
decade_keyword_map = { # Decade별 키워드
|
| 246 |
1950: "1950년대 한국전쟁 이후 재건, 원조 경제, 농업, 경공업",
|
| 247 |
1960: "1960년대 경제 개발 5개년 계획, 수출 주도 성장, 경공업 육성",
|
| 248 |
1970: "1970년대 중화학공업 육성, 오일 쇼크, 새마을 운동, 건설업",
|
|
@@ -252,15 +348,15 @@ def generate_news():
|
|
| 252 |
2010: "2010년대 스마트폰 보급, 소셜 미디어, 핀테크, 공유 경제, 바이오시밀러",
|
| 253 |
2020: "2020년대 코로나19 팬데믹, 디지털 전환, 플랫폼 경제, ESG 경영, 미래 모빌리티, 인공지능",
|
| 254 |
}
|
| 255 |
-
decade_keywords = decade_keyword_map.get(decade_count, "2020년대 미래산업, 플랫폼 경제")
|
| 256 |
|
| 257 |
prompt = f"""
|
| 258 |
지시:
|
| 259 |
{level_prompt}에 맞춰서, **{decade_count}년대 대한민국 경제**와 관련된 뉴스 기사 5개를 생성해주세요.
|
| 260 |
**뉴스 주제**는 "{decade_keywords}" 키워드를 참고하여 시대적 특징을 반영해주세요.
|
| 261 |
각 기사는 {sentence_count}문장 정도로 작성하고, {vocabulary_level}를 사용하여 학생들이 이해하기 쉬워야 합니다.
|
| 262 |
-
학생들이 뉴스를 읽고 {inference_level} 수준에서
|
| 263 |
-
|
| 264 |
긍정적 뉴스, 부정적 뉴스, 중립적 뉴스 다양하게 생성하세요.(긍정, 부정, 중립 이라는 말은 표시하지 마세요.)
|
| 265 |
뉴스에 따라 주식이 상승하기도 하고 하락하기도 할 수 있습니다.
|
| 266 |
각 뉴스 기사는 "## 뉴스 [번호]" 로 시작해주세요. (예: ## 뉴스 1, ## 뉴스 2 ...)
|
|
@@ -270,7 +366,7 @@ def generate_news():
|
|
| 270 |
chat_session = st.session_state["chat_session"]
|
| 271 |
messages = [{"role": "user", "content": prompt}]
|
| 272 |
|
| 273 |
-
try: # OpenAI API 호출 예외 처리
|
| 274 |
response = client.chat.completions.create(
|
| 275 |
model="gpt-4o-mini",
|
| 276 |
messages=messages,
|
|
@@ -293,17 +389,17 @@ def generate_news():
|
|
| 293 |
|
| 294 |
return news_articles[:5]
|
| 295 |
|
| 296 |
-
except Exception as e: # API 호출 에러
|
| 297 |
-
st.error(f"뉴스 생성 중 오류 발생했습니다: {e}")
|
| 298 |
-
return []
|
| 299 |
|
| 300 |
-
# explain_daily_news_meanings 함수 (
|
| 301 |
def explain_daily_news_meanings(daily_news):
|
| 302 |
if daily_news is None:
|
| 303 |
return {}
|
| 304 |
|
| 305 |
difficulty_level = st.session_state['difficulty_level']
|
| 306 |
-
difficulty_prompt_map = {
|
| 307 |
"초등학생": "초등학생 5~6학년",
|
| 308 |
"중학생": "중학생 1~3학년",
|
| 309 |
"고등학생": "고등학생 1~3학년",
|
|
@@ -318,13 +414,13 @@ def explain_daily_news_meanings(daily_news):
|
|
| 318 |
|
| 319 |
**지시:**
|
| 320 |
위 신문 기사의 핵심 의미를 {level_prompt}이 이해하기 쉽게 3문장 이내로 요약해서 "해설: " 다음에 설명해주세요.
|
| 321 |
-
|
| 322 |
|
| 323 |
뉴스 의미 해설:
|
| 324 |
"""
|
| 325 |
chat_session = st.session_state["chat_session"]
|
| 326 |
messages = [{"role": "user", "content": prompt}]
|
| 327 |
-
try:
|
| 328 |
response = client.chat.completions.create(
|
| 329 |
model="gpt-4o-mini",
|
| 330 |
messages=messages,
|
|
@@ -340,13 +436,26 @@ def explain_daily_news_meanings(daily_news):
|
|
| 340 |
meaning_text = response.choices[0].message.content.strip()
|
| 341 |
|
| 342 |
explanation = ""
|
|
|
|
| 343 |
|
| 344 |
if "해설:" in meaning_text:
|
| 345 |
explanation_start_index = meaning_text.find("해설:") + len("해설:")
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
st.error(
|
| 351 |
f"API 할당량 초과 오류가 발생했습니다. 잠시 후 다시 시도해주세요. 오류 메시지: {e}"
|
| 352 |
)
|
|
@@ -354,23 +463,26 @@ def explain_daily_news_meanings(daily_news):
|
|
| 354 |
time.sleep(1)
|
| 355 |
return meanings
|
| 356 |
|
| 357 |
-
# buy_stock, sell_stock 함수 (
|
| 358 |
-
def buy_stock(stock_name, quantity
|
| 359 |
-
if
|
|
|
|
|
|
|
|
|
|
| 360 |
st.session_state["messages"].append(
|
| 361 |
{"type": "error", "text": "존재하지 않는 주식 종목입니다."}
|
| 362 |
)
|
| 363 |
return
|
| 364 |
|
| 365 |
-
if quantity <= 0:
|
| 366 |
st.session_state["messages"].append(
|
| 367 |
{"type": "error", "text": "매수 수량은 1주 이상이어야 합니다."}
|
| 368 |
)
|
| 369 |
return
|
| 370 |
|
| 371 |
-
stock_price = st.session_state["stocks"][stock_name]["current_price"]
|
| 372 |
max_quantity = st.session_state["portfolio"]["cash"] // stock_price
|
| 373 |
-
if quantity > max_quantity:
|
| 374 |
st.session_state["messages"].append(
|
| 375 |
{
|
| 376 |
"type": "error",
|
|
@@ -385,7 +497,7 @@ def buy_stock(stock_name, quantity): # sector 인자 제거
|
|
| 385 |
|
| 386 |
total_price = stock_price * quantity
|
| 387 |
|
| 388 |
-
if st.session_state["portfolio"]["cash"] >= total_price:
|
| 389 |
st.session_state["portfolio"]["cash"] -= total_price
|
| 390 |
portfolio_stocks = st.session_state["portfolio"]["stocks"]
|
| 391 |
if (
|
|
@@ -413,7 +525,7 @@ def buy_stock(stock_name, quantity): # sector 인자 제거
|
|
| 413 |
f"{stock_name} {quantity}주 매수 완료. 총 {total_price:,.0f}원 소요.", icon="✅"
|
| 414 |
)
|
| 415 |
st.session_state['buy_confirm'] = False
|
| 416 |
-
else:
|
| 417 |
st.session_state["messages"].append(
|
| 418 |
{"type": "error", "text": "잔액이 부족합니다."}
|
| 419 |
)
|
|
@@ -421,8 +533,8 @@ def buy_stock(stock_name, quantity): # sector 인자 제거
|
|
| 421 |
st.error(f"잔액이 부족합니다. (최대 {max_quantity}주까지 매수 가능)")
|
| 422 |
st.session_state['buy_confirm'] = False
|
| 423 |
|
| 424 |
-
|
| 425 |
-
def sell_stock(stock_name, quantity):
|
| 426 |
if stock_name not in st.session_state["portfolio"]["stocks"]:
|
| 427 |
st.session_state["messages"].append(
|
| 428 |
{"type": "error", "text": "보유하고 있지 않은 주식입니다."}
|
|
@@ -450,13 +562,12 @@ def sell_stock(stock_name, quantity): # sell_stock 함수는 변경 없음
|
|
| 450 |
return
|
| 451 |
|
| 452 |
stock_price = 0
|
| 453 |
-
stock_sector = ""
|
| 454 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 455 |
if stock_name in stocks:
|
| 456 |
stock_price = stocks[stock_name]["current_price"]
|
| 457 |
stock_sector = sector
|
| 458 |
break
|
| 459 |
-
stock_price = st.session_state["stocks"][stock_name]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
|
| 460 |
|
| 461 |
if stock_price == 0:
|
| 462 |
st.session_state["messages"].append(
|
|
@@ -482,49 +593,68 @@ def sell_stock(stock_name, quantity): # sell_stock 함수는 변경 없음
|
|
| 482 |
st.success(f"{stock_name} {quantity}주 매도 완료. 총 {sell_price:,.0f}원 획득.")
|
| 483 |
st.session_state['sell_confirm'] = False
|
| 484 |
|
| 485 |
-
# update_stock_prices 함수 (
|
| 486 |
def update_stock_prices():
|
| 487 |
if not st.session_state["daily_news"]:
|
| 488 |
return
|
| 489 |
|
|
|
|
| 490 |
decade_count = st.session_state["decade_count"]
|
| 491 |
-
decade_economic_conditions = { # Decade별 경제 상황 (
|
| 492 |
-
1950: {"growth_rate": 0.02, "volatility": 0.03},
|
| 493 |
-
1960: {"growth_rate": 0.05, "volatility": 0.02},
|
| 494 |
-
1970: {"growth_rate": 0.08, "volatility": 0.05},
|
| 495 |
-
1980: {"growth_rate": 0.06, "volatility": 0.04},
|
| 496 |
-
1990: {"growth_rate": 0.03, "volatility": 0.1},
|
| 497 |
-
2000: {"growth_rate": 0.04, "volatility": 0.07},
|
| 498 |
-
2010: {"growth_rate": 0.03, "volatility": 0.05},
|
| 499 |
-
2020: {"growth_rate": 0.02, "volatility": 0.08},
|
| 500 |
}
|
| 501 |
-
economic_condition = decade_economic_conditions.get(decade_count, {"growth_rate": 0.03, "volatility": 0.06})
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
st.session_state["messages"].append({"type": "info", "text": "주가가 변동되었습니다."})
|
| 522 |
st.toast("주가가 변동되었습니다.", icon="📈")
|
| 523 |
st.info("주가가 변동되었습니다.")
|
| 524 |
-
st.session_state["sector_news_impact"] =
|
| 525 |
|
| 526 |
-
# display_portfolio, display_stock_prices, display_portfolio_table 함수 (
|
| 527 |
-
def display_portfolio():
|
| 528 |
portfolio = st.session_state["portfolio"]
|
| 529 |
cash = portfolio["cash"]
|
| 530 |
total_value = cash
|
|
@@ -533,15 +663,12 @@ def display_portfolio(): # display_portfolio 함수는 변경 없음
|
|
| 533 |
quantity = stock_info["quantity"]
|
| 534 |
purchase_price = stock_info["purchase_price"]
|
| 535 |
current_price = 0
|
| 536 |
-
stock_sector = ""
|
| 537 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 538 |
if stock_name in stocks:
|
| 539 |
current_price = stocks[stock_name]["current_price"]
|
| 540 |
-
stock_sector =
|
| 541 |
break
|
| 542 |
-
current_price = st.session_state["stocks"][stock_name]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
|
| 543 |
-
stock_sector = st.session_state["stocks"][stock_name]["sector"] # 수정: 섹터 iteration 없이 바로 접근
|
| 544 |
-
|
| 545 |
if current_price != 0:
|
| 546 |
stock_value = current_price * quantity
|
| 547 |
total_value += stock_value
|
|
@@ -558,33 +685,34 @@ def display_portfolio(): # display_portfolio 함수는 변경 없음
|
|
| 558 |
|
| 559 |
def display_stock_prices():
|
| 560 |
stocks_data = []
|
| 561 |
-
for
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
|
|
|
| 580 |
stocks_df = pd.DataFrame(stocks_data)
|
| 581 |
-
st.dataframe(stocks_df[["섹터", "종목", "현재 주가", "전일 대비"]], hide_index=True) # "
|
| 582 |
|
| 583 |
selected_stock_all_info = st.selectbox(
|
| 584 |
"종목 선택 (기업 정보 및 주가 그래프)", stocks_df["종목"].tolist()
|
| 585 |
)
|
| 586 |
if selected_stock_all_info:
|
| 587 |
-
selected_stock_sector = stocks_df[
|
| 588 |
stocks_df["종목"] == selected_stock_all_info
|
| 589 |
]["섹터"].iloc[0]
|
| 590 |
col1_info, col2_graph = st.columns([1, 2])
|
|
@@ -592,34 +720,38 @@ def display_stock_prices():
|
|
| 592 |
with col1_info:
|
| 593 |
st.subheader("기업 정보")
|
| 594 |
st.info(
|
| 595 |
-
f"**{selected_stock_all_info} ({selected_stock_sector})**\n\n{st.session_state['stocks'][selected_stock_all_info]['description']}"
|
| 596 |
)
|
| 597 |
|
| 598 |
-
with col2_graph:
|
| 599 |
st.subheader("주가 그래프")
|
| 600 |
price_history_df = pd.DataFrame(
|
| 601 |
{
|
| 602 |
"날짜": range(
|
| 603 |
1,
|
| 604 |
len(
|
| 605 |
-
st.session_state["stocks"][
|
|
|
|
|
|
|
| 606 |
)
|
| 607 |
+ 1,
|
| 608 |
),
|
| 609 |
-
"주가": st.session_state["stocks"][
|
|
|
|
|
|
|
| 610 |
}
|
| 611 |
)
|
| 612 |
fig = px.line(
|
| 613 |
price_history_df,
|
| 614 |
x="날짜",
|
| 615 |
y="주가",
|
| 616 |
-
title=f"{selected_stock_all_info} ({selected_stock_sector}) 주가 변동",
|
| 617 |
)
|
| 618 |
st.plotly_chart(fig)
|
| 619 |
else:
|
| 620 |
st.info("종목을 선택하여 기업 정보와 주가 그래프를 확인하세요.")
|
| 621 |
|
| 622 |
-
def display_portfolio_table():
|
| 623 |
portfolio = st.session_state["portfolio"]
|
| 624 |
if portfolio["stocks"]:
|
| 625 |
portfolio_data = []
|
|
@@ -631,14 +763,12 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
|
|
| 631 |
quantity = stock_info["quantity"]
|
| 632 |
purchase_price = stock_info["purchase_price"]
|
| 633 |
current_price = 0
|
| 634 |
-
stock_sector = ""
|
| 635 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 636 |
if stock_name in stocks:
|
| 637 |
current_price = stocks[stock_name]["current_price"]
|
| 638 |
-
stock_sector =
|
| 639 |
break
|
| 640 |
-
current_price = st.session_state["stocks"][stock_name]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
|
| 641 |
-
stock_sector = st.session_state["stocks"][stock_name]["sector"] # 수정: 섹터 iteration 없이 바로 접근
|
| 642 |
|
| 643 |
if current_price == 0:
|
| 644 |
continue
|
|
@@ -656,7 +786,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
|
|
| 656 |
portfolio_data.append(
|
| 657 |
{
|
| 658 |
"종목": stock_name,
|
| 659 |
-
"섹터": stock_sector,
|
| 660 |
"보유 수량": quantity,
|
| 661 |
"매수 단가": f"{purchase_price:,.0f} 원",
|
| 662 |
"현재가": f"{current_price:,.0f} 원",
|
|
@@ -665,7 +795,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
|
|
| 665 |
"수익률": f"{profit_rate:.2f}%",
|
| 666 |
}
|
| 667 |
)
|
| 668 |
-
portfolio_data.append(
|
| 669 |
{
|
| 670 |
"종목": "현금",
|
| 671 |
"섹터": "-",
|
|
@@ -679,7 +809,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
|
|
| 679 |
)
|
| 680 |
portfolio_df = pd.DataFrame(portfolio_data)
|
| 681 |
st.dataframe(portfolio_df, hide_index=True, height=350)
|
| 682 |
-
st.markdown(
|
| 683 |
f"""**현금 잔고:** {portfolio['cash']:,} 원
|
| 684 |
**📊 총 평가액:** {total_value:,.0f} 원
|
| 685 |
**🛒 총 매수 금액:** {total_purchase_value:,.0f} 원
|
|
@@ -690,7 +820,7 @@ def display_portfolio_table(): # display_portfolio_table 함수는 섹터 정보
|
|
| 690 |
st.info("보유 주식이 없습니다.")
|
| 691 |
|
| 692 |
# display_stock_glossary 함수 (기존 코드와 동일)
|
| 693 |
-
def display_stock_glossary():
|
| 694 |
glossary = {
|
| 695 |
"주식": "회사의 일부분을 나타내는 증서. 주식을 사면 회사의 주인이 되는 거예요.",
|
| 696 |
"주가": "주식 1개당 가격. 사람들이 주식을 사고팔 때 가격이 변해요.",
|
|
@@ -714,15 +844,15 @@ def display_stock_glossary(): # display_stock_glossary 함수는 변경 없음
|
|
| 714 |
def main():
|
| 715 |
col_news, col_main_ui = st.columns([1, 2])
|
| 716 |
|
| 717 |
-
with col_news:
|
| 718 |
-
st.header(f"📰 {st.session_state['decade_count']}년대 뉴스")
|
| 719 |
if st.button("뉴스 생성", use_container_width=True, key="news_gen_button"):
|
| 720 |
-
with st.spinner(f"{st.session_state['decade_count']}년대 뉴스 생성 중..."):
|
| 721 |
current_daily_news = generate_news()
|
| 722 |
st.session_state["daily_news"] = current_daily_news
|
| 723 |
|
| 724 |
if st.session_state["daily_news"]:
|
| 725 |
-
st.subheader(f"{st.session_state['decade_count']}년대 뉴스")
|
| 726 |
for i, news in enumerate(st.session_state["daily_news"]):
|
| 727 |
with st.expander(f"뉴스 {i+1}", expanded=False):
|
| 728 |
st.write(news)
|
|
@@ -730,14 +860,18 @@ def main():
|
|
| 730 |
if st.session_state["previous_daily_news"] and st.session_state[
|
| 731 |
"news_meanings"
|
| 732 |
]:
|
| 733 |
-
previous_decade = st.session_state['decade_count'] - 10
|
| 734 |
-
st.subheader(f"{previous_decade}년대 뉴스 해설")
|
| 735 |
st.info("AI가 분석한 지난 뉴스 해설입니다.")
|
| 736 |
-
with st.expander(f"{previous_decade}년대 뉴스 해설 보기", expanded=False):
|
| 737 |
if st.session_state["news_meanings"]:
|
| 738 |
for i, meaning_data in st.session_state["news_meanings"].items():
|
| 739 |
st.markdown(f"**뉴스 {i}**:")
|
| 740 |
-
st.markdown(f"**AI 해설:** {meaning_data['explanation']}") #
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
else:
|
| 742 |
st.info("지난 뉴스에 대한 해설이 없습니다.")
|
| 743 |
|
|
@@ -745,33 +879,39 @@ def main():
|
|
| 745 |
st.info("뉴스 생성 버튼을 눌러 오늘의 뉴스를 받아보세요.")
|
| 746 |
|
| 747 |
with col_main_ui:
|
| 748 |
-
menu = st.tabs(
|
| 749 |
['현재 주가', '내 포트폴리오', '주식 매수', '주식 매도', '지난 뉴스 해설']
|
| 750 |
)
|
| 751 |
|
| 752 |
-
with menu[0]:
|
| 753 |
st.subheader("📈 현재 주가 및 기업 정보")
|
| 754 |
st.markdown("주식 시장의 현재 가격과 기업 정보를 확인하세요.")
|
| 755 |
display_stock_prices()
|
| 756 |
|
| 757 |
-
with menu[1]:
|
| 758 |
st.subheader("📊 내 포트폴리오")
|
| 759 |
st.markdown("현재 보유 중인 주식과 자산을 확인하세요.")
|
| 760 |
display_portfolio_table()
|
| 761 |
|
| 762 |
-
with menu[2]:
|
| 763 |
st.subheader("💰 주식 매수")
|
| 764 |
st.markdown("AI 예측과 뉴스 분석을 바탕으로 주식을 매수해보세요.")
|
| 765 |
-
|
| 766 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
|
| 768 |
-
stock_price_buy = st.session_state["stocks"][
|
|
|
|
|
|
|
| 769 |
st.info(f"**{selected_stock_buy}** 현재 주가: {stock_price_buy:,.0f}원")
|
| 770 |
quantity_buy = st.number_input(
|
| 771 |
"매수 수량 (주):", min_value=1, value=1, step=1
|
| 772 |
)
|
| 773 |
|
| 774 |
-
if not st.session_state['buy_confirm']:
|
| 775 |
if st.button("주식 매수", use_container_width=True, key='buy_button_confirm'):
|
| 776 |
st.session_state['buy_confirm'] = True
|
| 777 |
else:
|
|
@@ -779,25 +919,24 @@ def main():
|
|
| 779 |
col_confirm, col_cancel = st.columns([1, 1])
|
| 780 |
with col_confirm:
|
| 781 |
if st.button("✅ 매수 확인", use_container_width=True, key='buy_confirm_button'):
|
| 782 |
-
buy_stock(selected_stock_buy, quantity_buy
|
| 783 |
|
| 784 |
with col_cancel:
|
| 785 |
if st.button("❌ 매수 취소", use_container_width=True, key='buy_cancel_button', type='secondary'):
|
| 786 |
st.session_state['buy_confirm'] = False
|
| 787 |
st.info("매수를 취소했습니다.")
|
| 788 |
|
| 789 |
-
with menu[3]:
|
| 790 |
st.subheader("📉 주식 매도")
|
| 791 |
st.markdown("보유 중인 주식을 판매하고 수익을 실현해보세요.")
|
| 792 |
if st.session_state["portfolio"]["stocks"]:
|
| 793 |
stock_names_sell = list(st.session_state["portfolio"]["stocks"].keys())
|
| 794 |
selected_stock_sell = st.selectbox("매도 종목 선택:", stock_names_sell)
|
| 795 |
stock_price_sell = 0
|
| 796 |
-
for sector, stocks in st.session_state["stocks"].items():
|
| 797 |
if selected_stock_sell in stocks:
|
| 798 |
stock_price_sell = stocks[selected_stock_sell]["current_price"]
|
| 799 |
break
|
| 800 |
-
stock_price_sell = st.session_state["stocks"][selected_stock_sell]["current_price"] # 수정: 섹터 iteration 없이 바로 접근
|
| 801 |
|
| 802 |
st.info(f"**{selected_stock_sell}** 현재 주가: {stock_price_sell:,.0f}원")
|
| 803 |
max_sell_quantity = st.session_state["portfolio"]["stocks"][
|
|
@@ -810,7 +949,7 @@ def main():
|
|
| 810 |
value=1,
|
| 811 |
step=1,
|
| 812 |
)
|
| 813 |
-
if not st.session_state['sell_confirm']:
|
| 814 |
if st.button("주식 매도", use_container_width=True, key='sell_button_confirm'):
|
| 815 |
st.session_state['sell_confirm'] = True
|
| 816 |
else:
|
|
@@ -818,7 +957,7 @@ def main():
|
|
| 818 |
col_confirm, col_cancel = st.columns([1, 1])
|
| 819 |
with col_confirm:
|
| 820 |
if st.button("✅ 매도 확인", use_container_width=True, key='sell_confirm_button'):
|
| 821 |
-
sell_stock(selected_stock_sell, quantity_sell)
|
| 822 |
with col_cancel:
|
| 823 |
if st.button("❌ 매도 취소", use_container_width=True, key='sell_cancel_button', type='secondary'):
|
| 824 |
st.session_state['sell_confirm'] = False
|
|
@@ -826,12 +965,12 @@ def main():
|
|
| 826 |
else:
|
| 827 |
st.info("보유 주식이 없습니다. 포트폴리오 탭에서 확인하세요.")
|
| 828 |
|
| 829 |
-
with menu[4]:
|
| 830 |
if st.session_state["previous_daily_news"] and st.session_state[
|
| 831 |
"news_meanings"
|
| 832 |
]:
|
| 833 |
-
previous_decade = st.session_state['decade_count'] - 10
|
| 834 |
-
st.subheader(f"{previous_decade}년대 뉴스 해설")
|
| 835 |
st.info("AI가 분석한 지난 뉴스 해설입니다.")
|
| 836 |
for i in range(len(st.session_state["previous_daily_news"])):
|
| 837 |
with st.expander(f"뉴스 {i+1}", expanded=False):
|
|
@@ -841,7 +980,12 @@ def main():
|
|
| 841 |
meaning_data = st.session_state["news_meanings"].get(str(i+1))
|
| 842 |
if meaning_data:
|
| 843 |
st.markdown("**AI 해설:**")
|
| 844 |
-
st.info(meaning_data['explanation'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
else:
|
| 846 |
st.warning("뉴스 해설을 생성하지 못했습니다.")
|
| 847 |
else:
|
|
@@ -849,12 +993,13 @@ def main():
|
|
| 849 |
"이전 뉴스 해설이 없습니다. 10년 후 버튼을 눌러 뉴스 해설을 받아보세요."
|
| 850 |
)
|
| 851 |
|
| 852 |
-
with st.sidebar:
|
| 853 |
-
st.markdown(f"# ⏳ 대한민국 경제 발전사 게임")
|
| 854 |
-
st.markdown(f"### {st.session_state['decade_count']}년대")
|
| 855 |
st.markdown("---")
|
| 856 |
|
| 857 |
-
|
|
|
|
| 858 |
"📈 난이도 선택",
|
| 859 |
["초등학생", "중학생", "고등학생"],
|
| 860 |
index=["초등학생", "중학생", "고등학생"].index(st.session_state['difficulty_level'])
|
|
@@ -862,7 +1007,7 @@ def main():
|
|
| 862 |
st.session_state['difficulty_level'] = difficulty_level
|
| 863 |
st.markdown("---")
|
| 864 |
|
| 865 |
-
decade_description_map = { # Decade별 게임 설명
|
| 866 |
1950: "🇰🇷 **1950년대: 재건의 시대**\n\n 전쟁의 상처를 딛고 일어서는 시기입니다. 원조 경제와 농업을 기반으로 재건에 힘쓰세요.",
|
| 867 |
1960: "🇰🇷 **1960년대: 경제 개발의 닻을 올리다**\n\n 경제 개발 5개년 계획이 시작됩니다. 수출과 경공업을 통해 경제 성장의 기반을 마련하세요.",
|
| 868 |
1970: "🇰🇷 **1970년대: 중화학공업, 성장의 엔진**\n\n 중화학공업 육성 정책이 본격화됩니다. 건설 붐과 함께 중동 시장을 개척해보세요.",
|
|
@@ -872,19 +1017,19 @@ def main():
|
|
| 872 |
2010: "🇰🇷 **2010년대: 스마트 혁명, 플랫폼 경제**\n\n 스마트폰이 세상을 바꾸고, 플랫폼 기업이 성장합니다. 새로운 경제 질서에 올라타세요.",
|
| 873 |
2020: "🇰🇷 **2020년대: 미래를 향한 도전**\n\n 팬데믹을 극복하고 미래 산업을 육성합니다. 친환경 에너지, 바이오, 플랫폼 기업의 미래를 예측해보세요.",
|
| 874 |
}
|
| 875 |
-
decade_description = decade_description_map.get(st.session_state['decade_count'], "🇰🇷 **미래 시대:**\n\n 미래 시대에 대비하여 새로운 산업과 기술에 투자해보세요.")
|
| 876 |
|
| 877 |
-
st.markdown(decade_description)
|
| 878 |
|
| 879 |
-
cash, total_value, profit_rate = display_portfolio()
|
| 880 |
st.metric(label="💰 현금 잔고", value=f"{cash:,.0f} 원")
|
| 881 |
st.metric(label="📊 총 평가 금액", value=f"{total_value:,.0f} 원")
|
| 882 |
st.metric(label="🚀 총 수익률", value=f"{profit_rate:.2f}%")
|
| 883 |
st.markdown("---")
|
| 884 |
|
| 885 |
-
if st.button("10년 후", use_container_width=True, key="decade_pass_button"): #
|
| 886 |
if st.session_state["daily_news"]:
|
| 887 |
-
with st.spinner(f"{st.session_state['decade_count']}년대 주가 변동 및 지난 뉴스 분석..."):
|
| 888 |
st.session_state["previous_daily_news"] = st.session_state["daily_news"]
|
| 889 |
meanings = explain_daily_news_meanings(
|
| 890 |
st.session_state["previous_daily_news"]
|
|
@@ -893,18 +1038,18 @@ def main():
|
|
| 893 |
st.session_state["news_meanings"] = meanings
|
| 894 |
update_stock_prices()
|
| 895 |
st.session_state["daily_news"] = generate_news()
|
| 896 |
-
st.session_state["decade_count"] += 10
|
| 897 |
-
|
| 898 |
st.rerun()
|
| 899 |
-
previous_decade = st.session_state['decade_count'] - 10
|
| 900 |
-
st.info(f"지난 {previous_decade}년대 뉴스 해설 탭에서 AI 분석을 확인해보세요.")
|
| 901 |
else:
|
| 902 |
st.warning("오늘의 뉴스를 먼저 생성해주세요.")
|
| 903 |
st.markdown("***")
|
| 904 |
|
| 905 |
-
display_stock_glossary()
|
| 906 |
|
| 907 |
-
with st.expander("🚀 앱 사용 가이드", expanded=False): # 앱
|
| 908 |
st.markdown(
|
| 909 |
"""
|
| 910 |
**대한민국 경제 발전사 주식 투자 게임**에 오신 것을 환영합니다! ⏳
|
|
@@ -948,8 +1093,8 @@ def main():
|
|
| 948 |
|
| 949 |
**대한민국 경제 발전사를 따라가는 흥미진진한 주식 투자 게임! 지금 시작하세요!** 🚀
|
| 950 |
""".format(decade=st.session_state['decade_count'], next_decade=st.session_state['decade_count'] + 10)
|
| 951 |
-
|
| 952 |
-
# Supabase 관련 함수 및 로그인 사이드바 제거
|
| 953 |
|
| 954 |
if __name__ == "__main__":
|
|
|
|
| 955 |
main()
|
|
|
|
| 6 |
import plotly.express as px
|
| 7 |
from openai import OpenAI
|
| 8 |
|
| 9 |
+
# --- Streamlit 설정 ---
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="⏳ 대한민국 경제 발전사 주식 투자 게임",
|
| 12 |
+
page_icon="📈",
|
| 13 |
+
layout="wide",
|
| 14 |
+
initial_sidebar_state="expanded",
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# --- Custom CSS (스타일링) ---
|
| 18 |
+
st.markdown(
|
| 19 |
+
"""
|
| 20 |
+
<style>
|
| 21 |
+
/* 전체 폰트 (Nanum Gothic) */
|
| 22 |
+
@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700&display=swap');
|
| 23 |
+
body {
|
| 24 |
+
font-family: 'Nanum Gothic', sans-serif !important;
|
| 25 |
+
}
|
| 26 |
+
/* 탭 메뉴 스타일 */
|
| 27 |
+
.stTabs [data-baseweb="tab-list"] button[aria-selected="true"] {
|
| 28 |
+
background-color: #007bff !important;
|
| 29 |
+
color: white !important;
|
| 30 |
+
font-weight: bold;
|
| 31 |
+
}
|
| 32 |
+
.stTabs [data-baseweb="tab-list"] button {
|
| 33 |
+
background-color: #f0f2f6;
|
| 34 |
+
color: #333;
|
| 35 |
+
border-radius: 8px 8px 0 0;
|
| 36 |
+
padding: 0.75em 1em;
|
| 37 |
+
margin-bottom: -1px; /* border overlap */
|
| 38 |
+
}
|
| 39 |
+
/* 사이드바 스타일 */
|
| 40 |
+
[data-testid="stSidebar"] {
|
| 41 |
+
width: 350px !important;
|
| 42 |
+
background-color: #f8f9fa; /* Light gray sidebar background */
|
| 43 |
+
padding: 20px;
|
| 44 |
+
}
|
| 45 |
+
[data-testid="stSidebar"] h1, [data-testid="stSidebar"] h3 {
|
| 46 |
+
color: #212529; /* Dark gray sidebar headings */
|
| 47 |
+
}
|
| 48 |
+
[data-testid="stSidebar"] hr {
|
| 49 |
+
border-top: 1px solid #e0e0e0; /* Lighter sidebar hr */
|
| 50 |
+
}
|
| 51 |
+
/* Metric 스타일 */
|
| 52 |
+
.streamlit-metric-label {
|
| 53 |
+
font-size: 16px;
|
| 54 |
+
color: #4a4a4a;
|
| 55 |
+
}
|
| 56 |
+
.streamlit-metric-value {
|
| 57 |
+
font-size: 28px;
|
| 58 |
+
font-weight: bold;
|
| 59 |
+
}
|
| 60 |
+
/* 버튼 스타일 */
|
| 61 |
+
div.stButton > button {
|
| 62 |
+
background-color: #007bff;
|
| 63 |
+
color: white;
|
| 64 |
+
padding: 12px 24px;
|
| 65 |
+
font-size: 16px;
|
| 66 |
+
border-radius: 8px;
|
| 67 |
+
border: none;
|
| 68 |
+
box-shadow: 2px 2px 5px rgba(0,0,0,0.1); /* Soft shadow */
|
| 69 |
+
transition: background-color 0.3s ease;
|
| 70 |
+
}
|
| 71 |
+
div.stButton > button:hover {
|
| 72 |
+
background-color: #0056b3;
|
| 73 |
+
box-shadow: 2px 2px 7px rgba(0,0,0,0.15); /* Slightly stronger shadow on hover */
|
| 74 |
+
}
|
| 75 |
+
/* 보조 버튼 스타일 */
|
| 76 |
+
div.stButton > button.secondary-button {
|
| 77 |
+
background-color: #6c757d;
|
| 78 |
+
color: white;
|
| 79 |
+
padding: 10px 20px;
|
| 80 |
+
font-size: 14px;
|
| 81 |
+
border-radius: 6px;
|
| 82 |
+
border: none;
|
| 83 |
+
transition: background-color 0.3s ease;
|
| 84 |
+
}
|
| 85 |
+
div.stButton > button.secondary-button:hover {
|
| 86 |
+
background-color: #5a6268;
|
| 87 |
+
}
|
| 88 |
+
/* Expander 스타일 */
|
| 89 |
+
.streamlit-expanderHeader {
|
| 90 |
+
font-weight: bold;
|
| 91 |
+
color: #212529;
|
| 92 |
+
border-bottom: 1px solid #e0e0e0;
|
| 93 |
+
padding-bottom: 8px;
|
| 94 |
+
margin-bottom: 15px;
|
| 95 |
+
}
|
| 96 |
+
/* Dataframe 스타일 */
|
| 97 |
+
.dataframe {
|
| 98 |
+
border: 1px solid #e0e0e0;
|
| 99 |
+
border-radius: 8px;
|
| 100 |
+
padding: 12px;
|
| 101 |
+
box-shadow: 2px 2px 5px rgba(0,0,0,0.05); /* Very subtle shadow */
|
| 102 |
+
}
|
| 103 |
+
/* Info, Success, Error, Warning Box 스타일 */
|
| 104 |
+
div.stInfo, div.stSuccess, div.stError, div.stWarning {
|
| 105 |
+
border-radius: 8px;
|
| 106 |
+
padding: 15px;
|
| 107 |
+
margin-bottom: 15px;
|
| 108 |
+
box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
|
| 109 |
+
}
|
| 110 |
+
div.stInfo {
|
| 111 |
+
background-color: #e7f3ff;
|
| 112 |
+
border-left: 5px solid #007bff;
|
| 113 |
+
}
|
| 114 |
+
div.stSuccess {
|
| 115 |
+
background-color: #e6f7ec;
|
| 116 |
+
border-left: 5px solid #28a745;
|
| 117 |
+
}
|
| 118 |
+
div.stError {
|
| 119 |
+
background-color: #fdeded;
|
| 120 |
+
border-left: 5px solid #dc3545;
|
| 121 |
+
}
|
| 122 |
+
div.stWarning {
|
| 123 |
+
background-color: #fffbe6;
|
| 124 |
+
border-left: 5px solid #ffc107;
|
| 125 |
+
}
|
| 126 |
+
/* Toast message 스타일 */
|
| 127 |
+
div.streamlit-toast-container {
|
| 128 |
+
z-index: 10000; /* Toast를 항상 맨 위에 표시 */
|
| 129 |
+
}
|
| 130 |
+
div[data-testid="stToast"] {
|
| 131 |
+
border-radius: 8px;
|
| 132 |
+
padding: 15px;
|
| 133 |
+
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
|
| 134 |
+
}
|
| 135 |
+
</style>
|
| 136 |
+
""",
|
| 137 |
+
unsafe_allow_html=True,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# --- API 키 설정 ---
|
| 141 |
if "OPENAI_API_KEY" not in os.environ:
|
| 142 |
+
st.error(
|
| 143 |
+
"OPENAI_API_KEY 환경 변수가 설정되지 않았습니다. Hugging Face Secrets 또는 환경 변수에 API 키를 설정해주세요."
|
| 144 |
+
)
|
| 145 |
st.stop()
|
| 146 |
+
|
| 147 |
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
| 148 |
|
| 149 |
# --- 세션 상태 초기화 ---
|
|
|
|
| 152 |
if "portfolio" not in st.session_state:
|
| 153 |
st.session_state["portfolio"] = {"cash": 10000000, "stocks": {}}
|
| 154 |
if "stocks" not in st.session_state:
|
| 155 |
+
st.session_state["stocks"] = {} # Decade별로 stocks 데이터를 초기화하므로, 여기서는 빈 딕셔너리로 시작
|
| 156 |
if "news_analysis_results" not in st.session_state:
|
| 157 |
st.session_state["news_analysis_results"] = {}
|
| 158 |
+
if "messages" not in st.session_state:
|
| 159 |
+
st.session_state["messages"] = []
|
| 160 |
+
if "daily_news" not in st.session_state:
|
| 161 |
+
st.session_state["daily_news"] = None
|
| 162 |
+
if "previous_daily_news" not in st.session_state:
|
| 163 |
+
st.session_state["previous_daily_news"] = None
|
| 164 |
+
if "news_date" not in st.session_state:
|
| 165 |
+
st.session_state["news_date"] = None
|
| 166 |
+
if "news_meanings" not in st.session_state:
|
| 167 |
+
st.session_state["news_meanings"] = {}
|
| 168 |
+
if (
|
| 169 |
+
"ai_news_analysis_output" not in st.session_state
|
| 170 |
+
):
|
| 171 |
+
st.session_state["ai_news_analysis_output"] = {}
|
| 172 |
+
if "day_count" not in st.session_state:
|
| 173 |
+
st.session_state["day_count"] = 1
|
| 174 |
+
if "decade_count" not in st.session_state: # Decade 카운트 추가
|
| 175 |
+
st.session_state["decade_count"] = 1950 # 시작 년도 설정
|
| 176 |
+
if "sector_news_impact" not in st.session_state:
|
| 177 |
+
st.session_state["sector_news_impact"] = {}
|
| 178 |
+
if 'buy_confirm' not in st.session_state:
|
| 179 |
+
st.session_state['buy_confirm'] = False
|
| 180 |
+
if 'sell_confirm' not in st.session_state:
|
| 181 |
+
st.session_state['sell_confirm'] = False
|
| 182 |
+
if 'difficulty_level' not in st.session_state:
|
| 183 |
+
st.session_state['difficulty_level'] = "중학생" # 기본 난이도 중학생으로 변경
|
| 184 |
+
|
| 185 |
+
# --- 주식 데이터 초기화 함수 (Decade별) ---
|
| 186 |
+
def initialize_stocks_for_decade(decade):
|
| 187 |
+
if decade == 1950:
|
| 188 |
+
return {
|
| 189 |
+
"재건": {
|
| 190 |
+
"대한방직": {"current_price": random.randint(100, 200), "price_history": [], "description": "6.25 전쟁 이후 재건 시대, 섬유 산업의 부흥을 이끈 대표 기업."},
|
| 191 |
+
"조선맥주": {"current_price": random.randint(50, 100), "price_history": [], "description": "국민들의 갈증을 해소하며 성장한 맥주 회사. 현재는 하이트진로."},
|
| 192 |
+
"락희화학": {"current_price": random.randint(80, 150), "price_history": [], "description": "플라스틱 산업의 선구자, 럭키화학! 현재 LG화학의 모태."},
|
| 193 |
+
},
|
| 194 |
+
"농업": {
|
| 195 |
+
"경성고무": {"current_price": random.randint(30, 60), "price_history": [], "description": "농업용 고무 제품 생산. 생활 필수품 수요 증가로 성장."},
|
| 196 |
+
"고려제당": {"current_price": random.randint(40, 80), "price_history": [], "description": "설탕, 밀가루 등 기초 식량 산업. CJ제일제당의 전신."},
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
elif decade == 1960:
|
| 200 |
+
return {
|
| 201 |
+
"경공업": {
|
| 202 |
+
"삼성물산": {"current_price": random.randint(300, 600), "price_history": [], "description": "수출 주도 경제 성장 견인. 종합상사로 해외 시장 개척."},
|
| 203 |
+
"선경직물": {"current_price": random.randint(250, 500), "price_history": [], "description": "Made in Korea 신화의 주역. 섬유 수출의 선두 주자, 현재 SK네트웍스."},
|
| 204 |
+
"태평양화학": {"current_price": random.randint(200, 400), "price_history": [], "description": "화장품 산업의 태동. ABC 크림으로 화장품 대중화, 아모레퍼시픽."},
|
| 205 |
+
},
|
| 206 |
+
"식품": {
|
| 207 |
+
"신동방": {"current_price": random.randint(150, 300), "price_history": [], "description": "라면, 스낵 등 인스턴트 식품 인기. 농심의 옛 이름."},
|
| 208 |
+
"미원": {"current_price": random.randint(120, 250), "price_history": [], "description": "MSG 조미료 '미원'으로 식탁 변화. 대상그룹의 모태."},
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
elif decade == 1970:
|
| 212 |
+
return {
|
| 213 |
+
"중화학공업": {
|
| 214 |
+
"포항제철": {"current_price": random.randint(800, 1500), "price_history": [], "description": "산업화의 쌀, 철강 생산. 국가 경제 발전의 핵심 동력, 현재 POSCO홀딩스."},
|
| 215 |
+
"현대조선": {"current_price": random.randint(700, 1300), "price_history": [], "description": "수출 효자 산업, 조선업. 세계적인 조선 강국으로 발돋움."},
|
| 216 |
+
"기아산업": {"current_price": random.randint(600, 1200), "price_history": [], "description": "자동차 산업 육성 정책 수혜. 브리사, 승용차 생산 시작, 현재 기아."},
|
| 217 |
+
"금성사": {"current_price": random.randint(500, 1000), "price_history": [], "description": "가전 제품 국산화. 흑백 TV, 냉장고 생산, 현재 LG전자."},
|
| 218 |
+
},
|
| 219 |
+
"건설": {
|
| 220 |
+
"현대건설": {"current_price": random.randint(400, 800), "price_history": [], "description": "중동 건설 붐 주도. 해외 건설 시장 진출, 현재 현대건설."},
|
| 221 |
+
"대림산업": {"current_price": random.randint(350, 700), "price_history": [], "description": "국내 건설 시장 성장. 아파트 건설 붐, 현재 DL이앤씨."},
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
elif decade == 1980:
|
| 225 |
+
return {
|
| 226 |
+
"전자": {
|
| 227 |
+
"삼성전자": {"current_price": random.randint(1500, 3000), "price_history": [], "description": "반도체 산업 투자 확대. D램 개발, 세계적인 IT 기업으로 성장."},
|
| 228 |
+
"금성사": {"current_price": random.randint(1200, 2500), "price_history": [], "description": "컬러 TV, VCR 생산. 가전 제품 수출 증가, 현재 LG전자."},
|
| 229 |
+
"대우전자": {"current_price": random.randint(1000, 2000), "price_history": [], "description": "전자 산업 경쟁 심화. '탱크주의'로 유명, 현재 위니아."},
|
| 230 |
+
},
|
| 231 |
+
"자동차": {
|
| 232 |
+
"현대자동차": {"current_price": random.randint(1300, 2700), "price_history": [], "description": "포니, 엑셀 수출. 자동차 대중화 시대 개막, 현재 현대자동차."},
|
| 233 |
+
"기아자동차": {"current_price": random.randint(1100, 2400), "price_history": [], "description": "프라이드 출시. 소형차 시장 확대, 현재 기아."},
|
| 234 |
+
"대우자동차": {"current_price": random.randint(900, 1800), "price_history": [], "description": "르망 출시. 다양한 차종 개발 경쟁, 현재 한국GM."},
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
elif decade == 1990:
|
| 238 |
+
return {
|
| 239 |
+
"정보통신": {
|
| 240 |
+
"한국통신": {"current_price": random.randint(2500, 5000), "price_history": [], "description": "통신 시장 개방 및 민영화. 초고속 인터넷 서비스 시작, 현재 KT."},
|
| 241 |
+
"SK텔레콤": {"current_price": random.randint(2300, 4500), "price_history": [], "description": "이동통신 서비스 확산. 디지털 이동통신 'CDMA' 상용화, 현재 SK텔레콤."},
|
| 242 |
+
"LG텔레콤": {"current_price": random.randint(2000, 4000), "price_history": [], "description": "PCS 서비스 경쟁. 이동통신 시장 경쟁 심화, 현재 LG유플러스."},
|
| 243 |
+
"데이콤": {"current_price": random.randint(1800, 3500), "price_history": [], "description": "국제전화, 시외전화 서비스. 통신 시장 다변화, 현재 LG유플러스."},
|
| 244 |
+
},
|
| 245 |
+
"금융": {
|
| 246 |
+
"국민은행": {"current_price": random.randint(1600, 3200), "price_history": [], "description": "금융 시장 자유화. 은행 경쟁 심화, 현재 KB국민은행."},
|
| 247 |
+
"신한은행": {"current_price": random.randint(1400, 2800), "price_history": [], "description": "IMF 외환 위기 속에서 성장. M&A 통해 몸집 불린 은행, 현재 신한은행."},
|
| 248 |
+
"하나은행": {"current_price": random.randint(1300, 2600), "price_history": [], "description": "외환 은행 강자. 국제 금융 업무 특화, 현재 하나은행."},
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
elif decade == 2000:
|
| 252 |
+
return {
|
| 253 |
+
"IT": {
|
| 254 |
+
"네이버": {"current_price": random.randint(4000, 8000), "price_history": [], "description": "인터넷 시대 개막. 검색 포털 1위, IT 강국 코리아."},
|
| 255 |
+
"다음": {"current_price": random.randint(3500, 7000), "price_history": [], "description": "Daum 카페, 이메일 서비스 인기. 네이버와 경쟁."},
|
| 256 |
+
"NHN": {"current_price": random.randint(3000, 6000), "price_history": [], "description": "온라인 게임 시장 성장. '한게임' 포털 운영, 현재 NHN."},
|
| 257 |
+
"싸이월드": {"current_price": random.randint(2500, 5000), "price_history": [], "description": "소셜 미디어 열풍. 미니홈피, 아바타 서비스 인기."},
|
| 258 |
+
},
|
| 259 |
+
"엔터테인먼트": {
|
| 260 |
+
"SM엔터테인먼트": {"current_price": random.randint(2000, 4000), "price_history": [], "description": "한류 열풍 시작. BoA, 동방신기 등 아이돌 그룹 인기."},
|
| 261 |
+
"YG엔터테인먼트": {"current_price": random.randint(1800, 3500), "price_history": [], "description": "힙합 음악 대중화. 빅뱅, 2NE1 등 개성 강한 그룹."},
|
| 262 |
+
"JYP엔터테인먼트": {"current_price": random.randint(1600, 3200), "price_history": [], "description": "댄스 음악 강세. god, 원더걸스 등 국민 그룹 배출."},
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
elif decade == 2010:
|
| 266 |
+
return {
|
| 267 |
+
"플랫폼": {
|
| 268 |
+
"카카오": {"current_price": random.randint(6000, 12000), "price_history": [], "description": "모바일 메신저 시대. 카카오톡, 국민 앱으로 성장."},
|
| 269 |
+
"네이버": {"current_price": random.randint(5500, 11000), "price_history": [], "description": "모바일 플랫폼 강화. 라인 메신저 해외 진출."},
|
| 270 |
+
"쿠팡": {"current_price": random.randint(5000, 10000), "price_history": [], "description": "이커머스 시장 급성장. 로켓 배송으로 온라인 쇼핑 혁신."},
|
| 271 |
+
"배달의민족": {"current_price": random.randint(4500, 9000), "price_history": [], "description": "O2O 서비스 확대. 배달 앱 시장 선점, 현재 우아한형제들."},
|
| 272 |
+
},
|
| 273 |
+
"바이오": {
|
| 274 |
+
"셀트리온": {"current_price": random.randint(4000, 8000), "price_history": [], "description": "바이오시밀러 개발 성공. 바이오 의약품 수출, K-바이오."},
|
| 275 |
+
"삼성바이오로직스": {"current_price": random.randint(3500, 7000), "price_history": [], "description": "바이오 의약품 생산 대행. CMO 사업 성장, 바이오 산업 육성."},
|
| 276 |
+
"메디톡스": {"current_price": random.randint(3000, 6000), "price_history": [], "description": "보톡스 국산화 성공. 미용 의료 시장 확대."},
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
elif decade == 2020:
|
| 280 |
+
return {
|
| 281 |
+
"미래산업": {
|
| 282 |
+
"LG에너지솔루션": {"current_price": random.randint(8000, 16000), "price_history": [], "description": "전기차 배터리 시장 선도. 친환경 에너지 시대 개막."},
|
| 283 |
+
"SK하이닉스": {"current_price": random.randint(7000, 14000), "price_history": [], "description": "반도체 슈퍼 호황. 메모리 반도체 수요 폭증, IT 강국 위상."},
|
| 284 |
+
"현대자동차": {"current_price": random.randint(6500, 13000), "price_history": [], "description": "전기차, 수소차 개발 박차. 미래 모빌리티 전환."},
|
| 285 |
+
"카카오뱅크": {"current_price": random.randint(6000, 12000), "price_history": [], "description": "디지털 금융 혁신. 인터넷 전문 은행 시대 개막."},
|
| 286 |
+
"하이브": {"current_price": random.randint(5500, 11000), "price_history": [], "description": "K-팝 세계화. BTS, 블랙핑크 글로벌 팬덤 확보."},
|
| 287 |
+
},
|
| 288 |
+
"플랫폼": { # 2020년대 플랫폼 기업들은 미래산업 섹터에도 포함될 수 있지만, 분리하여 표현
|
| 289 |
+
"네이버": {"current_price": random.randint(5000, 10000), "price_history": [], "description": "AI, 클라우드 등 신기술 투자 확대. 플랫폼 경쟁 심화."},
|
| 290 |
+
"카카오": {"current_price": random.randint(4500, 9000), "price_history": [], "description": "ESG 경영 강화. 사회적 책임 강조, 플랫폼 규제."},
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
else: # 2030년 이후를 위한 기본 데이터 (현재 2020년대 데이터와 유사하게 설정)
|
| 294 |
+
return {
|
| 295 |
+
"미래산업": {
|
| 296 |
+
"LG에너지솔루션": {"current_price": random.randint(8000, 16000), "price_history": [], "description": "전기차 배터리 시장 선도. 친환경 에너지 시대 개막."},
|
| 297 |
+
"SK하이닉스": {"current_price": random.randint(7000, 14000), "price_history": [], "description": "반도체 슈퍼 호황. 메모리 반도체 수요 폭증, IT 강국 위상."},
|
| 298 |
+
"현대자동차": {"current_price": random.randint(6500, 13000), "price_history": [], "description": "전기차, 수소차 개발 박차. 미래 모빌리티 전환."},
|
| 299 |
+
"카카오뱅크": {"current_price": random.randint(6000, 12000), "price_history": [], "description": "디지털 금융 혁신. 인터넷 전문 은행 시대 개막."},
|
| 300 |
+
"하이브": {"current_price": random.randint(5500, 11000), "price_history": [], "description": "K-팝 세계화. BTS, 블랙핑크 글로벌 팬덤 확보."},
|
| 301 |
+
},
|
| 302 |
+
"플랫폼": { # 2020년대 플랫폼 기업들은 미래산업 섹터에도 포함될 수 있지만, 분리하여 표현
|
| 303 |
+
"네이버": {"current_price": random.randint(5000, 10000), "price_history": [], "description": "AI, 클라우드 등 신기술 투자 확대. 플랫폼 경쟁 심화."},
|
| 304 |
+
"카카오": {"current_price": random.randint(4500, 9000), "price_history": [], "description": "ESG 경영 강화. 사회적 책임 강조, 플랫폼 규제."},
|
| 305 |
+
}
|
| 306 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
+
# --- 뉴스 생성 함수 ---
|
| 309 |
def generate_news():
|
| 310 |
decade_count = st.session_state["decade_count"]
|
| 311 |
difficulty_level = st.session_state['difficulty_level']
|
| 312 |
|
| 313 |
+
difficulty_prompt_map = {
|
| 314 |
+
"초등학생": {
|
| 315 |
+
"level_desc": "초등학생 5~6학년 수준",
|
| 316 |
+
"sentence_count": "8~10",
|
| 317 |
+
"vocabulary_level": "쉬운 어휘와 짧은 문장",
|
| 318 |
+
"inference_level": "단순하고 명확한 정보",
|
| 319 |
+
},
|
| 320 |
+
"중학생": {
|
| 321 |
+
"level_desc": "중학생 1~3학년 수준",
|
| 322 |
+
"sentence_count": "10~12",
|
| 323 |
+
"vocabulary_level": "일상적인 어휘와 약간의 전문 용어",
|
| 324 |
+
"inference_level": "일반적인 경제 흐름과 관련된 추론",
|
| 325 |
+
},
|
| 326 |
+
"고등학생": {
|
| 327 |
+
"level_desc": "고등학생 1~3학년 수준",
|
| 328 |
+
"sentence_count": "12~15",
|
| 329 |
+
"vocabulary_level": "다양한 어휘와 경제 전문 용어 포함",
|
| 330 |
+
"inference_level": "심층적인 경제 분석 및 다각적인 추론",
|
| 331 |
+
},
|
| 332 |
}
|
| 333 |
+
|
| 334 |
level_config = difficulty_prompt_map.get(difficulty_level, difficulty_prompt_map["중학생"])
|
| 335 |
+
|
| 336 |
level_prompt = level_config["level_desc"]
|
| 337 |
sentence_count = level_config["sentence_count"]
|
| 338 |
vocabulary_level = level_config["vocabulary_level"]
|
| 339 |
inference_level = level_config["inference_level"]
|
| 340 |
|
| 341 |
+
decade_keyword_map = { # Decade별 경제 뉴스 키워드 설정
|
| 342 |
1950: "1950년대 한국전쟁 이후 재건, 원조 경제, 농업, 경공업",
|
| 343 |
1960: "1960년대 경제 개발 5개년 계획, 수출 주도 성장, 경공업 육성",
|
| 344 |
1970: "1970년대 중화학공업 육성, 오일 쇼크, 새마을 운동, 건설업",
|
|
|
|
| 348 |
2010: "2010년대 스마트폰 보급, 소셜 미디어, 핀테크, 공유 경제, 바이오시밀러",
|
| 349 |
2020: "2020년대 코로나19 팬데믹, 디지털 전환, 플랫폼 경제, ESG 경영, 미래 모빌리티, 인공지능",
|
| 350 |
}
|
| 351 |
+
decade_keywords = decade_keyword_map.get(decade_count, "2020년대 미래산업, 플랫폼 경제") # 기본 키워드 설정
|
| 352 |
|
| 353 |
prompt = f"""
|
| 354 |
지시:
|
| 355 |
{level_prompt}에 맞춰서, **{decade_count}년대 대한민국 경제**와 관련된 뉴스 기사 5개를 생성해주세요.
|
| 356 |
**뉴스 주제**는 "{decade_keywords}" 키워드를 참고하여 시대적 특징을 반영해주세요.
|
| 357 |
각 기사는 {sentence_count}문장 정도로 작성하고, {vocabulary_level}를 사용하여 학생들이 이해하기 쉬워야 합니다.
|
| 358 |
+
학생들이 뉴스를 읽고 {inference_level} 수준에서 어떤 회사가 유망할지 또는 쇠락할지 스스로 추론할 수 있도록 {decade_count}년대 대한민국 경제 상황이나 산업 동향에 대한 뉴스를 만들어주세요.
|
| 359 |
+
특정 회사 이름이나 주식 종목을 직접적으로 언급하지 마세요.
|
| 360 |
긍정적 뉴스, 부정적 뉴스, 중립적 뉴스 다양하게 생성하세요.(긍정, 부정, 중립 이라는 말은 표시하지 마세요.)
|
| 361 |
뉴스에 따라 주식이 상승하기도 하고 하락하기도 할 수 있습니다.
|
| 362 |
각 뉴스 기사는 "## 뉴스 [번호]" 로 시작해주세요. (예: ## 뉴스 1, ## 뉴스 2 ...)
|
|
|
|
| 366 |
chat_session = st.session_state["chat_session"]
|
| 367 |
messages = [{"role": "user", "content": prompt}]
|
| 368 |
|
| 369 |
+
try: # OpenAI API 호출 예외 처리
|
| 370 |
response = client.chat.completions.create(
|
| 371 |
model="gpt-4o-mini",
|
| 372 |
messages=messages,
|
|
|
|
| 389 |
|
| 390 |
return news_articles[:5]
|
| 391 |
|
| 392 |
+
except Exception as e: # API 호출 에러 발생 시 사용자에게 알림
|
| 393 |
+
st.error(f"뉴스 생성 중 오류가 발생했습니다: {e}")
|
| 394 |
+
return [] # 오류 발생 시 빈 리스트 반환
|
| 395 |
|
| 396 |
+
# explain_daily_news_meanings 함수 (기존 코드와 동일)
|
| 397 |
def explain_daily_news_meanings(daily_news):
|
| 398 |
if daily_news is None:
|
| 399 |
return {}
|
| 400 |
|
| 401 |
difficulty_level = st.session_state['difficulty_level']
|
| 402 |
+
difficulty_prompt_map = {
|
| 403 |
"초등학생": "초등학생 5~6학년",
|
| 404 |
"중학생": "중학생 1~3학년",
|
| 405 |
"고등학생": "고등학생 1~3학년",
|
|
|
|
| 414 |
|
| 415 |
**지시:**
|
| 416 |
위 신문 기사의 핵심 의미를 {level_prompt}이 이해하기 쉽게 3문장 이내로 요약해서 "해설: " 다음에 설명해주세요.
|
| 417 |
+
그리고 이 뉴스와 관련된 주식 섹터 1~2개를 쉼표로 구분해서 "관련 섹터: " 다음에 알려주세요. 관련 섹터가 없다면 "관련 섹터: 없음" 이라고 해주세요.
|
| 418 |
|
| 419 |
뉴스 의미 해설:
|
| 420 |
"""
|
| 421 |
chat_session = st.session_state["chat_session"]
|
| 422 |
messages = [{"role": "user", "content": prompt}]
|
| 423 |
+
try:
|
| 424 |
response = client.chat.completions.create(
|
| 425 |
model="gpt-4o-mini",
|
| 426 |
messages=messages,
|
|
|
|
| 436 |
meaning_text = response.choices[0].message.content.strip()
|
| 437 |
|
| 438 |
explanation = ""
|
| 439 |
+
related_sectors = []
|
| 440 |
|
| 441 |
if "해설:" in meaning_text:
|
| 442 |
explanation_start_index = meaning_text.find("해설:") + len("해설:")
|
| 443 |
+
explanation_end_index = meaning_text.find("관련 섹터:")
|
| 444 |
+
if explanation_end_index != -1:
|
| 445 |
+
explanation = meaning_text[explanation_start_index:explanation_end_index].strip()
|
| 446 |
+
else:
|
| 447 |
+
explanation = meaning_text[explanation_start_index:].strip()
|
| 448 |
|
| 449 |
+
if "관련 섹터:" in meaning_text:
|
| 450 |
+
related_sectors_str = meaning_text.split("관련 섹터:")[1].strip()
|
| 451 |
+
if related_sectors_str.lower() != "없음":
|
| 452 |
+
related_sectors = [sector.strip() for sector in related_sectors_str.split(',')]
|
| 453 |
+
else:
|
| 454 |
+
related_sectors = [] # "없음" explicitly means empty list
|
| 455 |
+
|
| 456 |
+
meanings[str(i + 1)] = {"explanation": explanation, "sectors": related_sectors}
|
| 457 |
+
|
| 458 |
+
except Exception as e: # google.api_core.exceptions.ResourceExhausted -> Exception으로 변경
|
| 459 |
st.error(
|
| 460 |
f"API 할당량 초과 오류가 발생했습니다. 잠시 후 다시 시도해주세요. 오류 메시지: {e}"
|
| 461 |
)
|
|
|
|
| 463 |
time.sleep(1)
|
| 464 |
return meanings
|
| 465 |
|
| 466 |
+
# buy_stock, sell_stock 함수 (기존 코드와 동일)
|
| 467 |
+
def buy_stock(stock_name, quantity, sector):
|
| 468 |
+
if (
|
| 469 |
+
sector not in st.session_state["stocks"]
|
| 470 |
+
or stock_name not in st.session_state["stocks"][sector]
|
| 471 |
+
):
|
| 472 |
st.session_state["messages"].append(
|
| 473 |
{"type": "error", "text": "존재하지 않는 주식 종목입니다."}
|
| 474 |
)
|
| 475 |
return
|
| 476 |
|
| 477 |
+
if quantity <= 0:
|
| 478 |
st.session_state["messages"].append(
|
| 479 |
{"type": "error", "text": "매수 수량은 1주 이상이어야 합니다."}
|
| 480 |
)
|
| 481 |
return
|
| 482 |
|
| 483 |
+
stock_price = st.session_state["stocks"][sector][stock_name]["current_price"]
|
| 484 |
max_quantity = st.session_state["portfolio"]["cash"] // stock_price
|
| 485 |
+
if quantity > max_quantity:
|
| 486 |
st.session_state["messages"].append(
|
| 487 |
{
|
| 488 |
"type": "error",
|
|
|
|
| 497 |
|
| 498 |
total_price = stock_price * quantity
|
| 499 |
|
| 500 |
+
if st.session_state["portfolio"]["cash"] >= total_price:
|
| 501 |
st.session_state["portfolio"]["cash"] -= total_price
|
| 502 |
portfolio_stocks = st.session_state["portfolio"]["stocks"]
|
| 503 |
if (
|
|
|
|
| 525 |
f"{stock_name} {quantity}주 매수 완료. 총 {total_price:,.0f}원 소요.", icon="✅"
|
| 526 |
)
|
| 527 |
st.session_state['buy_confirm'] = False
|
| 528 |
+
else:
|
| 529 |
st.session_state["messages"].append(
|
| 530 |
{"type": "error", "text": "잔액이 부족합니다."}
|
| 531 |
)
|
|
|
|
| 533 |
st.error(f"잔액이 부족합니다. (최대 {max_quantity}주까지 매수 가능)")
|
| 534 |
st.session_state['buy_confirm'] = False
|
| 535 |
|
| 536 |
+
|
| 537 |
+
def sell_stock(stock_name, quantity):
|
| 538 |
if stock_name not in st.session_state["portfolio"]["stocks"]:
|
| 539 |
st.session_state["messages"].append(
|
| 540 |
{"type": "error", "text": "보유하고 있지 않은 주식입니다."}
|
|
|
|
| 562 |
return
|
| 563 |
|
| 564 |
stock_price = 0
|
| 565 |
+
stock_sector = ""
|
| 566 |
+
for sector, stocks in st.session_state["stocks"].items():
|
| 567 |
if stock_name in stocks:
|
| 568 |
stock_price = stocks[stock_name]["current_price"]
|
| 569 |
stock_sector = sector
|
| 570 |
break
|
|
|
|
| 571 |
|
| 572 |
if stock_price == 0:
|
| 573 |
st.session_state["messages"].append(
|
|
|
|
| 593 |
st.success(f"{stock_name} {quantity}주 매도 완료. 총 {sell_price:,.0f}원 획득.")
|
| 594 |
st.session_state['sell_confirm'] = False
|
| 595 |
|
| 596 |
+
# update_stock_prices 함수 (Decade 경제 상황 반영 추가)
|
| 597 |
def update_stock_prices():
|
| 598 |
if not st.session_state["daily_news"]:
|
| 599 |
return
|
| 600 |
|
| 601 |
+
sector_impacts = {sector: 0 for sector in st.session_state["stocks"]}
|
| 602 |
decade_count = st.session_state["decade_count"]
|
| 603 |
+
decade_economic_conditions = { # Decade별 경제 상황 반영 (수치 예시는 임의)
|
| 604 |
+
1950: {"growth_rate": 0.02, "volatility": 0.03}, # 재건 시대, 낮은 성장률, 변동성 중간
|
| 605 |
+
1960: {"growth_rate": 0.05, "volatility": 0.02}, # 경제 개발, 높은 성장률, 변동성 낮음
|
| 606 |
+
1970: {"growth_rate": 0.08, "volatility": 0.05}, # 중화학 공업, 고성장, 오일쇼크 변동성 증가
|
| 607 |
+
1980: {"growth_rate": 0.06, "volatility": 0.04}, # 안정 성장, 변동성 중간
|
| 608 |
+
1990: {"growth_rate": 0.03, "volatility": 0.1}, # IMF 외환위기, 낮은 성장률, 높은 변동성
|
| 609 |
+
2000: {"growth_rate": 0.04, "volatility": 0.07}, # IT 버블, 성장률 회복, 변동성 증가
|
| 610 |
+
2010: {"growth_rate": 0.03, "volatility": 0.05}, # 저성장 시대, 변동성 중간
|
| 611 |
+
2020: {"growth_rate": 0.02, "volatility": 0.08}, # 코로나19, 저성장, 높은 변동성
|
| 612 |
}
|
| 613 |
+
economic_condition = decade_economic_conditions.get(decade_count, {"growth_rate": 0.03, "volatility": 0.06}) # 기본값 설정
|
| 614 |
+
|
| 615 |
+
for i, news_article in enumerate(st.session_state["daily_news"]):
|
| 616 |
+
news_meaning = st.session_state["news_meanings"].get(str(i + 1))
|
| 617 |
+
if news_meaning:
|
| 618 |
+
related_sectors = news_meaning.get("sectors", [])
|
| 619 |
+
news_explanation = news_meaning.get("explanation", "")
|
| 620 |
+
|
| 621 |
+
news_sentiment = 0
|
| 622 |
+
if "상승" in news_article or "성장" in news_article or "긍정적" in news_article or "유망" in news_article or "호황" in news_article:
|
| 623 |
+
news_sentiment = 1
|
| 624 |
+
elif "하락" in news_article or "감소" in news_article or "부정적" in news_article or "어려움" in news_article or "침체" in news_article or "위기" in news_article:
|
| 625 |
+
news_sentiment = -1
|
| 626 |
+
else:
|
| 627 |
+
news_sentiment = 0
|
| 628 |
+
|
| 629 |
+
for sector in related_sectors:
|
| 630 |
+
if sector in sector_impacts:
|
| 631 |
+
sector_impacts[sector] += news_sentiment * 0.03 # 뉴스 sentiment 영향도 감소
|
| 632 |
+
|
| 633 |
+
for sector in st.session_state["stocks"]:
|
| 634 |
+
sector_impact = sector_impacts[sector]
|
| 635 |
+
# 경제 상황 반영: 성장률, 변동성
|
| 636 |
+
base_change_rate = economic_condition["growth_rate"] # 기본 성장률 적용
|
| 637 |
+
volatility = economic_condition["volatility"] # 변동성 적용
|
| 638 |
+
change_rate = random.uniform(-volatility, volatility) + base_change_rate + sector_impact # 변동폭 증가, sector_impact 감소
|
| 639 |
+
|
| 640 |
+
for stock_name in st.session_state["stocks"][sector]:
|
| 641 |
+
change_rate = max(-0.2, min(0.2, change_rate)) # 변동폭 제한 (±20% 이내)
|
| 642 |
+
st.session_state["stocks"][sector][stock_name]["current_price"] *= (
|
| 643 |
+
1 + change_rate
|
| 644 |
+
)
|
| 645 |
+
st.session_state["stocks"][sector][stock_name]["current_price"] = max(
|
| 646 |
+
1, int(st.session_state["stocks"][sector][stock_name]["current_price"])
|
| 647 |
+
)
|
| 648 |
+
st.session_state["stocks"][sector][stock_name]["price_history"].append(
|
| 649 |
+
st.session_state["stocks"][sector][stock_name]["current_price"]
|
| 650 |
+
)
|
| 651 |
st.session_state["messages"].append({"type": "info", "text": "주가가 변동되었습니다."})
|
| 652 |
st.toast("주가가 변동되었습니다.", icon="📈")
|
| 653 |
st.info("주가가 변동되었습니다.")
|
| 654 |
+
st.session_state["sector_news_impact"] = sector_impacts
|
| 655 |
|
| 656 |
+
# display_portfolio, display_stock_prices, display_portfolio_table 함수 (기존 코드와 동일)
|
| 657 |
+
def display_portfolio():
|
| 658 |
portfolio = st.session_state["portfolio"]
|
| 659 |
cash = portfolio["cash"]
|
| 660 |
total_value = cash
|
|
|
|
| 663 |
quantity = stock_info["quantity"]
|
| 664 |
purchase_price = stock_info["purchase_price"]
|
| 665 |
current_price = 0
|
| 666 |
+
stock_sector = ""
|
| 667 |
+
for sector, stocks in st.session_state["stocks"].items():
|
| 668 |
if stock_name in stocks:
|
| 669 |
current_price = stocks[stock_name]["current_price"]
|
| 670 |
+
stock_sector = sector
|
| 671 |
break
|
|
|
|
|
|
|
|
|
|
| 672 |
if current_price != 0:
|
| 673 |
stock_value = current_price * quantity
|
| 674 |
total_value += stock_value
|
|
|
|
| 685 |
|
| 686 |
def display_stock_prices():
|
| 687 |
stocks_data = []
|
| 688 |
+
for sector, sector_stocks in st.session_state["stocks"].items():
|
| 689 |
+
for stock_name, stock_info in sector_stocks.items():
|
| 690 |
+
price_history = stock_info["price_history"]
|
| 691 |
+
daily_change_rate_str = " - " # 기본값
|
| 692 |
+
if len(price_history) >= 2:
|
| 693 |
+
previous_day_price = price_history[-2]
|
| 694 |
+
current_price = price_history[-1]
|
| 695 |
+
daily_change_rate = (current_price - previous_day_price) / previous_day_price * 100
|
| 696 |
+
daily_change_rate_str = f"{daily_change_rate:.2f}%"
|
| 697 |
+
|
| 698 |
+
stocks_data.append(
|
| 699 |
+
{
|
| 700 |
+
"종목": stock_name,
|
| 701 |
+
"섹터": sector,
|
| 702 |
+
"현재 주가": f"{stock_info['current_price']:,} 원",
|
| 703 |
+
"전일 대비": daily_change_rate_str, # 전일 대비 등락률 추가
|
| 704 |
+
"price_history": stock_info["price_history"],
|
| 705 |
+
"description": stock_info["description"],
|
| 706 |
+
}
|
| 707 |
+
)
|
| 708 |
stocks_df = pd.DataFrame(stocks_data)
|
| 709 |
+
st.dataframe(stocks_df[["섹터", "종목", "현재 주가", "전일 대비"]], hide_index=True) # "전일 대비" 컬럼 추가
|
| 710 |
|
| 711 |
selected_stock_all_info = st.selectbox(
|
| 712 |
"종목 선택 (기업 정보 및 주가 그래프)", stocks_df["종목"].tolist()
|
| 713 |
)
|
| 714 |
if selected_stock_all_info:
|
| 715 |
+
selected_stock_sector = stocks_df[
|
| 716 |
stocks_df["종목"] == selected_stock_all_info
|
| 717 |
]["섹터"].iloc[0]
|
| 718 |
col1_info, col2_graph = st.columns([1, 2])
|
|
|
|
| 720 |
with col1_info:
|
| 721 |
st.subheader("기업 정보")
|
| 722 |
st.info(
|
| 723 |
+
f"**{selected_stock_all_info} ({selected_stock_sector})**\n\n{st.session_state['stocks'][selected_stock_sector][selected_stock_all_info]['description']}"
|
| 724 |
)
|
| 725 |
|
| 726 |
+
with col2_graph:
|
| 727 |
st.subheader("주가 그래프")
|
| 728 |
price_history_df = pd.DataFrame(
|
| 729 |
{
|
| 730 |
"날짜": range(
|
| 731 |
1,
|
| 732 |
len(
|
| 733 |
+
st.session_state["stocks"][selected_stock_sector][
|
| 734 |
+
selected_stock_all_info
|
| 735 |
+
]["price_history"]
|
| 736 |
)
|
| 737 |
+ 1,
|
| 738 |
),
|
| 739 |
+
"주가": st.session_state["stocks"][selected_stock_sector][
|
| 740 |
+
selected_stock_all_info
|
| 741 |
+
]["price_history"],
|
| 742 |
}
|
| 743 |
)
|
| 744 |
fig = px.line(
|
| 745 |
price_history_df,
|
| 746 |
x="날짜",
|
| 747 |
y="주가",
|
| 748 |
+
title=f"{selected_stock_all_info} ({selected_stock_sector}) 주가 변동",
|
| 749 |
)
|
| 750 |
st.plotly_chart(fig)
|
| 751 |
else:
|
| 752 |
st.info("종목을 선택하여 기업 정보와 주가 그래프를 확인하세요.")
|
| 753 |
|
| 754 |
+
def display_portfolio_table():
|
| 755 |
portfolio = st.session_state["portfolio"]
|
| 756 |
if portfolio["stocks"]:
|
| 757 |
portfolio_data = []
|
|
|
|
| 763 |
quantity = stock_info["quantity"]
|
| 764 |
purchase_price = stock_info["purchase_price"]
|
| 765 |
current_price = 0
|
| 766 |
+
stock_sector = ""
|
| 767 |
+
for sector, stocks in st.session_state["stocks"].items():
|
| 768 |
if stock_name in stocks:
|
| 769 |
current_price = stocks[stock_name]["current_price"]
|
| 770 |
+
stock_sector = sector
|
| 771 |
break
|
|
|
|
|
|
|
| 772 |
|
| 773 |
if current_price == 0:
|
| 774 |
continue
|
|
|
|
| 786 |
portfolio_data.append(
|
| 787 |
{
|
| 788 |
"종목": stock_name,
|
| 789 |
+
"섹터": stock_sector,
|
| 790 |
"보유 수량": quantity,
|
| 791 |
"매수 단가": f"{purchase_price:,.0f} 원",
|
| 792 |
"현재가": f"{current_price:,.0f} 원",
|
|
|
|
| 795 |
"수익률": f"{profit_rate:.2f}%",
|
| 796 |
}
|
| 797 |
)
|
| 798 |
+
portfolio_data.append(
|
| 799 |
{
|
| 800 |
"종목": "현금",
|
| 801 |
"섹터": "-",
|
|
|
|
| 809 |
)
|
| 810 |
portfolio_df = pd.DataFrame(portfolio_data)
|
| 811 |
st.dataframe(portfolio_df, hide_index=True, height=350)
|
| 812 |
+
st.markdown(
|
| 813 |
f"""**현금 잔고:** {portfolio['cash']:,} 원
|
| 814 |
**📊 총 평가액:** {total_value:,.0f} 원
|
| 815 |
**🛒 총 매수 금액:** {total_purchase_value:,.0f} 원
|
|
|
|
| 820 |
st.info("보유 주식이 없습니다.")
|
| 821 |
|
| 822 |
# display_stock_glossary 함수 (기존 코드와 동일)
|
| 823 |
+
def display_stock_glossary():
|
| 824 |
glossary = {
|
| 825 |
"주식": "회사의 일부분을 나타내는 증서. 주식을 사면 회사의 주인이 되는 거예요.",
|
| 826 |
"주가": "주식 1개당 가격. 사람들이 주식을 사고팔 때 가격이 변해요.",
|
|
|
|
| 844 |
def main():
|
| 845 |
col_news, col_main_ui = st.columns([1, 2])
|
| 846 |
|
| 847 |
+
with col_news:
|
| 848 |
+
st.header(f"📰 {st.session_state['decade_count']}년대 뉴스") # Decade 표시
|
| 849 |
if st.button("뉴스 생성", use_container_width=True, key="news_gen_button"):
|
| 850 |
+
with st.spinner(f"{st.session_state['decade_count']}년대 뉴스 생성 중..."): # Decade 표시
|
| 851 |
current_daily_news = generate_news()
|
| 852 |
st.session_state["daily_news"] = current_daily_news
|
| 853 |
|
| 854 |
if st.session_state["daily_news"]:
|
| 855 |
+
st.subheader(f"{st.session_state['decade_count']}년대 뉴스") # Decade 표시
|
| 856 |
for i, news in enumerate(st.session_state["daily_news"]):
|
| 857 |
with st.expander(f"뉴스 {i+1}", expanded=False):
|
| 858 |
st.write(news)
|
|
|
|
| 860 |
if st.session_state["previous_daily_news"] and st.session_state[
|
| 861 |
"news_meanings"
|
| 862 |
]:
|
| 863 |
+
previous_decade = st.session_state['decade_count'] - 10 # 이전 Decade 계산
|
| 864 |
+
st.subheader(f"{previous_decade}년대 뉴스 해설") # 이전 Decade 표시
|
| 865 |
st.info("AI가 분석한 지난 뉴스 해설입니다.")
|
| 866 |
+
with st.expander(f"{previous_decade}년대 뉴스 해설 보기", expanded=False): # 이전 Decade 표시
|
| 867 |
if st.session_state["news_meanings"]:
|
| 868 |
for i, meaning_data in st.session_state["news_meanings"].items():
|
| 869 |
st.markdown(f"**뉴스 {i}**:")
|
| 870 |
+
st.markdown(f"**AI 해설:** {meaning_data['explanation']}") # Markdown 으로 변경
|
| 871 |
+
if meaning_data['sectors']:
|
| 872 |
+
st.markdown(f"**관련 섹터:** {', '.join(meaning_data['sectors'])}") # Markdown 으로 변경
|
| 873 |
+
else:
|
| 874 |
+
st.markdown("**관련 섹터:** 없음") # Markdown 으로 변경
|
| 875 |
else:
|
| 876 |
st.info("지난 뉴스에 대한 해설이 없습니다.")
|
| 877 |
|
|
|
|
| 879 |
st.info("뉴스 생성 버튼을 눌러 오늘의 뉴스를 받아보세요.")
|
| 880 |
|
| 881 |
with col_main_ui:
|
| 882 |
+
menu = st.tabs(
|
| 883 |
['현재 주가', '내 포트폴리오', '주식 매수', '주식 매도', '지난 뉴스 해설']
|
| 884 |
)
|
| 885 |
|
| 886 |
+
with menu[0]:
|
| 887 |
st.subheader("📈 현재 주가 및 기업 정보")
|
| 888 |
st.markdown("주식 시장의 현재 가격과 기업 정보를 확인하세요.")
|
| 889 |
display_stock_prices()
|
| 890 |
|
| 891 |
+
with menu[1]:
|
| 892 |
st.subheader("📊 내 포트폴리오")
|
| 893 |
st.markdown("현재 보유 중인 주식과 자산을 확인하세요.")
|
| 894 |
display_portfolio_table()
|
| 895 |
|
| 896 |
+
with menu[2]:
|
| 897 |
st.subheader("💰 주식 매수")
|
| 898 |
st.markdown("AI 예측과 뉴스 분석을 바탕으로 주식을 매수해보세요.")
|
| 899 |
+
sector_names = list(st.session_state["stocks"].keys())
|
| 900 |
+
selected_sector_buy = st.selectbox("매수 섹터 선택:", sector_names)
|
| 901 |
+
stock_names_in_sector = list(
|
| 902 |
+
st.session_state["stocks"][selected_sector_buy].keys()
|
| 903 |
+
)
|
| 904 |
+
selected_stock_buy = st.selectbox("매수 종목 선택:", stock_names_in_sector)
|
| 905 |
|
| 906 |
+
stock_price_buy = st.session_state["stocks"][selected_sector_buy][
|
| 907 |
+
selected_stock_buy
|
| 908 |
+
]["current_price"]
|
| 909 |
st.info(f"**{selected_stock_buy}** 현재 주가: {stock_price_buy:,.0f}원")
|
| 910 |
quantity_buy = st.number_input(
|
| 911 |
"매수 수량 (주):", min_value=1, value=1, step=1
|
| 912 |
)
|
| 913 |
|
| 914 |
+
if not st.session_state['buy_confirm']:
|
| 915 |
if st.button("주식 매수", use_container_width=True, key='buy_button_confirm'):
|
| 916 |
st.session_state['buy_confirm'] = True
|
| 917 |
else:
|
|
|
|
| 919 |
col_confirm, col_cancel = st.columns([1, 1])
|
| 920 |
with col_confirm:
|
| 921 |
if st.button("✅ 매수 확인", use_container_width=True, key='buy_confirm_button'):
|
| 922 |
+
buy_stock(selected_stock_buy, quantity_buy, selected_sector_buy)
|
| 923 |
|
| 924 |
with col_cancel:
|
| 925 |
if st.button("❌ 매수 취소", use_container_width=True, key='buy_cancel_button', type='secondary'):
|
| 926 |
st.session_state['buy_confirm'] = False
|
| 927 |
st.info("매수를 취소했습니다.")
|
| 928 |
|
| 929 |
+
with menu[3]:
|
| 930 |
st.subheader("📉 주식 매도")
|
| 931 |
st.markdown("보유 중인 주식을 판매하고 수익을 실현해보세요.")
|
| 932 |
if st.session_state["portfolio"]["stocks"]:
|
| 933 |
stock_names_sell = list(st.session_state["portfolio"]["stocks"].keys())
|
| 934 |
selected_stock_sell = st.selectbox("매도 종목 선택:", stock_names_sell)
|
| 935 |
stock_price_sell = 0
|
| 936 |
+
for sector, stocks in st.session_state["stocks"].items():
|
| 937 |
if selected_stock_sell in stocks:
|
| 938 |
stock_price_sell = stocks[selected_stock_sell]["current_price"]
|
| 939 |
break
|
|
|
|
| 940 |
|
| 941 |
st.info(f"**{selected_stock_sell}** 현재 주가: {stock_price_sell:,.0f}원")
|
| 942 |
max_sell_quantity = st.session_state["portfolio"]["stocks"][
|
|
|
|
| 949 |
value=1,
|
| 950 |
step=1,
|
| 951 |
)
|
| 952 |
+
if not st.session_state['sell_confirm']:
|
| 953 |
if st.button("주식 매도", use_container_width=True, key='sell_button_confirm'):
|
| 954 |
st.session_state['sell_confirm'] = True
|
| 955 |
else:
|
|
|
|
| 957 |
col_confirm, col_cancel = st.columns([1, 1])
|
| 958 |
with col_confirm:
|
| 959 |
if st.button("✅ 매도 확인", use_container_width=True, key='sell_confirm_button'):
|
| 960 |
+
sell_stock(selected_stock_sell, quantity_sell)
|
| 961 |
with col_cancel:
|
| 962 |
if st.button("❌ 매도 취소", use_container_width=True, key='sell_cancel_button', type='secondary'):
|
| 963 |
st.session_state['sell_confirm'] = False
|
|
|
|
| 965 |
else:
|
| 966 |
st.info("보유 주식이 없습니다. 포트폴리오 탭에서 확인하세요.")
|
| 967 |
|
| 968 |
+
with menu[4]:
|
| 969 |
if st.session_state["previous_daily_news"] and st.session_state[
|
| 970 |
"news_meanings"
|
| 971 |
]:
|
| 972 |
+
previous_decade = st.session_state['decade_count'] - 10 # 이전 Decade 계산
|
| 973 |
+
st.subheader(f"{previous_decade}년대 뉴스 해설") # 이전 Decade 표시
|
| 974 |
st.info("AI가 분석한 지난 뉴스 해설입니다.")
|
| 975 |
for i in range(len(st.session_state["previous_daily_news"])):
|
| 976 |
with st.expander(f"뉴스 {i+1}", expanded=False):
|
|
|
|
| 980 |
meaning_data = st.session_state["news_meanings"].get(str(i+1))
|
| 981 |
if meaning_data:
|
| 982 |
st.markdown("**AI 해설:**")
|
| 983 |
+
st.info(meaning_data['explanation'])
|
| 984 |
+
if meaning_data['sectors']:
|
| 985 |
+
st.markdown("**관련 섹터:**")
|
| 986 |
+
st.info(', '.join(meaning_data['sectors']))
|
| 987 |
+
else:
|
| 988 |
+
st.info("**관련 섹터:** 없음")
|
| 989 |
else:
|
| 990 |
st.warning("뉴스 해설을 생성하지 못했습니다.")
|
| 991 |
else:
|
|
|
|
| 993 |
"이전 뉴스 해설이 없습니다. 10년 후 버튼을 눌러 뉴스 해설을 받아보세요."
|
| 994 |
)
|
| 995 |
|
| 996 |
+
with st.sidebar:
|
| 997 |
+
st.markdown(f"# ⏳ 대한민국 경제 발전사 게임") # 앱 제목 변경
|
| 998 |
+
st.markdown(f"### {st.session_state['decade_count']}년대") # Decade 표시
|
| 999 |
st.markdown("---")
|
| 1000 |
|
| 1001 |
+
# 난이도 선택 Selectbox (기존 코드와 동일)
|
| 1002 |
+
difficulty_level = st.selectbox(
|
| 1003 |
"📈 난이도 선택",
|
| 1004 |
["초등학생", "중학생", "고등학생"],
|
| 1005 |
index=["초등학생", "중학생", "고등학생"].index(st.session_state['difficulty_level'])
|
|
|
|
| 1007 |
st.session_state['difficulty_level'] = difficulty_level
|
| 1008 |
st.markdown("---")
|
| 1009 |
|
| 1010 |
+
decade_description_map = { # Decade별 게임 설명
|
| 1011 |
1950: "🇰🇷 **1950년대: 재건의 시대**\n\n 전쟁의 상처를 딛고 일어서는 시기입니다. 원조 경제와 농업을 기반으로 재건에 힘쓰세요.",
|
| 1012 |
1960: "🇰🇷 **1960년대: 경제 개발의 닻을 올리다**\n\n 경제 개발 5개년 계획이 시작됩니다. 수출과 경공업을 통해 경제 성장의 기반을 마련하세요.",
|
| 1013 |
1970: "🇰🇷 **1970년대: 중화학공업, 성장의 엔진**\n\n 중화학공업 육성 정책이 본격화됩니다. 건설 붐과 함께 중동 시장을 개척해보세요.",
|
|
|
|
| 1017 |
2010: "🇰🇷 **2010년대: 스마트 혁명, 플랫폼 경제**\n\n 스마트폰이 세상을 바꾸고, 플랫폼 기업이 성장합니다. 새로운 경제 질서에 올라타세요.",
|
| 1018 |
2020: "🇰🇷 **2020년대: 미래를 향한 도전**\n\n 팬데믹을 극복하고 미래 산업을 육성합니다. 친환경 에너지, 바이오, 플랫폼 기업의 미래를 예측해보세요.",
|
| 1019 |
}
|
| 1020 |
+
decade_description = decade_description_map.get(st.session_state['decade_count'], "🇰🇷 **미래 시대:**\n\n 미래 시대에 대비하여 새로운 산업과 기술에 투자해보세요.") # 기본 설명
|
| 1021 |
|
| 1022 |
+
st.markdown(decade_description) # Decade별 설명
|
| 1023 |
|
| 1024 |
+
cash, total_value, profit_rate = display_portfolio()
|
| 1025 |
st.metric(label="💰 현금 잔고", value=f"{cash:,.0f} 원")
|
| 1026 |
st.metric(label="📊 총 평가 금액", value=f"{total_value:,.0f} 원")
|
| 1027 |
st.metric(label="🚀 총 수익률", value=f"{profit_rate:.2f}%")
|
| 1028 |
st.markdown("---")
|
| 1029 |
|
| 1030 |
+
if st.button("10년 후", use_container_width=True, key="decade_pass_button"): # 버튼 텍스트 변경
|
| 1031 |
if st.session_state["daily_news"]:
|
| 1032 |
+
with st.spinner(f"{st.session_state['decade_count']}년대 주가 변동 및 지난 뉴스 분석..."): # Decade 표시
|
| 1033 |
st.session_state["previous_daily_news"] = st.session_state["daily_news"]
|
| 1034 |
meanings = explain_daily_news_meanings(
|
| 1035 |
st.session_state["previous_daily_news"]
|
|
|
|
| 1038 |
st.session_state["news_meanings"] = meanings
|
| 1039 |
update_stock_prices()
|
| 1040 |
st.session_state["daily_news"] = generate_news()
|
| 1041 |
+
st.session_state["decade_count"] += 10 # Decade + 10
|
| 1042 |
+
st.session_state["stocks"] = initialize_stocks_for_decade(st.session_state["decade_count"]) # 다음 Decade 주식 데이터 초기화
|
| 1043 |
st.rerun()
|
| 1044 |
+
previous_decade = st.session_state['decade_count'] - 10 # 이전 Decade 계산
|
| 1045 |
+
st.info(f"지난 {previous_decade}년대 뉴스 해설 탭에서 AI 분석을 확인해보세요.") # 이전 Decade 표시
|
| 1046 |
else:
|
| 1047 |
st.warning("오늘의 뉴스를 먼저 생성해주세요.")
|
| 1048 |
st.markdown("***")
|
| 1049 |
|
| 1050 |
+
display_stock_glossary()
|
| 1051 |
|
| 1052 |
+
with st.expander("🚀 앱 사용 가이드", expanded=False): # 앱 가이드 수정
|
| 1053 |
st.markdown(
|
| 1054 |
"""
|
| 1055 |
**대한민국 경제 발전사 주식 투자 게임**에 오신 것을 환영합니다! ⏳
|
|
|
|
| 1093 |
|
| 1094 |
**대한민국 경제 발전사를 따라가는 흥미진진한 주식 투자 게임! 지금 시작하세요!** 🚀
|
| 1095 |
""".format(decade=st.session_state['decade_count'], next_decade=st.session_state['decade_count'] + 10)
|
| 1096 |
+
)
|
|
|
|
| 1097 |
|
| 1098 |
if __name__ == "__main__":
|
| 1099 |
+
st.session_state["stocks"] = initialize_stocks_for_decade(st.session_state["decade_count"]) # stocks 초기화 (최초 실행 시 1950년대 데이터)
|
| 1100 |
main()
|