| | import os |
| | import streamlit as st |
| | import random |
| | import time |
| | import pandas as pd |
| | from datetime import date |
| | import plotly.express as px |
| | from openai import OpenAI |
| | import json |
| | from supabase import create_client, Client |
| |
|
| | |
| | st.set_page_config( |
| | page_title="수준별 모의 주식 거래", |
| | page_icon="💰", |
| | layout="wide", |
| | initial_sidebar_state="expanded", |
| | ) |
| |
|
| | |
| | |
| | st.markdown( |
| | """ |
| | <style> |
| | /* 전체 폰트 변경 (Nanum Gothic, Google Fonts CDN 사용) */ |
| | @import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic:wght@400;700&display=swap'); |
| | body { |
| | font-family: 'Nanum Gothic', sans-serif !important; |
| | } |
| | |
| | /* 탭 메뉴 스타일 */ |
| | .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] { |
| | background-color: #007bff !important; |
| | color: white !important; |
| | font-weight: bold; |
| | } |
| | .stTabs [data-baseweb="tab-list"] button { |
| | background-color: #f0f2f6; |
| | color: #333; |
| | border-radius: 8px 8px 0 0; |
| | padding: 0.75em 1em; |
| | margin-bottom: -1px; /* border overlap */ |
| | } |
| | |
| | /* 사이드바 스타일 */ |
| | [data-testid="stSidebar"] { |
| | width: 350px !important; |
| | background-color: #f8f9fa; /* Light gray sidebar background */ |
| | padding: 20px; |
| | } |
| | [data-testid="stSidebar"] h1, [data-testid="stSidebar"] h3 { |
| | color: #212529; /* Dark gray sidebar headings */ |
| | } |
| | [data-testid="stSidebar"] hr { |
| | border-top: 1px solid #e0e0e0; /* Lighter sidebar hr */ |
| | } |
| | |
| | /* Metric 스타일 */ |
| | .streamlit-metric-label { |
| | font-size: 16px; |
| | color: #4a4a4a; |
| | } |
| | .streamlit-metric-value { |
| | font-size: 28px; |
| | font-weight: bold; |
| | } |
| | |
| | /* 버튼 스타일 */ |
| | div.stButton > button { |
| | background-color: #007bff; |
| | color: white; |
| | padding: 12px 24px; |
| | font-size: 16px; |
| | border-radius: 8px; |
| | border: none; |
| | box-shadow: 2px 2px 5px rgba(0,0,0,0.1); /* Soft shadow */ |
| | transition: background-color 0.3s ease; |
| | } |
| | div.stButton > button:hover { |
| | background-color: #0056b3; |
| | box-shadow: 2px 2px 7px rgba(0,0,0,0.15); /* Slightly stronger shadow on hover */ |
| | } |
| | |
| | /* 보조 버튼 스타일 */ |
| | div.stButton > button.secondary-button { |
| | background-color: #6c757d; |
| | color: white; |
| | padding: 10px 20px; |
| | font-size: 14px; |
| | border-radius: 6px; |
| | border: none; |
| | transition: background-color 0.3s ease; |
| | } |
| | div.stButton > button.secondary-button:hover { |
| | background-color: #5a6268; |
| | } |
| | |
| | /* Expander 스타일 */ |
| | .streamlit-expanderHeader { |
| | font-weight: bold; |
| | color: #212529; |
| | border-bottom: 1px solid #e0e0e0; |
| | padding-bottom: 8px; |
| | margin-bottom: 15px; |
| | } |
| | |
| | /* Dataframe 스타일 */ |
| | .dataframe { |
| | border: 1px solid #e0e0e0; |
| | border-radius: 8px; |
| | padding: 12px; |
| | box-shadow: 2px 2px 5px rgba(0,0,0,0.05); /* Very subtle shadow */ |
| | } |
| | |
| | /* Info, Success, Error, Warning Box 스타일 (더 부드러운 스타일) */ |
| | div.stInfo, div.stSuccess, div.stError, div.stWarning { |
| | border-radius: 8px; |
| | padding: 15px; |
| | margin-bottom: 15px; |
| | box-shadow: 2px 2px 5px rgba(0,0,0,0.05); |
| | } |
| | div.stInfo { |
| | background-color: #e7f3ff; |
| | border-left: 5px solid #007bff; |
| | } |
| | div.stSuccess { |
| | background-color: #e6f7ec; |
| | border-left: 5px solid #28a745; |
| | } |
| | div.stError { |
| | background-color: #fdeded; |
| | border-left: 5px solid #dc3545; |
| | } |
| | div.stWarning { |
| | background-color: #fffbe6; |
| | border-left: 5px solid #ffc107; |
| | } |
| | |
| | /* Toast message 스타일 */ |
| | div.streamlit-toast-container { |
| | z-index: 10000; /* Toast를 항상 맨 위에 표시 */ |
| | } |
| | div[data-testid="stToast"] { |
| | border-radius: 8px; |
| | padding: 15px; |
| | box-shadow: 2px 2px 5px rgba(0,0,0,0.1); |
| | } |
| | |
| | </style> |
| | """, |
| | unsafe_allow_html=True, |
| | ) |
| |
|
| | |
| | if "OPENAI_API_KEY" not in os.environ: |
| | st.error("OPENAI_API_KEY 환경 변수가 설정되지 않았습니다. API 키를 설정해주세요.") |
| | st.stop() |
| | client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) |
| |
|
| | |
| | SUPABASE_URL = os.environ.get("SUPABASE_URL") |
| | SUPABASE_KEY = os.environ.get("SUPABASE_KEY") |
| | if not SUPABASE_URL or not SUPABASE_KEY: |
| | st.warning("Supabase URL 또는 Key가 설정되지 않았습니다. 데이터 저장/로드가 불가능합니다.") |
| | supabase = None |
| | else: |
| | try: |
| | supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) |
| | except Exception as e: |
| | st.error(f"Supabase 클라이언트 생성 실패: {e}") |
| | supabase = None |
| |
|
| | |
| | LEVELS = { |
| | "초등": {"name": "초등 (5~6학년)", "initial_cash": 1_000_000, "grade_level": "초등학생 5~6학년"}, |
| | "중등": {"name": "중등 (1~3학년)", "initial_cash": 5_000_000, "grade_level": "중학생 1~3학년"}, |
| | "고등": {"name": "고등 (1~3학년)", "initial_cash": 10_000_000, "grade_level": "고등학생 1~3학년"}, |
| | } |
| |
|
| | |
| | |
| | STOCK_DESCRIPTIONS = { |
| | "초등": { |
| | "삼성전자": "TV, 스마트폰 만드는 회사! 갤럭시 알지? 반도체 칩도 세계 최고!", |
| | "SK하이닉스": "컴퓨터, 스마트폰의 기억력 담당! 사진, 영상 저장 도와줘.", |
| | "LG디스플레이": "TV, 스마트폰 화면 만드는 회사! OLED 기술로 선명하게!", |
| | "현대자동차": "자동차 만드는 회사! 쏘나타, 아이오닉 들어봤지? 전기차도 만들어.", |
| | "기아": "디자인 예쁜 자동차 회사! K5, 쏘렌토, EV6 멋지지?", |
| | "현대모비스": "자동차 부품 만드는 회사! 엔진, 브레이크 등 안전 부품 담당.", |
| | "LG에너지솔루션": "전기차 배터리 세계 1등! 미래 에너지 책임져.", |
| | "SK이노베이션": "기름 만들고, 플라스틱 원료도 만들어. 전기차 배터리도!", |
| | "두산에너빌리티": "전기 만드는 발전소 짓는 회사! 원자력, 풍력 발전소도.", |
| | "네이버": "궁금한 거 검색하는 네이버! 뉴스, 웹툰, 쇼핑 다 있어.", |
| | "카카오": "카카오톡 만든 회사! 택시, 페이, 게임 등 편리한 서비스 가득.", |
| | "카카오뱅크": "스마트폰 은행! 앱으로 쉽게 돈 보내고 관리해.", |
| | "CJ제일제당": "맛있는 음식 만드는 회사! 햇반, 비비고 만두 알지?", |
| | "아모레퍼시픽": "화장품 만드는 회사! 설화수, 이니스프리 들어봤지?", |
| | "LG생활건강": "샴푸, 치약, 화장품 만드는 회사! 코카콜라도 팔아.", |
| | "KB금융": "KB국민은행 있는 금융 회사! 돈 관리 도와줘.", |
| | "신한지주": "신한은행 있는 금융 회사! 카드, 보험도 있어.", |
| | "하나금융지주": "하나은행 있는 금융 회사! 외국 돈 거래 잘해.", |
| | "삼성물산": "건물 짓고, 옷도 팔고, 에버랜드도 운영해!", |
| | "HD현대": "큰 배 만들고, 굴착기 같은 건설 기계도 만들어.", |
| | "GS건설": "아파트 '자이' 짓는 회사! 살기 좋은 집 만들어.", |
| | "롯데쇼핑": "롯데백화점, 롯데마트 운영! 쇼핑은 여기서!", |
| | "이마트": "큰 마트 이마트! 없는 거 빼고 다 있어. 노브랜드 유명해.", |
| | "KT": "인터넷, 스마트폰 통신 회사! 전화, TV 서비스 제공.", |
| | "SK텔레콤": "스마트폰 통신 1등 회사! 빠른 5G 서비스 제공.", |
| | "삼성바이오로직스": "특별한 약(바이오 의약품) 만드는 회사! 아픈 사람 도와줘.", |
| | "셀트리온": "바이오 의약품 개발하고 만드는 회사! 병 치료 도와줘.", |
| | "LG화학": "플라스틱, 배터리 만드는 화학 회사! 생활 곳곳에 있어.", |
| | "금호석유화학": "타이어 원료(합성고무) 만드는 회사! 산업에 꼭 필요해.", |
| | "POSCO홀딩스": "튼튼한 철 만드는 회사! 자동차, 건물에 쓰여.", |
| | "현대제철": "자동차, 건물용 철 만드는 회사! 현대차 그룹이야.", |
| | "대한항공": "비행기 회사! 해외여행 갈 때 타는 비행기.", |
| | "HMM": "큰 배로 물건 실어 나르는 회사! 수출입 도와줘.", |
| | "CJ ENM": "tvN, Mnet 방송국 운영! 영화, 음악도 만들어.", |
| | "하이브": "BTS 소속사! 아이돌 키우고 음악 만들어.", |
| | "오리온": "초코파이, 포카칩 만드는 과자 회사!", |
| | "농심": "신라면, 짜파게티 만드는 라면 회사!", |
| | }, |
| | "중등": { |
| | "삼성전자": "대한민국 대표 전자 기업. 스마트폰(갤럭시), TV, 가전제품 및 반도체(메모리, 시스템LSI) 생산. 글로벌 시장 점유율 높음.", |
| | "SK하이닉스": "메모리 반도체(DRAM, NAND Flash) 전문 기업. 데이터센터, PC, 모바일 기기 등에 필수적인 부품 공급.", |
| | "LG디스플레이": "디스플레이 패널(OLED, LCD) 생산 기업. TV, 스마트폰, 노트북, 차량용 디스플레이 등에 사용. OLED 기술 선도.", |
| | "현대자동차": "국내 1위, 글로벌 상위권 자동차 제조사. 내연기관차, 전기차(아이오닉), 수소차(넥쏘) 등 다양한 라인업 보유.", |
| | "기아": "현대차그룹 계열 자동차 제조사. K시리즈, 쏘렌토, 스포티지 등 인기 모델 보유. 디자인 경쟁력 강조 및 전기차(EV) 라인업 확장 중.", |
| | "현대모비스": "현대차그룹 핵심 부품 계열사. 자동차 모듈, 핵심 부품(전동화, 램프 등), A/S 부품 사업 영위.", |
| | "LG에너지솔루션": "글로벌 전기차 배터리 시장 선두 기업. 파우치형 배터리 강점. GM, 현대차 등 다수 완성차 업체에 공급.", |
| | "SK이노베이션": "정유, 석유화학, 윤활유 및 배터리 사업 영위. SK온을 통해 전기차 배터리 사업 확장 중.", |
| | "두산에너빌리티": "발전 설비(원자력, 화력, 풍력 등) 및 플랜트 건설 전문 기업. 해수담수화, SMR(소형모듈원전), 가스터빈 등 신사업 추진.", |
| | "네이버": "국내 1위 검색 포털. 검색, 커머스, 핀테크(네이버페이), 콘텐츠(웹툰), 클라우드 등 다양한 인터넷 서비스 제공.", |
| | "카카오": "국민 메신저 카카오톡 기반 플랫폼 기업. 모빌리티, 페이, 게임, 웹툰, 뱅크 등 다양한 생활 밀착형 서비스 확장.", |
| | "카카오뱅크": "인터넷 전문 은행. 비대면 금융 서비스 강점. 간편 송금, 대출, 예적금 상품 제공. 플랫폼 기반 성장 추구.", |
| | "CJ제일제당": "국내 대표 식품 기업. 햇반, 비비고 등 HMR(가정간편식) 강자. 바이오(아미노산 등), 사료 사업도 영위. 글로벌 확장 중.", |
| | "아모레퍼시픽": "국내 1위 화장품 기업. 설화수, 라네즈, 이니스프리 등 다수 브랜드 보유. 중국 시장 의존도 높았으나 다변화 노력 중.", |
| | "LG생활건강": "화장품(후, 숨, 오휘), 생활용품(페리오, 엘라스틴), 음료(코카콜라) 사업 영위. 럭셔리 화장품 강점.", |
| | "KB금융": "국내 리딩 금융지주사. KB국민은행, KB증권, KB손해보험 등 계열사 보유. 은행 중심 안정적 수익 구조.", |
| | "신한지주": "KB금융과 경쟁하는 리딩 금융지주사. 신한은행, 신한카드, 신한금융투자 등 보유. 비은행 부문 경쟁력 강화 노력.", |
| | "하나금융지주": "주요 금융지주사 중 하나. 하나은행, 하나증권, 하나카드 등 보유. 외환 및 글로벌 부문 강점.", |
| | "삼성물산": "삼성그룹 지배구조 핵심. 건설(래미안), 상사, 패션(빈폴), 리조트(에버랜드), 바이오(삼성바이오로직스 지분) 등 다양한 사업 영위.", |
| | "HD현대": "조선(HD한국조선해양), 건설기계(HD현대인프라코어, HD현대건설기계), 에너지(HD현대오일뱅크) 등 중공업 중심 그룹 지주사.", |
| | "GS건설": "주택 브랜드 '자이'로 유명한 대형 건설사. 주택, 건축, 플랜트, 인프라 등 다양한 건설 사업 영위.", |
| | "롯데쇼핑": "롯데그룹 유통 부문 핵심. 백화점, 마트, 슈퍼, 아울렛, 이커머스(롯데ON) 등 운영. 오프라인 채널 강점.", |
| | "이마트": "신세계그룹 계열 국내 1위 대형마트. 창고형 할인점(트레이더스), 전문점(노브랜드), 온라인(SSG닷컴) 등 운영.", |
| | "KT": "유무선 통신 서비스 제공. 인터넷, IPTV, 이동통신 등. B2B(클라우드, AI) 및 미디어/콘텐츠 사업 확장 중.", |
| | "SK텔레콤": "국내 1위 이동통신사. 5G 네트워크 경쟁력. AI, 메타버스, 구독 서비스 등 신성장 동력 발굴.", |
| | "삼성바이오로직스": "바이오의약품 위탁개발생산(CDMO) 전문 기업. 글로벌 최대 규모 생산 능력 보유. 높은 기술력과 신뢰도 강점.", |
| | "셀트리온": "바이오시밀러(바이오의약품 복제약) 개발 및 생산 기업. 램시마, 트룩시마 등 글로벌 시장 판매. 신약 개발도 추진.", |
| | "LG화학": "석유화학, 첨단소재(배터리 소재 등), 생명과학 사업 영위. 배터리 소재 부문 성장성 주목.", |
| | "금호석유화학": "합성고무(타이어 원료), 합성수지(플라스틱 원료) 주력 화학 기업. 페놀유도체 등 정밀화학 제품도 생산.", |
| | "POSCO홀딩스": "국내 1위, 글로벌 경쟁력 갖춘 철강 기업 포스코의 지주사. 철강 외 이차전지소재, 수소 등 친환경 미래소재 사업 육성.", |
| | "현대제철": "현대차그룹 계열 철강사. 자동차 강판, 건설용 형강/철근 등 생산. 전기로 기반 친환경 생산 전환 추진.", |
| | "대한항공": "국내 1위, FSC(Full Service Carrier) 항공사. 여객 및 화물 운송 사업. 아시아나항공 인수 추진 중.", |
| | "HMM": "국내 최대 컨테이너 선사. 글로벌 해운 얼라이언스 '디 얼라이언스' 회원사. 해운 시황에 따른 실적 변동성 큼.", |
| | "CJ ENM": "미디어(tvN, Mnet), 영화(CJ엔터테인먼트), 음악(스톤뮤직), 커머스 사업 영위. 콘텐츠 제작 및 유통 역량 보유.", |
| | "하이브": "BTS 소속사로 시작한 글로벌 엔터테인먼트 기업. 멀티 레이블 체제. 위버스 플랫폼 기반 팬덤 비즈니스 확장.", |
| | "오리온": "초코파이로 유명한 제과 기업. 중국, 베트남, 러시아 등 해외 시장 성공적 진출. 간편대용식, 바이오 사업 진출 모색.", |
| | "농심": "신라면, 짜파게티 등 대표 라면 브랜드 보유. 스낵(새우깡 등), 음료 사업도 영위. 해외 시장, 특히 미국 성장세 주목.", |
| | }, |
| | "고등": { |
| | "삼성전자": "글로벌 IT 리더. DX(Device eXperience: 스마트폰, 가전)와 DS(Device Solutions: 반도체) 부문 영위. 파운드리 경쟁력 강화 및 AI 반도체 시장 대응 중요.", |
| | "SK하이닉스": "메모리 반도체 강자. HBM(고대역폭메모리) 등 AI 서버용 고성능 메모리 수요 증가 수혜 기대. NAND 시장 업황 회복 주목.", |
| | "LG디스플레이": "대형 OLED 시장 주도. IT용 OLED 및 차량용 P-OLED 등 신시장 개척. LCD 사업 축소 및 OLED 전환 가속화.", |
| | "현대자동차": "글로벌 완성차 업체. 전용 전기차 플랫폼 E-GMP 기반 아이오닉 시리즈 호평. SDV(소프트웨어 중심 자동차) 전환 및 미래 모빌리티(UAM, 로보틱스) 투자 확대.", |
| | "기아": "현대차그룹 내 디자인 및 EV 특화 브랜드. EV 라인업 성공적 안착. PBV(목적기반모빌리티) 시장 선점 목표.", |
| | "현대모비스": "자동차 핵심 부품 공급사. 전동화, 자율주행 관련 핵심 기술 내재화 노력. 그룹사 외 수주 확대 및 소프트웨어 역량 강화 필요.", |
| | "LG에너지솔루션": "글로벌 Top-tier 배터리 셀 제조사. 북미 중심 생산 능력 확대. 원통형, 파우치형, LFP 등 다양한 폼팩터 및 소재 기술 보유. IRA 수혜 기대.", |
| | "SK이노베이션": "정유/화학 기반 에너지 기업. 배터리 자회사 SK온의 흑자 전환 및 IPO 추진 중요. 카본 투 그린(Carbon to Green) 전략 실행.", |
| | "두산에너빌리티": "전력 인프라 핵심 기업. 원전 생태계 복원 및 SMR 기술 개발 선도. 가스터빈 국산화 및 수소, 풍력 등 친환경 에너지 포트폴리오 강화.", |
| | "네이버": "국내 최대 인터넷 플랫폼. 검색 광고, 커머스 중심 안정적 성장. AI(하이퍼클로바X), 클라우드, 웹툰 글로벌 확장 등 미래 성장 동력 확보 노력.", |
| | "카카오": "모바일 플랫폼 기반 서비스 확장. 카카오톡 채널 및 광고 수익화. 모빌리티, 페이 등 주요 자회사 수익성 개선 및 규제 리스크 관리 중요.", |
| | "카카오뱅크": "대표적인 인터넷 전문 은행. 중저신용자 대출 확대 및 플랫폼 비즈니스 강화. 금리 환경 변화 및 핀테크 경쟁 심화 대응 필요.", |
| | "CJ제일제당": "식품(K-Food 글로벌 확산), 바이오(스페셜티 아미노산), F&C(사료) 사업 포트폴리오. 수익성 중심 경영 및 재무구조 개선 노력.", |
| | "아모레퍼시픽": "화장품 산업 대표 기업. 중국 의존도 축소 및 북미, 유럽 등 신시장 개척. 온라인 채널 강화 및 브랜드 리빌딩 진행 중.", |
| | "LG생활건강": "화장품, 생활용품, 음료 3개 부문 안정적 사업 구조. 중국 리오프닝 효과 및 북미 사업 성과 주목. 브랜드 포트폴리오 관리 중요.", |
| | "KB금융": "리딩 금융그룹. 은행의 안정적 이익 기반 위에 비은행(증권, 보험, 카드) 시너지 창출. 디지털 전환 및 비금융 플랫폼 확장 노력.", |
| | "신한지주": "균형 잡힌 사업 포트폴리오. 비은행 부문 이익 기여도 증대 노력. 글로벌 및 자본시장 부문 경쟁력 강화. 주주환원 정책 확대.", |
| | "하나금융지주": "은행 중심 금융그룹. 기업금융 및 외환 부문 강점. 비은행 경쟁력 강화 및 디지털 금융 혁신 추진.", |
| | "삼성물산": "삼성그룹 사실상 지주회사 역할. 보유 지분 가치(삼성전자, 삼성바이오로직스 등) 중요. 건설 수주 및 상사 트레이딩 실적, 신사업(친환경 에너지 등) 성과 주목.", |
| | "HD현대": "조선 부문 업황 개선 수혜. 건설기계 북미/신흥국 인프라 투자 수혜. 정유 부문 실적 안정화 및 로봇, AI 등 신기술 투자.", |
| | "GS건설": "주택 시장 변동성 영향. 해외 플랜트 수주 및 신사업(모듈러 주택, 수처리 등) 성과 중요. 재무 건전성 관리 필요.", |
| | "롯데쇼핑": "백화점, 마트 등 오프라인 유통 강자. 이커머스(롯데ON) 경쟁력 강화 및 수익성 개선 과제. 해외 사업(베트남 등) 성과 주목.", |
| | "이마트": "오프라인 할인점 경쟁력 유지 및 온라인(SSG닷컴, G마켓) 시너지 창출 노력. SCK컴퍼니(스타벅스) 실적 기여. 수익성 개선 집중.", |
| | "KT": "통신 본업 안정성 기반 비통신(미디어, 클라우드, AI) 성장 추구. CEO 리스크 해소 후 성장 전략 구체화. 주주환원 정책 유지.", |
| | "SK텔레콤": "견조한 무선 사업 실적. AI 컴퍼니 전환 목표(에이닷 등). T우주(구독), 이프랜드(메타버스) 등 신사업 성과 가시화 필요.", |
| | "삼성바이오로직스": "글로벌 CDMO 시장 지배력 강화. 4공장 가동 및 5공장 증설 계획. ADC(항체-약물 접합체) 등 차세대 기술 투자.", |
| | "셀트리온": "바이오시밀러 퍼스트무버. 미국 시장 직판 체제 구축. 휴미라, 스텔라라 등 블록버스터 바이오시밀러 출시 예정. 신약 개발 역량 강화.", |
| | "LG화학": "기초소재(석유화학), 첨단소재(양극재 등), 생명과학 사업 영위. LG에너지솔루션 지분 가치 반영. 친환경 소재 및 배터리 소재 집중 육성.", |
| | "금호석유화학": "합성고무/수지 등 주력 제품 시황 중요. NB라텍스(의료용 장갑 소재) 수요 변화 주목. 주주환원 정책 및 신성장 동력 확보 노력.", |
| | "POSCO홀딩스": "철강 사업 안정성 및 친환경 전환. 이차전지소재(리튬, 니켈, 양/음극재) 밸류체인 구축 가속화. 수소 사업 비전 제시.", |
| | "현대제철": "자동차 강판 등 그룹사 물량 기반 안정적 실적. 건설 시황 영향. 탄소중립 목표 달성 위한 수소환원제철 기술 개발 중요.", |
| | "대한항공": "여객 수요 회복 및 견조한 화물 실적. 유가 및 환율 변동성 영향. 아시아나항공 인수 관련 불확실성 해소 필요.", |
| | "HMM": "컨테이너 운임 시황 민감. 선대 확장 및 효율화 노력. 매각 이슈 및 글로벌 해운 동맹 재편 영향 주목.", |
| | "CJ ENM": "방송 광고 시장 둔화 영향. 티빙(OTT) 성장 및 수익성 개선 과제. 피프스시즌(미국 제작사) 등 글로벌 콘텐츠 경쟁력 강화.", |
| | "하이브": "멀티 레이블 체제 안착 및 신인 그룹 성공적 데뷔. 위버스 플랫폼 수익 모델 다각화. 게임, AI 등 신규 사업 확장.", |
| | "오리온": "견고한 국내 및 해외(중국, 베트남, 러시아) 실적. 제품 카테고리 확장(간편대용식 등) 및 신규 시장 진출 모색. 바이오 사업 투자.", |
| | "농심": "국내외 라면 시장 지배력. 해외 법인 고성장 지속. 비용 상승 부담 완화 및 판가 인상 효과. 건강기능식품 등 신사업 추진.", |
| | } |
| | } |
| |
|
| | |
| | GLOSSARY = { |
| | "초등": { |
| | "주식": "회사의 작은 조각. 이걸 사면 나도 회사 주인!", |
| | "주가": "주식 1개의 가격. 사고 싶은 사람이 많으면 오르고, 팔고 싶은 사람이 많으면 내려.", |
| | "매수": "주식을 사는 것. '나 이 회사 주식 살래!'", |
| | "매도": "주식을 파는 것. '나 이 주식 팔아서 돈으로 바꿀래!'", |
| | "포트폴리오": "내가 가진 주식과 현금 꾸러미. 어떤 주식을 얼마나 가졌는지 보여줘.", |
| | "수익률": "내가 투자한 돈이 얼마나 늘었는지 알려주는 숫자. (%)", |
| | "상승": "주가가 오르는 것. 기분 좋아!", |
| | "하락": "주가가 내리는 것. 조금 슬퍼.", |
| | "변동": "주가가 오르락내리락 춤추는 것.", |
| | "투자": "돈을 불리기 위해 주식 같은 곳에 돈을 넣는 것.", |
| | "섹터": "비슷한 일을 하는 회사들 모임. (예: 자동차 회사 모임, 과자 회사 모임)", |
| | "전일 대비": "어제랑 비교해서 주가가 얼마나 변했는지 보여주는 것.", |
| | "뉴스": "세상 소식. 회사에 좋은 소식도 있고, 나쁜 소식도 있어.", |
| | "현금": "내가 지금 바로 쓸 수 있는 돈.", |
| | "평가액": "내가 가진 주식들을 지금 가격으로 계산하면 얼마인지 알려주는 것.", |
| | "손익": "내가 돈을 벌었는지(이익), 잃었는지(손해) 알려주는 것.", |
| | }, |
| | "중등": { |
| | "주식": "기업이 자금을 모으기 위해 발행하는 소유권 증서. 주주는 회사의 일부를 소유.", |
| | "주가": "시장에서 거래되는 주식 1주당 가격. 수요와 공급에 따라 결정됨.", |
| | "매수": "주식을 사는 행위. 가격 상승을 기대하고 구매.", |
| | "매도": "보유한 주식을 파는 행위. 이익 실현 또는 손실 확정 목적.", |
| | "포트폴리오": "투자자가 보유한 다양한 자산(주식, 채권, 현금 등)의 구성.", |
| | "수익률": "투자 원금 대비 발생한 이익의 비율. (총 평가액 - 총 투자금) / 총 투자금 * 100%", |
| | "상승": "주가가 이전 가격보다 오르는 현상.", |
| | "하락": "주가가 이전 가격보다 내리는 현상.", |
| | "변동성": "주가나 시장 지수가 움직이는 정도. 변동성이 크면 가격 변화가 심함.", |
| | "투자": "미래의 수익을 기대하고 현재의 자금을 투입하는 행위.", |
| | "섹터": "산업 분류. 비슷한 사업을 영위하는 기업들의 그룹 (예: IT 섹터, 바이오 섹터).", |
| | "전일 대비 등락률": "오늘 종가가 어제 종가에 비해 얼마나 변동했는지 백분율로 표시.", |
| | "뉴스 (경제)": "기업 실적, 경제 지표 발표, 정책 변화 등 주가에 영향을 미칠 수 있는 정보.", |
| | "현금": "즉시 사용 가능한 자금. 포트폴리오 내 유동성 자산.", |
| | "평가액 (주식)": "보유 주식 수량 × 현재 주가. 포트폴리오의 현재 가치.", |
| | "손익": "매수 가격과 현재(또는 매도) 가격의 차이로 발생하는 이익 또는 손실.", |
| | "시가총액": "기업의 전체 주식 가치. 주가 × 총 발행 주식 수.", |
| | "배당금": "기업이 이익의 일부를 주주에게 나눠주는 돈.", |
| | }, |
| | "고등": { |
| | "주식 (보통주)": "기업의 소유권을 나타내는 대표적인 유가증권. 의결권과 배당권 보유.", |
| | "주가": "자본시장에서 결정되는 주식의 시장 가격. 기업 가치, 업황, 경제 상황 등 복합적 요인 반영.", |
| | "매수 (Long Position)": "가격 상승을 예상하고 특정 자산을 매입하는 것.", |
| | "매도 (Short Selling / Position Closing)": "보유 자산을 팔거나(청산), 가격 하락을 예상하고 빌려서 파는 것(공매도).", |
| | "포트폴리오": "위험 분산 및 수익 극대화를 위해 여러 자산에 분산 투자한 집합.", |
| | "수익률 (CAGR, 누적)": "투자기간 동안의 연평균 복합 수익률 또는 총 누적 수익률.", |
| | "상승 (Bull Market)": "주식 시장이 전반적으로 장기간 상승하는 추세.", |
| | "하락 (Bear Market)": "주식 시장이 전반적으로 장기간 하락하는 추세.", |
| | "변동성 (Volatility)": "자산 가격의 변동 정도를 나타내는 통계적 지표. 표준편차 등으로 측정.", |
| | "투자 (Investment)": "자본을 투입하여 미래의 자본 이득이나 소득 증대를 추구하는 행위.", |
| | "섹터/산업": "경제 활동 영역에 따른 기업 분류 (GICS, KRX 산업분류 등). 경기 순환과의 연관성 분석.", |
| | "전일 대비 등락률": "기준 시점(주로 전일 종가) 대비 가격 변화율. 시장 모멘텀 파악 지표.", |
| | "뉴스 (거시/미시)": "금리, 환율, GDP 등 거시경제 지표 및 개별 기업 뉴스(실적, M&A, 신기술 등).", |
| | "현금 (Cash Equivalents)": "현금 및 단기 금융상품. 포트폴리오의 안정성 및 기회 확보 수단.", |
| | "평가액 (Mark-to-Market)": "보유 자산을 현재 시장 가격으로 평가한 금액.", |
| | "손익 (실현/미실현)": "매매를 통해 확정된 손익(실현)과 평가상의 손익(미실현).", |
| | "시가총액 (Market Capitalization)": "기업의 규모와 시장 가치를 나타내는 지표.", |
| | "배당수익률": "주가 대비 배당금의 비율. 투자 매력도 판단 지표 중 하나.", |
| | "PER (주가수익비율)": "주가 / 주당순이익(EPS). 기업의 수익성 대비 주가 수준 평가.", |
| | "PBR (주가순자산비율)": "주가 / 주당순자산(BPS). 기업의 자산가치 대비 주가 수준 평가.", |
| | "ROE (자기자본이익률)": "당기순이익 / 자기자본. 기업의 수익성 및 효율성 지표.", |
| | } |
| | } |
| |
|
| | |
| | def initialize_session_state(selected_level): |
| | level_info = LEVELS[selected_level] |
| | initial_cash = level_info["initial_cash"] |
| |
|
| | |
| | if "portfolio" not in st.session_state or st.session_state.get("force_reset"): |
| | st.session_state["portfolio"] = {"cash": initial_cash, "stocks": {}} |
| | st.session_state["initial_cash_set"] = initial_cash |
| | if "force_reset" in st.session_state: |
| | del st.session_state["force_reset"] |
| |
|
| | |
| | if "stocks" not in st.session_state or not st.session_state["stocks"]: |
| | st.session_state["stocks"] = {} |
| | base_stocks_data = { |
| | "기술(Tech)": ["삼성전자", "SK하이닉스", "LG디스플레이"], |
| | "자동차(Auto)": ["현대자동차", "기아", "현대모비스"], |
| | "에너지(Energy)": ["LG에너지솔루션", "SK이노베이션", "두산에너빌리티"], |
| | "인터넷(Internet)": ["네이버", "카카오", "카카오뱅크"], |
| | "소비재(Consumer Goods)": ["CJ제일제당", "아모레퍼시픽", "LG생활건강"], |
| | "금융(Finance)": ["KB금융", "신한지주", "하나금융지주"], |
| | "건설(Construction)": ["삼성물산", "HD현대", "GS건설"], |
| | "유통(Retail)": ["롯데쇼핑", "이마트"], |
| | "통신(Telecom)": ["KT", "SK텔레콤"], |
| | "제약/바이오(Pharma/Bio)": ["삼성바이오로직스", "셀트리온"], |
| | "화학(Chemical)": ["LG화학", "금호석유화학"], |
| | "철강(Steel)": ["POSCO홀딩스", "현대제철"], |
| | "운송(Transportation)": ["대한항공", "HMM"], |
| | "엔터테인먼트(Entertainment)": ["CJ ENM", "하이브"], |
| | "식품(Food)": ["오리온", "농심"], |
| | } |
| | |
| | for sector, stock_list in base_stocks_data.items(): |
| | st.session_state["stocks"][sector] = {} |
| | for stock_name in stock_list: |
| | |
| | if stock_name in ["삼성전자", "SK하이닉스", "기아", "KB금융", "신한지주", "HD현대", "GS건설", "KT", "SK텔레콤", "현대제철", "대한항공", "HMM", "카카오", "카카오뱅크"]: |
| | price = random.randint(30000, 90000) |
| | elif stock_name in ["LG디스플레이", "두산에너빌리티", "하나금융지주"]: |
| | price = random.randint(15000, 45000) |
| | elif stock_name in ["현대자동차", "현대모비스", "SK이노베이션", "네이버", "아모레퍼시픽", "삼성물산", "롯데쇼핑", "이마트", "셀트리온", "금호석유화학", "CJ ENM", "하이브", "오리온"]: |
| | price = random.randint(120000, 280000) |
| | elif stock_name in ["LG에너지솔루션", "CJ제일제당", "POSCO홀딩스", "농심"]: |
| | price = random.randint(280000, 450000) |
| | elif stock_name in ["LG생활건강", "삼성바이오로직스", "LG화학"]: |
| | price = random.randint(500000, 800000) |
| | else: |
| | price = random.randint(50000, 150000) |
| |
|
| | st.session_state["stocks"][sector][stock_name] = { |
| | "current_price": price, |
| | "price_history": [price], |
| | "description_초등": STOCK_DESCRIPTIONS["초등"].get(stock_name, "설명 없음"), |
| | "description_중등": STOCK_DESCRIPTIONS["중등"].get(stock_name, "설명 없음"), |
| | "description_고등": STOCK_DESCRIPTIONS["고등"].get(stock_name, "설명 없음"), |
| | } |
| |
|
| | |
| | if "chat_session" not in st.session_state: st.session_state["chat_session"] = [] |
| | if "news_analysis_results" not in st.session_state: st.session_state["news_analysis_results"] = {} |
| | if "messages" not in st.session_state: st.session_state["messages"] = [] |
| | if "daily_news" not in st.session_state: st.session_state["daily_news"] = None |
| | if "previous_daily_news" not in st.session_state: st.session_state["previous_daily_news"] = None |
| | |
| | if "news_meanings" not in st.session_state: st.session_state["news_meanings"] = {} |
| | |
| | if "day_count" not in st.session_state: st.session_state["day_count"] = 1 |
| | if "sector_news_impact" not in st.session_state: st.session_state["sector_news_impact"] = {} |
| | if 'buy_confirm' not in st.session_state: st.session_state['buy_confirm'] = False |
| | if 'sell_confirm' not in st.session_state: st.session_state['sell_confirm'] = False |
| | if 'user_id' not in st.session_state: st.session_state['user_id'] = None |
| | if 'user_settings' not in st.session_state: st.session_state['user_settings'] = None |
| | if 'selected_level' not in st.session_state: st.session_state['selected_level'] = "초등" |
| |
|
| | |
| | def generate_news(): |
| | selected_level = st.session_state.get('selected_level', '초등') |
| | level_info = LEVELS[selected_level] |
| | grade_level_text = level_info['grade_level'] |
| |
|
| | if selected_level == '초등': |
| | level_instruction = f"{grade_level_text} 수준에 맞춰 아주 쉽고 구체적인 예시(예: 장난감, 과자, 게임)를 들어 설명해주세요. 어려운 경제 용어(예: 금리, 환율, 인플레이션)는 최대한 피하고, 일상 생활과 관련된 내용으로 작성해주세요." |
| | sentence_count = "8~10문장" |
| | elif selected_level == '중등': |
| | level_instruction = f"{grade_level_text} 수준에 맞춰 작성해주세요. 기본적인 경제 개념(예: 수요와 공급, 경쟁, 인기 상품)을 포함해도 좋습니다. 너무 전문적이지 않게 설명해주세요." |
| | sentence_count = "10~12문장" |
| | else: |
| | level_instruction = f"{grade_level_text} 수준에 맞춰 작성해주세요. 경제 지표(예: 성장률, 실업률), 국제 관계, 기술 트렌드, 금리 변동 등 좀 더 심도 있는 내용을 다루어도 좋습니다. 분석적인 시각을 포함해주세요." |
| | sentence_count = "12~15문장" |
| |
|
| | prompt = f""" |
| | 지시: |
| | {level_instruction} |
| | 주식 시장과 경제에 관련된 뉴스 기사 5개를 생성해주세요. |
| | 각 기사는 {sentence_count} 정도로 자세하게 작성하고, 특정 회사 이름이나 주식 종목을 직접적으로 언급하지 마세요. |
| | 학생들이 뉴스를 읽고 어떤 종류의 회사가 유망할지 또는 어려움을 겪을지 스스로 추론할 수 있도록, 일반적인 경제 상황이나 특정 산업(예: IT, 자동차, 게임, 식품, 에너지 등) 동향에 대한 뉴스를 만들어주세요. |
| | 긍정적인 뉴스, 부정적인 뉴스, 중립적인 뉴스를 다양하게 포함하되, '긍정적/부정적/중립적'이라는 단어는 뉴스 본문에 쓰지 마세요. |
| | 뉴스 내용에 따라 관련 주식들의 가격이 오르거나 내릴 수 있는 단서를 포함해주세요. |
| | 각 뉴스 기사는 "## 뉴스 [번호]" 로 시작해주세요. (예: ## 뉴스 1, ## 뉴스 2 ...) |
| | |
| | **생성된 뉴스 기사:** |
| | """ |
| | messages = [{"role": "user", "content": prompt}] |
| |
|
| | try: |
| | response = client.chat.completions.create( |
| | model="gpt-4o-mini", |
| | messages=messages, |
| | temperature=0.7, |
| | max_tokens=1500, |
| | top_p=0.95, |
| | frequency_penalty=0, |
| | presence_penalty=0 |
| | ) |
| | news_text = response.choices[0].message.content.strip() |
| |
|
| | news_articles = [] |
| | if news_text: |
| | |
| | raw_articles = news_text.split("## 뉴스 ") |
| | for article in raw_articles: |
| | if article.strip(): |
| | |
| | content = article.split('\n', 1)[-1].strip() if '\n' in article else article.strip() |
| | if content: |
| | |
| | if content and content[0].isdigit() and content[1:3] in ['\n', '. ']: |
| | content = content.split('\n', 1)[-1].strip() |
| | news_articles.append(content) |
| |
|
| | |
| | if len(news_articles) < 5: |
| | news_articles.extend(["(뉴스 생성 실패)"] * (5 - len(news_articles))) |
| | return news_articles[:5] |
| |
|
| | except Exception as e: |
| | st.error(f"뉴스 생성 중 오류 발생: {e}") |
| | return ["(뉴스 생성 오류)"] * 5 |
| |
|
| |
|
| | |
| | def explain_daily_news_meanings(daily_news): |
| | if daily_news is None: |
| | return {} |
| |
|
| | selected_level = st.session_state.get('selected_level', '초등') |
| | level_info = LEVELS[selected_level] |
| | grade_level_text = level_info['grade_level'] |
| |
|
| | if selected_level == '초등': |
| | level_instruction = f"{grade_level_text}이 이해하기 쉽게 아주 쉬운 단어로 2~3문장 이내로 요약해주세요. 비유나 쉬운 예시를 사용하면 좋습니다." |
| | elif selected_level == '중등': |
| | level_instruction = f"{grade_level_text}이 이해하기 쉽게 핵심 내용을 3문장 정도로 요약해주세요. 관련 경제 용어가 있다면 간단히 설명해주세요." |
| | else: |
| | level_instruction = f"{grade_level_text}이 이해할 수 있도록 핵심 내용과 이 뉴스가 경제나 특정 산업에 미칠 수 있는 잠재적 영향을 3-4문장 정도로 분석적으로 요약해주세요." |
| |
|
| | meanings = {} |
| | valid_sectors_list = list(st.session_state["stocks"].keys()) |
| |
|
| | for i, news_article in enumerate(daily_news): |
| | if "(뉴스 생성 오류)" in news_article or "(뉴스 생성 실패)" in news_article: |
| | meanings[str(i + 1)] = {"explanation": "뉴스 생성에 실패하여 해설할 수 없습니다.", "sectors": []} |
| | continue |
| |
|
| | prompt = f""" |
| | **신문 기사:** |
| | {news_article} |
| | |
| | **지시:** |
| | 위 신문 기사의 핵심 의미를 {level_instruction} "해설: " 다음에 설명해주세요. |
| | 그리고 이 뉴스와 가장 관련성이 높은 주식 섹터 1~2개를 쉼표로 구분해서 "관련 섹터: " 다음에 알려주세요. 제시된 섹터 목록 [{', '.join(valid_sectors_list)}] 중에서만 선택하고, 관련 섹터가 명확하지 않거나 없다면 "관련 섹터: 없음" 이라고 해주세요. |
| | |
| | 뉴스 의미 해설: |
| | """ |
| | messages = [{"role": "user", "content": prompt}] |
| | try: |
| | response = client.chat.completions.create( |
| | model="gpt-4o-mini", |
| | messages=messages, |
| | temperature=0.5, |
| | max_tokens=300, |
| | top_p=0.95, |
| | frequency_penalty=0, |
| | presence_penalty=0 |
| | ) |
| | meaning_text = response.choices[0].message.content.strip() |
| |
|
| | explanation = "" |
| | related_sectors = [] |
| |
|
| | |
| | if "해설:" in meaning_text: |
| | explanation_start_index = meaning_text.find("해설:") + len("해설:") |
| | |
| | explanation_end_index = meaning_text.find("관련 섹터:") |
| | if explanation_end_index != -1: |
| | explanation = meaning_text[explanation_start_index:explanation_end_index].strip() |
| | else: |
| | explanation = meaning_text[explanation_start_index:].strip() |
| | else: |
| | explanation = "AI 해설 생성에 실패했습니다." |
| |
|
| | |
| | if "관련 섹터:" in meaning_text: |
| | related_sectors_str = meaning_text.split("관련 섹터:")[-1].strip() |
| | if related_sectors_str.lower() != "없음" and related_sectors_str: |
| | |
| | potential_sectors = [sector.strip() for sector in related_sectors_str.split(',')] |
| | related_sectors = [s for s in potential_sectors if s in valid_sectors_list] |
| | else: |
| | related_sectors = [] |
| | else: |
| | related_sectors = [] |
| |
|
| | meanings[str(i + 1)] = {"explanation": explanation, "sectors": related_sectors} |
| |
|
| | except Exception as e: |
| | st.error(f"뉴스 {i+1} 해설 중 오류 발생: {e}") |
| | meanings[str(i + 1)] = {"explanation": f"오류 발생: {e}", "sectors": []} |
| | time.sleep(0.5) |
| | return meanings |
| |
|
| | |
| | def buy_stock(stock_name, quantity, sector): |
| | if ( |
| | sector not in st.session_state["stocks"] |
| | or stock_name not in st.session_state["stocks"][sector] |
| | ): |
| | st.error("존재하지 않는 주식 종목입니다.") |
| | st.toast("존재하지 않는 주식 종목입니다.", icon="❌") |
| | return |
| |
|
| | if quantity <= 0: |
| | st.error("매수 수량은 1주 이상이어야 합니다.") |
| | st.toast("매수 수량은 1주 이상이어야 합니다.", icon="❌") |
| | return |
| |
|
| | stock_price = st.session_state["stocks"][sector][stock_name]["current_price"] |
| | total_price = stock_price * quantity |
| |
|
| | if st.session_state["portfolio"]["cash"] >= total_price: |
| | st.session_state["portfolio"]["cash"] -= total_price |
| | portfolio_stocks = st.session_state["portfolio"]["stocks"] |
| | if stock_name in portfolio_stocks: |
| | |
| | current_quantity = portfolio_stocks[stock_name]["quantity"] |
| | current_total_purchase = portfolio_stocks[stock_name]["purchase_price"] * current_quantity |
| | new_quantity = current_quantity + quantity |
| | new_total_purchase = current_total_purchase + total_price |
| | portfolio_stocks[stock_name]["quantity"] = new_quantity |
| | portfolio_stocks[stock_name]["purchase_price"] = new_total_purchase / new_quantity if new_quantity > 0 else 0 |
| | else: |
| | portfolio_stocks[stock_name] = { |
| | "quantity": quantity, |
| | "purchase_price": stock_price, |
| | } |
| | success_msg = f"{stock_name} {quantity}주 매수 완료! (총 {total_price:,.0f}원)" |
| | st.success(success_msg) |
| | st.toast(success_msg, icon="✅") |
| | st.session_state['buy_confirm'] = False |
| | save_session_data() |
| | else: |
| | max_quantity = st.session_state["portfolio"]["cash"] // stock_price if stock_price > 0 else 0 |
| | error_msg = f"잔액 부족! (최대 {max_quantity}주 매수 가능)" |
| | st.error(error_msg) |
| | st.toast(error_msg, icon="❌") |
| | st.session_state['buy_confirm'] = False |
| |
|
| | def sell_stock(stock_name, quantity): |
| | if stock_name not in st.session_state["portfolio"]["stocks"]: |
| | st.error("보유하고 있지 않은 주식입니다.") |
| | st.toast("보유하고 있지 않은 주식입니다.", icon="❌") |
| | return |
| |
|
| | owned_quantity = st.session_state["portfolio"]["stocks"][stock_name]["quantity"] |
| |
|
| | if quantity <= 0: |
| | st.error("매도 수량은 1주 이상이어야 합니다.") |
| | st.toast("매도 수량은 1주 이상이어야 합니다.", icon="❌") |
| | return |
| |
|
| | if quantity > owned_quantity: |
| | st.error(f"매도 가능 수량 초과! (최대 {owned_quantity}주 매도 가능)") |
| | st.toast(f"매도 가능 수량 초과! (최대 {owned_quantity}주 매도 가능)", icon="❌") |
| | return |
| |
|
| | |
| | stock_price = 0 |
| | stock_sector = "" |
| | for sector, stocks_in_sector in st.session_state["stocks"].items(): |
| | if stock_name in stocks_in_sector: |
| | stock_price = stocks_in_sector[stock_name]["current_price"] |
| | stock_sector = sector |
| | break |
| |
|
| | if stock_price <= 0: |
| | st.error("주식 가격 정보를 찾을 수 없거나 유효하지 않습니다.") |
| | st.toast("주식 가격 정보를 찾을 수 없거나 유효하지 않습니다.", icon="❌") |
| | return |
| |
|
| | sell_value = stock_price * quantity |
| | st.session_state["portfolio"]["cash"] += sell_value |
| | st.session_state["portfolio"]["stocks"][stock_name]["quantity"] -= quantity |
| |
|
| | |
| | if st.session_state["portfolio"]["stocks"][stock_name]["quantity"] == 0: |
| | del st.session_state["portfolio"]["stocks"][stock_name] |
| |
|
| | success_msg = f"{stock_name} {quantity}주 매도 완료! (+{sell_value:,.0f}원)" |
| | st.success(success_msg) |
| | st.toast(success_msg, icon="✅") |
| | st.session_state['sell_confirm'] = False |
| | save_session_data() |
| |
|
| | |
| | def update_stock_prices(): |
| | if not st.session_state.get("news_meanings"): |
| | |
| | for sector in st.session_state["stocks"]: |
| | for stock_name in st.session_state["stocks"][sector]: |
| | change_rate = random.uniform(-0.03, 0.03) |
| | change_rate = max(-0.1, min(0.1, change_rate)) |
| | current_price = st.session_state["stocks"][sector][stock_name]["current_price"] |
| | new_price = current_price * (1 + change_rate) |
| | new_price = max(1, int(new_price)) |
| | st.session_state["stocks"][sector][stock_name]["current_price"] = new_price |
| | st.session_state["stocks"][sector][stock_name]["price_history"].append(new_price) |
| | st.info("주가가 임의로 변동되었습니다.") |
| | st.toast("주가가 임의로 변동되었습니다.", icon="📈") |
| | st.session_state["sector_news_impact"] = {} |
| | return |
| |
|
| | |
| | sector_impacts = {sector: 0.0 for sector in st.session_state["stocks"]} |
| | news_meanings = st.session_state["news_meanings"] |
| |
|
| | for news_index, meaning_data in news_meanings.items(): |
| | explanation = meaning_data.get("explanation", "") |
| | related_sectors = meaning_data.get("sectors", []) |
| |
|
| | |
| | positive_keywords = ["성장", "증가", "호황", "개발 성공", "수출 증가", "인기", "기대", "긍정적", "개선", "호조", "확대"] |
| | negative_keywords = ["감소", "하락", "부진", "어려움", "위기", "경쟁 심화", "규제", "부정적", "악화", "축소", "둔화"] |
| |
|
| | sentiment_score = 0 |
| | |
| | for p_kw in positive_keywords: |
| | sentiment_score += explanation.count(p_kw) |
| | for n_kw in negative_keywords: |
| | sentiment_score -= explanation.count(n_kw) |
| |
|
| | |
| | impact_magnitude = 0.0 |
| | if sentiment_score > 0: |
| | impact_magnitude = random.uniform(0.01, 0.04) * min(sentiment_score, 3) |
| | elif sentiment_score < 0: |
| | impact_magnitude = random.uniform(-0.04, -0.01) * min(abs(sentiment_score), 3) |
| |
|
| | for sector in related_sectors: |
| | if sector in sector_impacts: |
| | sector_impacts[sector] += impact_magnitude |
| |
|
| | |
| | for sector in st.session_state["stocks"]: |
| | sector_impact = sector_impacts.get(sector, 0.0) |
| | for stock_name in st.session_state["stocks"][sector]: |
| | |
| | random_change = random.uniform(-0.02, 0.02) |
| | |
| | total_change_rate = random_change + sector_impact |
| | |
| | total_change_rate = max(-0.15, min(0.15, total_change_rate)) |
| |
|
| | current_price = st.session_state["stocks"][sector][stock_name]["current_price"] |
| | new_price = current_price * (1 + total_change_rate) |
| | |
| | new_price = max(1, int(new_price)) |
| |
|
| | st.session_state["stocks"][sector][stock_name]["current_price"] = new_price |
| | st.session_state["stocks"][sector][stock_name]["price_history"].append(new_price) |
| |
|
| | st.info("뉴스 영향을 반영하여 주가가 변동되었습니다.") |
| | st.toast("주가가 변동되었습니다.", icon="📊") |
| | st.session_state["sector_news_impact"] = sector_impacts |
| |
|
| |
|
| | |
| | def calculate_portfolio_summary(): |
| | portfolio = st.session_state.get("portfolio", {"cash": 0, "stocks": {}}) |
| | cash = portfolio.get("cash", 0) |
| | total_stock_value = 0 |
| | total_purchase_value = 0 |
| |
|
| | for stock_name, stock_info in portfolio.get("stocks", {}).items(): |
| | quantity = stock_info.get("quantity", 0) |
| | purchase_price = stock_info.get("purchase_price", 0) |
| | current_price = 0 |
| | |
| | for sector, stocks_in_sector in st.session_state.get("stocks", {}).items(): |
| | if stock_name in stocks_in_sector: |
| | current_price = stocks_in_sector[stock_name].get("current_price", 0) |
| | break |
| | if current_price > 0 and quantity > 0: |
| | total_stock_value += current_price * quantity |
| | total_purchase_value += purchase_price * quantity |
| |
|
| | total_value = cash + total_stock_value |
| | |
| | initial_cash = st.session_state.get("initial_cash_set", LEVELS[st.session_state.get('selected_level', '초등')]['initial_cash']) |
| |
|
| | total_profit_loss = total_value - initial_cash |
| | total_profit_rate = (total_profit_loss / initial_cash) * 100 if initial_cash > 0 else 0 |
| |
|
| | return cash, total_value, total_profit_loss, total_profit_rate, initial_cash |
| |
|
| | |
| |
|
| | def display_stock_prices(): |
| | selected_level = st.session_state.get('selected_level', '초등') |
| | stocks_data = [] |
| | if "stocks" not in st.session_state or not st.session_state["stocks"]: |
| | st.warning("주식 정보가 로드되지 않았습니다. 앱을 다시 시작하거나 관리자에게 문의하세요.") |
| | return |
| |
|
| | for sector, sector_stocks in st.session_state["stocks"].items(): |
| | for stock_name, stock_info in sector_stocks.items(): |
| | price_history = stock_info.get("price_history", []) |
| | current_price = stock_info.get("current_price", 0) |
| | daily_change_rate_str = " - " |
| | if len(price_history) >= 2: |
| | previous_day_price = price_history[-2] |
| | if previous_day_price > 0: |
| | daily_change_rate = (current_price - previous_day_price) / previous_day_price * 100 |
| | daily_change_rate_str = f"{daily_change_rate:+.2f}%" |
| | else: |
| | daily_change_rate_str = "N/A" |
| |
|
| | stocks_data.append( |
| | { |
| | "종목": stock_name, |
| | "섹터": sector, |
| | "현재 주가": f"{current_price:,.0f} 원", |
| | "전일 대비": daily_change_rate_str, |
| | "price_history": price_history, |
| | |
| | "description": stock_info.get(f"description_{selected_level}", stock_info.get("description_중등", "설명 없음")), |
| | } |
| | ) |
| |
|
| | if not stocks_data: |
| | st.info("표시할 주식 데이터가 없습니다.") |
| | return |
| |
|
| | stocks_df = pd.DataFrame(stocks_data) |
| | |
| | st.dataframe(stocks_df[["섹터", "종목", "현재 주가", "전일 대비"]], hide_index=True, use_container_width=True) |
| |
|
| | st.markdown("---") |
| | |
| | stock_names_list = ["종목 선택..."] + stocks_df["종목"].tolist() |
| | selected_stock_all_info = st.selectbox( |
| | "종목 상세 정보 보기 (기업 정보 및 주가 그래프)", stock_names_list, key="stock_detail_select" |
| | ) |
| |
|
| | if selected_stock_all_info and selected_stock_all_info != "종목 선택...": |
| | |
| | selected_stock_data_list = stocks_df[stocks_df["종목"] == selected_stock_all_info] |
| | if not selected_stock_data_list.empty: |
| | selected_stock_data = selected_stock_data_list.iloc[0] |
| | selected_stock_sector = selected_stock_data["섹터"] |
| |
|
| | col1_info, col2_graph = st.columns([1, 1]) |
| |
|
| | with col1_info: |
| | st.subheader(f"🏢 {selected_stock_all_info} ({selected_stock_sector}) 기업 정보") |
| | |
| | st.info(f"{selected_stock_data['description']}") |
| |
|
| | with col2_graph: |
| | st.subheader("📈 주가 그래프") |
| | price_history = selected_stock_data["price_history"] |
| | if len(price_history) > 1: |
| | price_history_df = pd.DataFrame({ |
| | "날짜": range(1, len(price_history) + 1), |
| | "주가": price_history, |
| | }) |
| | fig = px.line( |
| | price_history_df, x="날짜", y="주가", |
| | labels={'날짜': f'거래일 (Day)', '주가': '주가 (원)'} |
| | ) |
| | fig.update_layout(margin=dict(l=0, r=0, t=30, b=0)) |
| | st.plotly_chart(fig, use_container_width=True) |
| | else: |
| | st.warning("주가 기록이 부족하여 그래프를 표시할 수 없습니다.") |
| | else: |
| | st.warning(f"'{selected_stock_all_info}' 종목 정보를 찾을 수 없습니다.") |
| |
|
| |
|
| | def display_portfolio_table(): |
| | portfolio = st.session_state.get("portfolio", {"cash": 0, "stocks": {}}) |
| | portfolio_stocks = portfolio.get("stocks", {}) |
| |
|
| | if portfolio_stocks: |
| | portfolio_data = [] |
| | total_stock_value = 0 |
| | total_purchase_value_all = 0 |
| |
|
| | for stock_name, stock_info in portfolio_stocks.items(): |
| | quantity = stock_info.get("quantity", 0) |
| | purchase_price = stock_info.get("purchase_price", 0) |
| | current_price = 0 |
| | stock_sector = "" |
| | |
| | for sector, stocks_in_sector in st.session_state.get("stocks", {}).items(): |
| | if stock_name in stocks_in_sector: |
| | current_price = stocks_in_sector[stock_name].get("current_price", 0) |
| | stock_sector = sector |
| | break |
| |
|
| | if current_price <= 0 or quantity <= 0: continue |
| |
|
| | current_value = current_price * quantity |
| | purchase_value_total = purchase_price * quantity |
| | profit_loss = current_value - purchase_value_total |
| | profit_rate = (profit_loss / purchase_value_total) * 100 if purchase_value_total > 0 else 0 |
| |
|
| | total_stock_value += current_value |
| | total_purchase_value_all += purchase_value_total |
| |
|
| | portfolio_data.append({ |
| | "종목": stock_name, |
| | "섹터": stock_sector, |
| | "보유 수량": quantity, |
| | "평균 매수가": f"{purchase_price:,.0f} 원", |
| | "현재가": f"{current_price:,.0f} 원", |
| | "평가액": f"{current_value:,.0f} 원", |
| | "손익": f"{profit_loss:,.0f} 원", |
| | "수익률": f"{profit_rate:.2f}%", |
| | }) |
| |
|
| | |
| | portfolio_data.append({ |
| | "종목": "💰 현금", "섹터": "-", "보유 수량": "-", "평균 매수가": "-", |
| | "현재가": "-", "평가액": f"{portfolio.get('cash', 0):,.0f} 원", "손익": "-", "수익률": "-", |
| | }) |
| |
|
| | portfolio_df = pd.DataFrame(portfolio_data) |
| | |
| | st.dataframe(portfolio_df[[ |
| | "종목", "섹터", "보유 수량", "평균 매수가", "현재가", "평가액", "손익", "수익률" |
| | ]], hide_index=True, use_container_width=True) |
| |
|
| | st.markdown("---") |
| | |
| | cash, total_value, total_profit_loss, total_profit_rate, initial_cash = calculate_portfolio_summary() |
| |
|
| | st.markdown(f"**💰 현금 잔고:** {cash:,.0f} 원") |
| | st.markdown(f"**📊 총 평가액 (주식 + 현금):** {total_value:,.0f} 원") |
| | st.markdown(f"**📈 총 손익:** {total_profit_loss:,.0f} 원") |
| | st.markdown(f"**🚀 총 수익률:** {total_profit_rate:.2f}%") |
| | |
| | |
| |
|
| | else: |
| | st.info("보유 주식이 없습니다. '주식 매수' 탭에서 주식을 구매해보세요!") |
| |
|
| |
|
| | |
| | def display_stock_glossary(): |
| | selected_level = st.session_state.get('selected_level', '초등') |
| | glossary = GLOSSARY.get(selected_level, GLOSSARY['초등']) |
| |
|
| | with st.sidebar.expander(f"📚 주식 용어 사전 ({LEVELS[selected_level]['name']})", expanded=False): |
| | for term, definition in glossary.items(): |
| | st.markdown(f"**{term}:** {definition}") |
| | st.markdown("---") |
| |
|
| |
|
| | |
| | def login_sidebar(): |
| | |
| | if 'user_settings' in st.session_state and st.session_state['user_settings'] is not None: |
| | st.sidebar.success(f"{st.session_state['user_id']}님, 환영합니다!") |
| | |
| | if st.sidebar.button("로그아웃"): |
| | |
| | keys_to_reset = ["user_id", "user_settings", "portfolio", "stocks", "day_count", "daily_news", "previous_daily_news", "news_meanings", "initial_cash_set"] |
| | for key in keys_to_reset: |
| | if key in st.session_state: |
| | del st.session_state[key] |
| | st.session_state['selected_level'] = "초등" |
| | st.session_state['force_reset'] = True |
| | st.rerun() |
| | return True |
| |
|
| | |
| | st.sidebar.header("로그인") |
| | account = st.sidebar.text_input("아이디", key="login_id") |
| | pw = st.sidebar.text_input("비밀번호", type="password", key="login_pw") |
| |
|
| | if st.sidebar.button("로그인", key="login_button"): |
| | if not supabase: |
| | st.error("데이터베이스 연결 오류로 로그인할 수 없습니다.") |
| | return False |
| | if not account or not pw: |
| | st.warning("아이디와 비밀번호를 입력해주세요.") |
| | return False |
| | try: |
| | response = supabase.table("users").select("*").eq("account", account).eq("pw", pw).execute() |
| | if response.data and len(response.data) > 0: |
| | user_data = response.data[0] |
| | st.session_state["user_id"] = user_data["account"] |
| | st.session_state["selected_level"] = user_data.get("level", "초등") |
| |
|
| | if "data" in user_data and user_data["data"]: |
| | try: |
| | user_settings = json.loads(user_data["data"]) |
| | |
| | for key in ["stocks", "previous_daily_news", "news_meanings", "day_count", "portfolio", "daily_news", "initial_cash_set"]: |
| | if key in user_settings: |
| | st.session_state[key] = user_settings[key] |
| | st.session_state['user_settings'] = user_settings |
| | st.success("로그인 성공! 게임 데이터를 불러왔습니다.") |
| | |
| | except json.JSONDecodeError: |
| | st.error("저장된 데이터 형식 오류. 새 게임을 시작합니다.") |
| | st.session_state['user_settings'] = {"new_user": True} |
| | initialize_session_state(st.session_state["selected_level"]) |
| | except Exception as e: |
| | st.error(f"데이터 로드 중 오류: {e}. 새 게임을 시작합니다.") |
| | st.session_state['user_settings'] = {"new_user": True} |
| | initialize_session_state(st.session_state["selected_level"]) |
| | else: |
| | st.info("저장된 게임 데이터가 없습니다. 새 게임을 시작합니다.") |
| | st.session_state['user_settings'] = {"new_user": True} |
| | initialize_session_state(st.session_state["selected_level"]) |
| |
|
| | |
| | st.session_state['selected_level'] = user_data.get("level", "초등") |
| | st.rerun() |
| | else: |
| | st.error("아이디 또는 비밀번호가 일치하지 않습니다.") |
| | except Exception as e: |
| | st.error(f"로그인 중 오류 발생: {e}") |
| | return False |
| |
|
| | |
| |
|
| | return False |
| |
|
| | def save_session_data(): |
| | if supabase and 'user_id' in st.session_state and st.session_state['user_id']: |
| | keys_to_save = ["stocks", "previous_daily_news", "news_meanings", "day_count", "portfolio", "daily_news", "selected_level", "initial_cash_set"] |
| | data_to_save = {key: st.session_state[key] for key in keys_to_save if key in st.session_state} |
| |
|
| | try: |
| | json_data = json.dumps(data_to_save, ensure_ascii=False, allow_nan=False, default=lambda o: '<not serializable>') |
| | except ValueError: |
| | def replace_nan_inf(obj): |
| | if isinstance(obj, dict): |
| | return {k: replace_nan_inf(v) for k, v in obj.items()} |
| | elif isinstance(obj, list): |
| | return [replace_nan_inf(elem) for elem in obj] |
| | elif isinstance(obj, float) and (obj != obj or obj == float('inf') or obj == float('-inf')): |
| | return None |
| | return obj |
| | cleaned_data = replace_nan_inf(data_to_save) |
| | json_data = json.dumps(cleaned_data, ensure_ascii=False, allow_nan=False, default=lambda o: '<not serializable>') |
| |
|
| | if json_data: |
| | try: |
| | supabase.table("users").update({"data": json_data}).eq("account", st.session_state["user_id"]).execute() |
| | except Exception as e: |
| | st.error(f"데이터 저장 중 오류 발생: {e}") |
| |
|
| |
|
| | |
| | def main(): |
| | |
| | is_logged_in = login_sidebar() |
| |
|
| | |
| | if not is_logged_in: |
| | st.info("사이드바에서 로그인해주세요.") |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | st.stop() |
| |
|
| | |
| |
|
| | |
| | |
| | should_initialize = False |
| | if 'user_settings' not in st.session_state or st.session_state.get('user_settings', {}).get('new_user'): |
| | should_initialize = True |
| | if 'user_settings' in st.session_state and st.session_state.get('user_settings', {}).get('new_user'): |
| | st.session_state['user_settings'].pop('new_user') |
| | elif "stocks" not in st.session_state or not st.session_state["stocks"] or "portfolio" not in st.session_state: |
| | st.warning("게임 데이터 일부가 유실되어 초기화합니다.") |
| | should_initialize = True |
| |
|
| | if should_initialize: |
| | initialize_session_state(st.session_state['selected_level']) |
| |
|
| |
|
| | |
| | current_level_index = list(LEVELS.keys()).index(st.session_state.get('selected_level', '초등')) |
| | selected_level = st.sidebar.selectbox( |
| | "학습 수준", |
| | options=list(LEVELS.keys()), |
| | format_func=lambda x: LEVELS[x]['name'], |
| | key="level_selector_post_login", |
| | index=current_level_index |
| | ) |
| |
|
| | |
| | if st.session_state.get('selected_level') != selected_level: |
| | st.session_state['selected_level'] = selected_level |
| | st.sidebar.warning("학습 수준이 변경되었습니다. 용어 사전과 뉴스/해설의 난이도가 조정됩니다.") |
| | |
| | if st.sidebar.button("⚠️ 레벨 변경 적용 (게임 초기화)", key="level_reset_confirm"): |
| | st.session_state['force_reset'] = True |
| | initialize_session_state(selected_level) |
| | save_session_data() |
| | st.success("레벨이 변경되었고 게임이 초기화되었습니다.") |
| | st.rerun() |
| | else: |
| | st.sidebar.info("게임 초기화를 원하시면 위 버튼을 눌러주세요. 누르지 않으면 현재 게임 상태는 유지됩니다.") |
| | save_session_data() |
| | st.rerun() |
| |
|
| |
|
| | |
| | st.title(f"📈 {LEVELS[selected_level]['name']} 모의 주식 투자") |
| |
|
| | |
| | with st.sidebar: |
| | st.markdown("---") |
| | st.markdown(f"### Day {st.session_state.get('day_count', 1)}") |
| | st.markdown("---") |
| | |
| | cash, total_value, total_profit_loss, total_profit_rate, _ = calculate_portfolio_summary() |
| | st.metric(label="💰 현금 잔고", value=f"{cash:,.0f} 원") |
| | st.metric(label="📊 총 평가 금액", value=f"{total_value:,.0f} 원", delta=f"{total_profit_loss:,.0f} 원") |
| | st.metric(label="🚀 총 수익률", value=f"{total_profit_rate:.2f}%") |
| | st.markdown("---") |
| |
|
| | |
| | if st.button("☀️ 하루 지나기", use_container_width=True, key="day_pass_button"): |
| | if st.session_state.get("daily_news"): |
| | current_day = st.session_state.get('day_count', 1) |
| | with st.spinner(f"Day {current_day} 마감 및 Day {current_day + 1} 준비 중..."): |
| | |
| | st.session_state["previous_daily_news"] = st.session_state["daily_news"] |
| | |
| | meanings = explain_daily_news_meanings(st.session_state["previous_daily_news"]) |
| | if meanings: |
| | st.session_state["news_meanings"] = meanings |
| | else: |
| | st.session_state["news_meanings"] = {} |
| | |
| | update_stock_prices() |
| | |
| | st.session_state["daily_news"] = generate_news() |
| | |
| | st.session_state["day_count"] = current_day + 1 |
| | |
| | save_session_data() |
| | st.success(f"Day {st.session_state['day_count']} 시작! 주가가 변동되었고 새로운 뉴스가 생성되었습니다.") |
| | st.toast("새로운 하루가 시작되었습니다!", icon="🌅") |
| | st.rerun() |
| | else: |
| | st.warning("오늘의 뉴스를 먼저 생성해주세요.") |
| | st.markdown("---") |
| |
|
| | |
| | display_stock_glossary() |
| |
|
| | |
| | with st.sidebar.expander("🚀 앱 사용 가이드", expanded=False): |
| | |
| | st.markdown( |
| | """ |
| | **1단계: 뉴스 생성하기** |
| | - 왼쪽 '오늘의 뉴스' 영역에서 '뉴스 생성' 버튼을 클릭하세요. |
| | - AI가 현재 설정된 학습 수준에 맞춰 주식 시장 뉴스를 5개 만들어줍니다. |
| | |
| | **2단계: 뉴스 읽고 예측하기** |
| | - 생성된 뉴스를 꼼꼼히 읽어보세요. |
| | - '어떤 종류의 회사가 이득을 볼까?' 또는 '어떤 회사가 어려울까?' 생각해보세요. |
| | - 뉴스를 통해 경제 흐름을 읽는 연습을 할 수 있습니다. |
| | |
| | **3단계: 주가 및 기업 정보 확인하기** |
| | - 메인 화면의 '📈 현재 주가' 탭에서 주식들의 현재 가격과 변동률을 확인하세요. |
| | - 관심 있는 종목을 선택하면 해당 기업에 대한 설명(수준별)과 주가 그래프를 볼 수 있습니다. |
| | |
| | **4단계: 주식 매수하기** |
| | - '💰 주식 매수' 탭에서 원하는 종목과 수량을 선택 후 '주식 매수' 버튼을 누르세요. |
| | - 정말 매수할지 확인 창이 뜹니다. '매수 확인'을 누르면 거래가 완료됩니다. |
| | - **팁:** 뉴스를 보고 유망하다고 생각되는 섹터의 주식을 골라보세요! |
| | |
| | **5단계: 내 포트폴리오 확인하기** |
| | - '📊 내 포트폴리오' 탭에서 내가 가진 주식과 현금 상황을 확인하세요. |
| | - 각 주식의 수익률과 총 자산 변화를 볼 수 있습니다. |
| | |
| | **6단계: 주식 매도하기** |
| | - '📉 주식 매도' 탭에서 팔고 싶은 주식과 수량을 선택 후 '주식 매도' 버튼을 누르세요. |
| | - 확인 창에서 '매도 확인'을 누르면 주식을 팔고 현금을 얻습니다. |
| | - **팁:** 주가가 충분히 올랐다고 생각될 때 팔아 이익을 실현해보세요! |
| | |
| | **7단계: 하루 지나기 & 뉴스 해설 보기** |
| | - 사이드바의 '☀️ 하루 지나기' 버튼을 클릭하면 시간이 흐릅니다. |
| | - 주가가 변동되고, 새로운 뉴스가 생성됩니다. |
| | - '📰 어제 뉴스 해설' 탭에서 AI가 분석한 이전 날 뉴스의 의미와 관련 섹터를 확인해보세요. (수준별 해설 제공) |
| | |
| | **꾸준히 학습하기! 🌱** |
| | - 매일 뉴스를 읽고, 투자를 결정하고, 결과를 확인하는 과정을 반복하며 경제와 투자에 대한 감각을 키워보세요! |
| | - 모르는 용어는 '📚 주식 용어 사전'을 참고하세요. |
| | """ |
| | ) |
| |
|
| | |
| | col_news, col_main_ui = st.columns([1, 2]) |
| |
|
| | with col_news: |
| | st.header(f"📰 Day {st.session_state.get('day_count', 1)} 뉴스") |
| | |
| | if st.button("오늘의 뉴스 생성하기", use_container_width=True, key="news_gen_button", help="AI가 오늘의 경제 뉴스를 생성합니다."): |
| | with st.spinner(f"Day {st.session_state.get('day_count', 1)} 뉴스 생성 중... (수준: {LEVELS[selected_level]['name']})"): |
| | st.session_state["daily_news"] = generate_news() |
| | st.session_state["news_meanings"] = {} |
| | save_session_data() |
| | st.rerun() |
| |
|
| | |
| | if st.session_state.get("daily_news"): |
| | st.markdown("---") |
| | st.subheader("오늘의 주요 뉴스") |
| | for i, news in enumerate(st.session_state["daily_news"]): |
| | with st.expander(f"**뉴스 {i+1}**", expanded=(i==0)): |
| | st.write(news) |
| | st.markdown("---") |
| | st.info("💡 뉴스를 읽고 어떤 섹터/기업에 영향이 있을지 예측해보세요! '하루 지나기' 후 '어제 뉴스 해설' 탭에서 AI 분석을 확인할 수 있습니다.") |
| | else: |
| | st.info("👆 '오늘의 뉴스 생성하기' 버튼을 눌러 뉴스를 받아보세요.") |
| |
|
| |
|
| | with col_main_ui: |
| | |
| | tab_titles = ['📈 현재 주가', '📊 내 포트폴리오', '💰 주식 매수', '📉 주식 매도', '📰 어제 뉴스 해설'] |
| | tabs = st.tabs(tab_titles) |
| |
|
| | with tabs[0]: |
| | st.subheader("📈 현재 주가 및 기업 정보") |
| | st.markdown("실시간 주가 변동과 기업 정보를 확인하세요. 종목 선택 시 상세 정보가 표시됩니다.") |
| | display_stock_prices() |
| |
|
| | with tabs[1]: |
| | st.subheader("📊 내 포트폴리오") |
| | st.markdown("보유 중인 주식과 자산 현황을 확인하세요.") |
| | display_portfolio_table() |
| |
|
| | with tabs[2]: |
| | st.subheader("💰 주식 매수") |
| | st.markdown("투자하고 싶은 주식을 매수해보세요.") |
| |
|
| | |
| | sector_names = ["섹터 선택..."] + list(st.session_state.get("stocks", {}).keys()) |
| | selected_sector_buy = st.selectbox("1. 매수할 섹터 선택:", sector_names, key="buy_sector") |
| |
|
| | if selected_sector_buy != "섹터 선택...": |
| | stock_names_in_sector = ["종목 선택..."] + list(st.session_state.get("stocks", {}).get(selected_sector_buy, {}).keys()) |
| | selected_stock_buy = st.selectbox("2. 매수할 종목 선택:", stock_names_in_sector, key="buy_stock") |
| |
|
| | if selected_stock_buy != "종목 선택...": |
| | stock_info_buy = st.session_state.get("stocks", {}).get(selected_sector_buy, {}).get(selected_stock_buy) |
| | if stock_info_buy: |
| | stock_price_buy = stock_info_buy.get("current_price", 0) |
| | |
| | description_key = f"description_{selected_level}" |
| | stock_description = stock_info_buy.get(description_key, stock_info_buy.get("description_중등","설명 없음")) |
| |
|
| | st.info(f"**{selected_stock_buy}** 현재 주가: **{stock_price_buy:,.0f}원**") |
| | st.caption(f"기업 정보: {stock_description}") |
| |
|
| | |
| | available_cash = st.session_state.get("portfolio", {}).get("cash", 0) |
| | max_buy_quantity = available_cash // stock_price_buy if stock_price_buy > 0 else 0 |
| | st.caption(f"현금 잔고: {available_cash:,.0f}원 (최대 {max_buy_quantity}주 매수 가능)") |
| |
|
| | quantity_buy = st.number_input( |
| | f"3. 매수 수량 입력 (최대 {max_buy_quantity}주):", |
| | min_value=1, |
| | max_value=max(1, max_buy_quantity), |
| | value=1, |
| | step=1, |
| | key="buy_quantity", |
| | disabled=(max_buy_quantity == 0) |
| | ) |
| |
|
| | total_buy_price = stock_price_buy * quantity_buy |
| | st.markdown(f"**예상 매수 금액:** {total_buy_price:,.0f} 원") |
| |
|
| | |
| | if not st.session_state.get('buy_confirm', False): |
| | if st.button("주식 매수", use_container_width=True, key='buy_button_confirm', disabled=(max_buy_quantity == 0 or quantity_buy <= 0)): |
| | if quantity_buy > max_buy_quantity: |
| | st.error(f"매수 가능 수량 초과! (최대 {max_buy_quantity}주)") |
| | elif quantity_buy <= 0: |
| | st.error("매수 수량은 1주 이상이어야 합니다.") |
| | else: |
| | st.session_state['buy_confirm'] = True |
| | st.rerun() |
| | else: |
| | st.warning(f"**{selected_stock_buy} {quantity_buy}주**를 **{total_buy_price:,.0f}원**에 매수하시겠습니까?") |
| | col_confirm, col_cancel = st.columns([1, 1]) |
| | with col_confirm: |
| | if st.button("✅ 네, 매수합니다", use_container_width=True, key='buy_confirm_button'): |
| | buy_stock(selected_stock_buy, quantity_buy, selected_sector_buy) |
| | st.rerun() |
| | with col_cancel: |
| | if st.button("❌ 아니요, 취소합니다", use_container_width=True, key='buy_cancel_button'): |
| | st.session_state['buy_confirm'] = False |
| | st.info("매수를 취소했습니다.") |
| | st.rerun() |
| | else: |
| | st.warning("선택한 종목 정보를 불러올 수 없습니다.") |
| | else: |
| | st.info("먼저 매수할 섹터를 선택해주세요.") |
| |
|
| |
|
| | with tabs[3]: |
| | st.subheader("📉 주식 매도") |
| | st.markdown("보유 중인 주식을 판매하여 현금화하세요.") |
| |
|
| | portfolio_stocks = st.session_state.get("portfolio", {}).get("stocks", {}) |
| | if portfolio_stocks: |
| | owned_stock_names = ["종목 선택..."] + list(portfolio_stocks.keys()) |
| | selected_stock_sell = st.selectbox("1. 매도할 종목 선택:", owned_stock_names, key="sell_stock") |
| |
|
| | if selected_stock_sell != "종목 선택...": |
| | stock_info_sell = portfolio_stocks.get(selected_stock_sell) |
| | if stock_info_sell: |
| | owned_quantity = stock_info_sell.get("quantity", 0) |
| | purchase_price_avg = stock_info_sell.get("purchase_price", 0) |
| |
|
| | |
| | current_price_sell = 0 |
| | for sector, stocks_in_sector in st.session_state.get("stocks", {}).items(): |
| | if selected_stock_sell in stocks_in_sector: |
| | current_price_sell = stocks_in_sector[selected_stock_sell].get("current_price", 0) |
| | break |
| |
|
| | st.info(f"**{selected_stock_sell}** 보유 수량: **{owned_quantity}주**") |
| | st.caption(f"평균 매수가: {purchase_price_avg:,.0f}원 / 현재가: {current_price_sell:,.0f}원") |
| |
|
| | quantity_sell = st.number_input( |
| | f"2. 매도 수량 입력 (최대 {owned_quantity}주):", |
| | min_value=1, |
| | max_value=owned_quantity, |
| | value=1, |
| | step=1, |
| | key="sell_quantity", |
| | disabled=(owned_quantity == 0) |
| | ) |
| |
|
| | total_sell_price = current_price_sell * quantity_sell |
| | st.markdown(f"**예상 매도 금액:** {total_sell_price:,.0f} 원") |
| |
|
| | |
| | if not st.session_state.get('sell_confirm', False): |
| | if st.button("주식 매도", use_container_width=True, key='sell_button_confirm', disabled=(owned_quantity == 0 or quantity_sell <= 0)): |
| | if quantity_sell > owned_quantity: |
| | st.error(f"매도 가능 수량 초과! (최대 {owned_quantity}주)") |
| | elif quantity_sell <= 0: |
| | st.error("매도 수량은 1주 이상이어야 합니다.") |
| | else: |
| | st.session_state['sell_confirm'] = True |
| | st.rerun() |
| | else: |
| | st.warning(f"**{selected_stock_sell} {quantity_sell}주**를 **{total_sell_price:,.0f}원**에 매도하시겠습니까?") |
| | col_confirm, col_cancel = st.columns([1, 1]) |
| | with col_confirm: |
| | if st.button("✅ 네, 매도합니다", use_container_width=True, key='sell_confirm_button'): |
| | sell_stock(selected_stock_sell, quantity_sell) |
| | st.rerun() |
| | with col_cancel: |
| | if st.button("❌ 아니요, 취소합니다", use_container_width=True, key='sell_cancel_button'): |
| | st.session_state['sell_confirm'] = False |
| | st.info("매도를 취소했습니다.") |
| | st.rerun() |
| | else: |
| | st.warning("선택한 보유 주식 정보를 찾을 수 없습니다.") |
| | else: |
| | st.info("매도할 주식이 없습니다. 먼저 주식을 매수하세요.") |
| |
|
| | with tabs[4]: |
| | st.subheader(f"📰 Day {st.session_state.get('day_count', 1) - 1} 뉴스 해설") |
| | st.markdown(f"AI가 분석한 어제 뉴스의 의미와 관련 섹터입니다. ({LEVELS[selected_level]['name']} 수준)") |
| |
|
| | previous_daily_news = st.session_state.get("previous_daily_news") |
| | news_meanings = st.session_state.get("news_meanings") |
| |
|
| | if previous_daily_news and news_meanings: |
| | if len(previous_daily_news) == len(news_meanings): |
| | for i in range(len(previous_daily_news)): |
| | news_index_str = str(i + 1) |
| | meaning_data = news_meanings.get(news_index_str) |
| | if meaning_data: |
| | with st.expander(f"**뉴스 {news_index_str}**", expanded=False): |
| | st.markdown("**어제 뉴스 원문:**") |
| | st.write(previous_daily_news[i]) |
| | st.markdown("---") |
| | st.markdown("**AI 해설:**") |
| | explanation = meaning_data.get("explanation", "해설 없음") |
| | sectors = meaning_data.get("sectors", []) |
| | st.info(explanation) |
| | if sectors: |
| | st.markdown("**관련 섹터:**") |
| | st.success(f"{', '.join(sectors)}") |
| | else: |
| | st.markdown("**관련 섹터:** 없음") |
| | else: |
| | st.warning(f"뉴스 {news_index_str}에 대한 해설 데이터를 찾을 수 없습니다.") |
| | else: |
| | st.warning("뉴스 개수와 해설 개수가 일치하지 않습니다. 데이터 오류 가능성이 있습니다.") |
| |
|
| | elif st.session_state.get('day_count', 1) == 1: |
| | st.info("첫 날에는 이전 뉴스가 없습니다. '하루 지나기'를 눌러 다음 날로 이동하세요.") |
| | else: |
| | st.info("아직 어제 뉴스에 대한 해설이 생성되지 않았습니다. '하루 지나기'를 진행했는지 확인해주세요.") |
| |
|
| | st.markdown("본 게임은 실제 투자가 아닌 학습용 시뮬레이션으로, 초·중·고등학생의 금융 이해력 향상과 합리적 판단력 함양을 위한 교육 활동입니다. OECD와 유럽연합은 조기 금융교육의 중요성을 강조하고 있으며, 국내 또한 관련 교육과정이 확대되고 있습니다. 본 활동은 이러한 흐름에 맞춘 체험 중심 경제·금융학습입니다.") |
| |
|
| |
|
| | if __name__ == "__main__": |
| | |
| | if 'selected_level' not in st.session_state: |
| | st.session_state['selected_level'] = "초등" |
| | main() |