콘텐츠로 이동
20 posts in the category “Dev”

LangGraph와 LangSmith 연결하기: AI 에이전트 실행 추적과 디버깅

LangGraph 상태 그래프와 LangSmith 실행 추적을 표현한 이미지

LangGraph로 AI 에이전트를 만들면 단순한 함수 호출보다 복잡한 흐름을 구성할 수 있습니다. 상태(state)를 여러 노드가 읽고 수정하고, 조건에 따라 다른 노드로 이동하거나 사람의 검토를 기다릴 수도 있습니다. 문제는 실행 결과만 봐서는 어느 노드에서 잘못된 판단이 시작됐는지 찾기 어렵다는 점입니다.

이때 LangSmith를 연결하면 에이전트 실행을 단계별 trace로 확인할 수 있습니다. 각 노드의 입력과 출력, 실행 시간, 오류를 한 화면에서 비교할 수 있어 프롬프트와 워크플로를 개선하기가 쉬워집니다.

두 도구는 서로 대체 관계가 아닙니다.

도구담당 영역
LangGraph상태, 노드, 엣지, 조건 분기, 재개 가능한 실행 흐름
LangSmith실행 trace, 프롬프트·응답 확인, 평가, 모니터링

LangGraph가 에이전트의 실행 구조라면 LangSmith는 그 실행을 관찰하는 기록 장치입니다. 특히 여러 번의 LLM 호출이나 도구 호출이 이어지는 에이전트에서는 최종 답변보다 중간 단계의 기록이 더 중요합니다.

Python 환경에서 다음 패키지를 설치합니다.

Terminal window
pip install -U langgraph langsmith

LangSmith 프로젝트에서 발급한 API 키는 소스 코드에 직접 넣지 않고 환경 변수로 관리합니다.

Terminal window
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="lsv2_..."
export LANGSMITH_PROJECT="quantylab-agent-dev"

LANGSMITH_PROJECT를 지정하면 실행 결과를 애플리케이션별 프로젝트로 분리할 수 있습니다. 개발·스테이징·운영 환경마다 프로젝트 이름을 나누면 실험 trace와 실제 사용자 요청을 섞지 않을 수 있습니다.

다음 그래프는 입력된 질문을 받아 답변을 만드는 간단한 예제입니다.

from typing import TypedDict
from langgraph.graph import END, START, StateGraph
class State(TypedDict):
question: str
answer: str
def answer_question(state: State) -> dict:
# 실제 애플리케이션에서는 이곳에서 LLM을 호출합니다.
return {"answer": f"질문을 처리했습니다: {state['question']}"}
builder = StateGraph(State)
builder.add_node("answer_question", answer_question)
builder.add_edge(START, "answer_question")
builder.add_edge("answer_question", END)
graph = builder.compile()
result = graph.invoke({"question": "LangGraph란 무엇인가?", "answer": ""})
print(result["answer"])

LangSmith 추적을 활성화한 상태에서 graph.invoke()를 실행하면 그래프 실행이 프로젝트에 기록됩니다. LangGraph와 LangChain 통합을 사용하는 경우에는 노드 안의 모델 호출도 부모 실행의 하위 trace로 나타납니다.

LangGraph 노드가 직접 호출하는 데이터 조회, 문서 검색, 후처리 함수는 @traceable로 감싸면 별도의 실행 단위로 기록할 수 있습니다.

from langsmith import traceable
@traceable(name="load-market-context", run_type="tool")
def load_market_context(symbol: str) -> dict:
# 데이터베이스나 외부 API 조회
return {"symbol": symbol, "context": "시장 데이터"}
def research_node(state: dict) -> dict:
context = load_market_context(state["symbol"])
return {"context": context}

이렇게 하면 research_node 안에서 실행된 데이터 조회를 따로 확인할 수 있습니다. 응답이 이상할 때 모델 프롬프트만 의심하지 않고, 검색 결과와 데이터 변환 결과까지 역추적할 수 있습니다.

첫째, API 키와 비밀번호를 graph state에 넣지 않습니다. state는 체크포인터나 디버깅 출력에 저장될 수 있으므로 인증 정보 대신 식별자와 상태만 전달해야 합니다.

둘째, 개인정보와 원문 데이터의 전송 범위를 확인합니다. LangSmith에 기록되는 입력·출력에는 사용자 질문과 검색 문서가 포함될 수 있으므로 필요한 경우 민감한 필드를 마스킹하거나 선택적 tracing을 사용합니다.

셋째, 프로젝트와 태그를 일관되게 사용합니다. 예를 들어 quantylab-agent-dev, quantylab-agent-prod를 분리하고 모델 버전, 데이터 기준일, 실험 이름을 metadata나 tag로 남기면 실행 결과 비교가 쉬워집니다.

LangGraph는 복잡한 AI 에이전트의 실행 흐름을 명시적으로 만들고, LangSmith는 그 흐름을 관찰하고 개선하게 해줍니다. 먼저 LANGSMITH_TRACING과 프로젝트를 설정한 뒤 그래프 전체를 확인하고, 원인 분석이 필요한 함수에 @traceable을 추가하는 순서가 가장 실용적입니다.

공식 문서도 함께 참고하면 좋습니다.

윈도우에서 리눅스(우분투) 사용하자! WSL(Windows Subsystem for Linux) 소개

개발을 주로 리눅스에서 하는 개발자들이 많을 것입니다. 여러모로 리눅스는 개발 편의성과 생산성을 높여 줍니다. 그러나 개발 외적으로는 리눅스가 불편한 점이 많아 윈도우를 포기할 수도 없습니다. 윈도우를 포기할 수 없는 이유 3가지를 들어 보겠습니다.

  1. 한국의 많은 사이트들(특히 공공기관 사이트들)이 윈도우에서만 사용가능하거나 윈도우에서 편리하다. (Ex. 연말정산)
  2. 윈도우에 최적화된 MS Office를 꼭 써야 한다.
  3. 게임…

다행히도 윈도우에서 리눅스를 사용할 수 있는 다양한 방법이 있습니다.

  1. Virtual Machine: VMWare, Virtual Box 등
  2. Docker Container
  3. WSL (Windows Subsystem for Linux)

여기서 Virtual Machine을 이용한 방법은 많은 분들이 이미 알고 계실거라 생각합니다. 그러나 Virtual Machine에게 컴퓨터의 리소스를 나눠줌으로써 (정말 고사양 컴퓨터가 아니라면) 성능 문제가 생기기 쉽습니다.

Docker Container는 개발 환경을 독립적으로 유지하기에 좋지만 번거로운 감이 있습니다.

그래서 이 포스트에서는 WSL을 이용하여 윈도우에서 우분투를 쉽게 사용하는 방법을 소개하고자 합니다.

먼저 WSL을 활성화 시켜야 합니다. 윈도우 설정 > 앱 > 프로그램 및 기능 > Windows 기능 켜기/끄기로 갑니다. 아니면 윈도우 키 > "Windows 기능 켜기/끄기" 검색해도 됩니다.

Windows 기능 켜기/끄기

이렇게 WSL을 설치하고 나면 Microsoft Store에서 Ubuntu를 설치할 수 있습니다.

Microsoft Store

Ubuntu는 가장 최신의 LTS 버전이므로 가장 왼쪽에 있는 Ubuntu를 설치하면 됩니다. 버전이 명시되어 있는 Ubuntu를 설치해도 됩니다.

Ubuntu를 설치하고 나면 다음과 같이 bash 터미널을 띄울 수 있습니다.

ubuntu bash

이제 윈도우에서도 우분투를 맘껏 사용하시면 됩니다. 글을 쓰는 시점에서 아직 하나 아쉬운 점은 WSL에서 tensorflow-gpu를 사용할 수 없다는 것입니다. 이 부분에 대해서는 MS에서 GPU 관련 개선을 진행중이라 하니 기다려보면 되겠습니다.

WSL 우분투의 파일시스템은 C:\Users\<사용자이름>\AppData\Local\Packages 아래에 CanonicalGroupLimited.Ubuntu로 시작하는 폴더를 찾아보면 됩니다. 이 폴더에서 LocalState\rootfs를 보면 우분투 bash에서 보던 파일 구조를 확인할 수 있습니다.

장고(Django) 환경의 Jupyter Notebook 실행하기

Django로 웹개발을 하다보면 Django의 ORM(Object-relational Mapping)을 자주 사용하게 됩니다. Django의 ORM 관련 문서는 여기를 참고해 주세요.

from django.db import models 모듈의 Model 클래스로 DB를 쉽게 구축할 수 있습니다. 그럼에도 DB 구축 과정에서 테스트성 작업도 필요하게 됩니다. 예를들어, 테스트 데이터를 생성해 보거나, 모델을 이용하여 질의(Query)를 만들어 보거나, 데이터를 수정해 보거나, 테스트로 입력한 데이터들을 삭제하거나 등입니다. 즉, 데이터 CRUD(Create, Retrieve, Update, Delete)를 테스트 해볼일이 많습니다.

Django 설정이 로딩된 상태에서만 모델들을 사용할 수 있습니다. 이들을 테스트하기 위해서 Django View에서 테스트 코드를 작성하고 GET 호출을 하는 번거롭고 비효율적인 방법으로 테스트할 수도 있습니다만, 보다 효과적인 방법을 소개합니다.

Terminal window
python manage.py shell

이렇게 Django 프로젝트의 설정이 로딩된 쉘을 실행하고, 이 위에서 모델들을 다룰 수 있습니다.

먼저 django-extensions를 설치합니다.

Terminal window
pip install django-extensions

그리고 settings.py에서 INSTALLED_APPS 리스트에 django_extensions를 추가합니다.

INSTALLED_APPS = [
...
'django_extensions',
]

이제 다음과 같이 shell_plus를 실행하면 관련있는 Django 모듈들을 자동으로 임포트해 줍니다.

Terminal window
python manage.py shell_plus

3. django-extensions의 shell_plus + Jupyter Notebook

섹션 제목: “3. django-extensions의 shell_plus + Jupyter Notebook”

Django 환경을 Jupyter Notebook(Lab)으로 사용할 수 있습니다.

Terminal window
python manage.py shell_plus --notebook

bokeh로 봉차트(Candlestick Chart) 그리기

파이썬에서 봉차트를 제공하는 라이브러리가 많지 않습니다. 필자는 matplotlib에서 떨어져 나온 mpl_finance와 bokeh 정도로 알고 있습니다. 이미 mpl_finance로 봉차트를 그리는 방법은 이전 포스트 Matplotlib으로 봉차트(Candlestick Chart) 그리기 에서 다루었습니다.

이번 포스트에서 bokeh로 봉차트를 그리는 방법에 대해서 다루겠습니다. bokeh는 Anaconda에서 개발 중인 차트 라이브러리 입니다. matplotlib과 matplotlib 기반인 seaborn과는 다르게 독자 노선을 가지는 라이브러리 입니다. bokeh는 웹에 최적화 되어있는 라이브러리 입니다. matplotlib을 웹브라우저에 띄우려면 차트를 png 등의 그림으로 변환하여 보여줘야 해서 응답형(reponsible) 차트를 웹에 띄우기가 어렵습니다. 그래서 보통 웹에 차트를 띄울 때는 데이터만 서버에서 받고 Chart.js와 D3.js 같은 자바스크립트 라이브러리를 사용하여 데이터를 가시화 합니다. bokeh는 파이썬에서 차트를 가시화하고 그 결과를 쉽게 웹에 띄울 수 있는 여러 방법을 제공합니다. 이 포스트에서는 다루지 않겠습니다.

bokeh에서 봉차트를 제공하고 있습니다. bokeh 봉차트 문서에서 상세한 정보를 확인해 보세요.

먼저 Anaconda3가 설치되어 있는 상태라 가정하고 글을 이어나가겠습니다. Jupyter Lab(Notebook)에서 bokeh가 잘 설치되어 있는지 확인합니다.

from bokeh.io import output_notebook, show
from bokeh.plotting import figure, gridplot
output_notebook()

BokehJS 0.12.16 successfully loaded.

이렇게 로딩이 성공되었다는 메시지가 뜨는지 확인합니다. 만약 다음과 같은 메시지가 뜬다면 Jupyter Lab의 Extension을 설치해야 합니다.

Loading BokehJS ...
JavaScript output is disabled in JupyterLab

Anaconda Prompt에서 다음과 같이 jupyterlab_bokeh 확장을 설치합니다.

Terminal window
jupyter labextension install jupyterlab_bokeh

설치가 되지 않는다면 아마도 node.js가 설치되어 있지 않아서일 것입니다. 그렇다면 conda install nodejs를 통해 먼저 node.js를 설치하고 다시 jupyterlab_bokeh 확장을 설치합니다.

bokeh가 잘 설치되어 있다면 이제 봉차트를 그릴 준비가 되었습니다. 차트 데이터는 다음과 같이 ['date', 'open', 'high', 'low', 'close', 'volume']의 리스트로 있습니다.

import pandas as pd
data = [['20190219', 125000.0, 127500.0, 123500.0, 126000.0, 57757], ['20190220', 125000.0, 127000.0, 124500.0, 126000.0, 68453], ['20190221', 125500.0, 126000.0, 124000.0, 125000.0, 43961], ['20190222', 125000.0, 125000.0, 123500.0, 125000.0, 31065], ['20190225', 125500.0, 126000.0, 124000.0, 125500.0, 45852], ['20190226', 125000.0, 127000.0, 124500.0, 126500.0, 37404], ['20190227', 126500.0, 127000.0, 124500.0, 126000.0, 36131], ['20190228', 126500.0, 127000.0, 124500.0, 125000.0, 69474], ['20190304', 124500.0, 125500.0, 122500.0, 123500.0, 65517], ['20190305', 123000.0, 125000.0, 122000.0, 124500.0, 35186], ['20190306', 124000.0, 124500.0, 122500.0, 123500.0, 35449], ['20190307', 123500.0, 124000.0, 122000.0, 123500.0, 34768], ['20190308', 122500.0, 122500.0, 120500.0, 121500.0, 35118], ['20190311', 121500.0, 122500.0, 120000.0, 122500.0, 39576], ['20190312', 123000.0, 124000.0, 122000.0, 123500.0, 24117], ['20190313', 123000.0, 123500.0, 121500.0, 123500.0, 37649], ['20190314', 123000.0, 124000.0, 122000.0, 123500.0, 95132], ['20190315', 123000.0, 128000.0, 123000.0, 126500.0, 107246], ['20190318', 127000.0, 131000.0, 126500.0, 131000.0, 74644], ['20190319', 130000.0, 134000.0, 129500.0, 133000.0, 68348], ['20190320', 132000.0, 133000.0, 129500.0, 131000.0, 42697], ['20190321', 130000.0, 132000.0, 127500.0, 128500.0, 54018], ['20190322', 127500.0, 129500.0, 126500.0, 127000.0, 32380], ['20190325', 125500.0, 126500.0, 124000.0, 124500.0, 37185], ['20190326', 124500.0, 125500.0, 123500.0, 124000.0, 45161], ['20190327', 124000.0, 125000.0, 123500.0, 124000.0, 34336], ['20190328', 124000.0, 124500.0, 120500.0, 121500.0, 43518], ['20190329', 122500.0, 125000.0, 122000.0, 124500.0, 39035], ['20190401', 124000.0, 126500.0, 124000.0, 126500.0, 22463], ['20190402', 125500.0, 126500.0, 123500.0, 126000.0, 31754], ['20190403', 124500.0, 128000.0, 124500.0, 128000.0, 36250], ['20190404', 128500.0, 128500.0, 126000.0, 128000.0, 34854], ['20190405', 127500.0, 129000.0, 126000.0, 127500.0, 33513], ['20190408', 127500.0, 128000.0, 126000.0, 128000.0, 39005], ['20190409', 128000.0, 129000.0, 127500.0, 128500.0, 33266], ['20190410', 128000.0, 129000.0, 126500.0, 128000.0, 64476], ['20190411', 128500.0, 129000.0, 125000.0, 125000.0, 84802], ['20190412', 126000.0, 127500.0, 125500.0, 127000.0, 39663], ['20190415', 126500.0, 128500.0, 126000.0, 127000.0, 61140], ['20190416', 127000.0, 129000.0, 126500.0, 128500.0, 40123], ['20190417', 128500.0, 129000.0, 127000.0, 128000.0, 30846], ['20190418', 128000.0, 128500.0, 124000.0, 124500.0, 55346], ['20190419', 124500.0, 125000.0, 122000.0, 123000.0, 53439], ['20190422', 123000.0, 123500.0, 121000.0, 122500.0, 30421], ['20190423', 122500.0, 123500.0, 120500.0, 121500.0, 54997], ['20190424', 122500.0, 122500.0, 119500.0, 120000.0, 63486], ['20190425', 121000.0, 121000.0, 118500.0, 119000.0, 36046], ['20190426', 118500.0, 119500.0, 117000.0, 119000.0, 43749], ['20190429', 119000.0, 119500.0, 117000.0, 119500.0, 33516], ['20190430', 122000.0, 123000.0, 119000.0, 119500.0, 94118], ['20190502', 118500.0, 122000.0, 118000.0, 121000.0, 56723], ['20190503', 121000.0, 122000.0, 119500.0, 120000.0, 35240], ['20190507', 118500.0, 119500.0, 117000.0, 117500.0, 44453], ['20190508', 116500.0, 117000.0, 115000.0, 116500.0, 52805], ['20190509', 117000.0, 117000.0, 113000.0, 113000.0, 116012], ['20190510', 113000.0, 114500.0, 110000.0, 111500.0, 86072], ['20190513', 110500.0, 110500.0, 107500.0, 108500.0, 70847], ['20190514', 107500.0, 108000.0, 105000.0, 106500.0, 92820], ['20190515', 107000.0, 107500.0, 105000.0, 107000.0, 68937], ['20190516', 107000.0, 107500.0, 104000.0, 105000.0, 64047]]
df = pd.DataFrame(data, columns=['date', 'open', 'high', 'low', 'close', 'volume'])
dateopenhighlowclosevolume
20190219125000.0127500.0123500.0126000.057757
20190220125000.0127000.0124500.0126000.068453
20190221125500.0126000.0124000.0125000.043961
20190222125000.0125000.0123500.0125000.031065
20190225125500.0126000.0124000.0125500.045852
20190226125000.0127000.0124500.0126500.037404
20190227126500.0127000.0124500.0126000.036131
20190228126500.0127000.0124500.0125000.069474
20190304124500.0125500.0122500.0123500.065517
20190305123000.0125000.0122000.0124500.035186
20190306124000.0124500.0122500.0123500.035449
20190307123500.0124000.0122000.0123500.034768
20190308122500.0122500.0120500.0121500.035118
20190311121500.0122500.0120000.0122500.039576
20190312123000.0124000.0122000.0123500.024117
20190313123000.0123500.0121500.0123500.037649
20190314123000.0124000.0122000.0123500.095132
20190315123000.0128000.0123000.0126500.0107246
20190318127000.0131000.0126500.0131000.074644
20190319130000.0134000.0129500.0133000.068348
20190320132000.0133000.0129500.0131000.042697
20190321130000.0132000.0127500.0128500.054018
20190322127500.0129500.0126500.0127000.032380
20190325125500.0126500.0124000.0124500.037185
20190326124500.0125500.0123500.0124000.045161
20190327124000.0125000.0123500.0124000.034336
20190328124000.0124500.0120500.0121500.043518
20190329122500.0125000.0122000.0124500.039035
20190401124000.0126500.0124000.0126500.022463
20190402125500.0126500.0123500.0126000.031754
20190403124500.0128000.0124500.0128000.036250
20190404128500.0128500.0126000.0128000.034854
20190405127500.0129000.0126000.0127500.033513
20190408127500.0128000.0126000.0128000.039005
20190409128000.0129000.0127500.0128500.033266
20190410128000.0129000.0126500.0128000.064476
20190411128500.0129000.0125000.0125000.084802
20190412126000.0127500.0125500.0127000.039663
20190415126500.0128500.0126000.0127000.061140
20190416127000.0129000.0126500.0128500.040123
20190417128500.0129000.0127000.0128000.030846
20190418128000.0128500.0124000.0124500.055346
20190419124500.0125000.0122000.0123000.053439
20190422123000.0123500.0121000.0122500.030421
20190423122500.0123500.0120500.0121500.054997
20190424122500.0122500.0119500.0120000.063486
20190425121000.0121000.0118500.0119000.036046
20190426118500.0119500.0117000.0119000.043749
20190429119000.0119500.0117000.0119500.033516
20190430122000.0123000.0119000.0119500.094118
20190502118500.0122000.0118000.0121000.056723
20190503121000.0122000.0119500.0120000.035240
20190507118500.0119500.0117000.0117500.044453
20190508116500.0117000.0115000.0116500.052805
20190509117000.0117000.0113000.0113000.0116012
20190510113000.0114500.0110000.0111500.086072
20190513110500.0110500.0107500.0108500.070847
20190514107500.0108000.0105000.0106500.092820
20190515107000.0107500.0105000.0107000.068937
20190516107000.0107500.0104000.0105000.064047

bokeh를 이용해 봉차트를 그려봅니다. 먼저 데이터에서 양봉과 음봉에 대한 mask를 저장합니다.

inc = df.close >= df.open
dec = df.open > df.close

inc는 양봉, dec는 음봉에 해당합니다. 이제 봉들을 그려줍니다. 하나의 봉은 bokeh에서 segment와 vbar로 구성되어 있습니다. segment는 high와 low까지 선으로 그은 봉의 꼬리를 그리는데 사용됩니다. vbar로는 봉의 몸통을 그립니다.

p_candlechart = figure(plot_width=1050, plot_height=200, x_range=(-1, len(df)), tools="crosshair")
p_candlechart.segment(df.index[inc], df.high[inc], df.index[inc], df.low[inc], color="red")
p_candlechart.segment(df.index[dec], df.high[dec], df.index[dec], df.low[dec], color="blue")
p_candlechart.vbar(df.index[inc], 0.5, df.open[inc], df.close[inc], fill_color="red", line_color="red")
p_candlechart.vbar(df.index[dec], 0.5, df.open[dec], df.close[dec], fill_color="blue", line_color="blue")

봉차트 밑에 거래량 막대차트(Bar Chart)까지 그려 줍니다. 이 때도 vbar를 사용하면 됩니다.

p_volumechart = figure(plot_width=1050, plot_height=100, x_range=p_candlechart.x_range, tools="crosshair")
p_volumechart.vbar(df.index, 0.5, df.volume, fill_color="black", line_color="black")

그리고 마지막으로 위 차트들을 gridplot으로 배치하여 가시화합니다.

p = gridplot([[p_candlechart], [p_volumechart]], toolbar_location=None)
show(p)

이 코드들을 모아서 다음과 같은 봉차트를 그릴 수 있습니다.

from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.layouts import gridplot
inc = df.close >= df.open
dec = df.open > df.close
p_candlechart = figure(plot_width=1050, plot_height=200, x_range=(-1, len(df)), tools="crosshair")
p_candlechart.segment(df.index[inc], df.high[inc], df.index[inc], df.low[inc], color="red")
p_candlechart.segment(df.index[dec], df.high[dec], df.index[dec], df.low[dec], color="blue")
p_candlechart.vbar(df.index[inc], 0.5, df.open[inc], df.close[inc], fill_color="red", line_color="red")
p_candlechart.vbar(df.index[dec], 0.5, df.open[dec], df.close[dec], fill_color="blue", line_color="blue")
p_volumechart = figure(plot_width=1050, plot_height=100, x_range=p_candlechart.x_range, tools="crosshair")
p_volumechart.vbar(df.index, 0.5, df.volume, fill_color="black", line_color="black")
p = gridplot([[p_candlechart], [p_volumechart]], toolbar_location=None)
show(p)

chart1

그러나 여기서 각 axis에 표시된 레이블들이 유용한 정보를 주지 못하고 있습니다. 좀 더 보기 좋게 레이블들을 포메팅 해보겠습니다.

p_candlechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')
p_candlechart.xaxis.visible = False

p_candlechart의 y 레이블을 다음과 같이 천단위로 콤마를 붙인 숫자로 표현합니다.

여기서 x 레이블은 거래량 차트에만 표시하기 위해서 숨기도록 합니다.

p_volumechart의 x 레이블을 날짜로 표현하고 y 레이블을 숫자로 표현합니다.

major_label = {
i: date.strftime('%Y%m%d') for i, date in enumerate(pd.to_datetime(df["date"]))
}
major_label.update({len(df): ''})
p_volumechart.xaxis.major_label_overrides = major_label
p_volumechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')

x 레이블의 마지막 값은 잘려서 나와서 숨겼습니다. 이제 이 코드들을 추가해서 다시 봉차트를 그려봅니다.

from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.layouts import gridplot
from bokeh.models.formatters import NumeralTickFormatter
inc = df.close >= df.open
dec = df.open > df.close
p_candlechart = figure(plot_width=1050, plot_height=200, x_range=(-1, len(df)), tools="crosshair")
p_candlechart.segment(df.index[inc], df.high[inc], df.index[inc], df.low[inc], color="red")
p_candlechart.segment(df.index[dec], df.high[dec], df.index[dec], df.low[dec], color="blue")
p_candlechart.vbar(df.index[inc], 0.5, df.open[inc], df.close[inc], fill_color="red", line_color="red")
p_candlechart.vbar(df.index[dec], 0.5, df.open[dec], df.close[dec], fill_color="blue", line_color="blue")
p_candlechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')
p_candlechart.xaxis.visible = False
p_volumechart = figure(plot_width=1050, plot_height=100, x_range=p_candlechart.x_range, tools="crosshair")
p_volumechart.vbar(df.index, 0.5, df.volume, fill_color="black", line_color="black")
major_label = {
i: date.strftime('%Y%m%d') for i, date in enumerate(pd.to_datetime(df["date"]))
}
major_label.update({len(df): ''})
p_volumechart.xaxis.major_label_overrides = major_label
p_volumechart.yaxis[0].formatter = NumeralTickFormatter(format='0,0')
p = gridplot([[p_candlechart], [p_volumechart]], toolbar_location=None)
show(p)

chart2

이렇게 x축은 날짜가, y축은 숫자가 보기좋게 나오게 됩니다. 이 차트를 html, json 등으로 웹에 넣어줄 수 있습니다. 자세한 사항은 bokeh 문서 Embedding Plots and Apps를 확인해 주세요.

아나콘다(Anaconda) 32bit 환경 설치하기

아나콘다는 파이썬 및 데이터 과학(data science) 패키지가 포함된 배포판 입니다. 기본적으로 아나콘다는 64bit 버전을 제공하고 있습니다.

anaconda

그러나 시스템 트레이딩과 같이 Windows 환경에서 다른 프로그램들과 통신할 때 32bit 버전이 필요한 경우가 종종 있습니다. 이번 포스트에서는 아나콘다에서 32bit 파이썬 환경을 구성하는 방법에 대해서 다룹니다.

먼저 64bit 버전의 아나콘다를 다운받아서 설치합니다. 아나콘다 다운로드 페이지에서 최신 버전의 아나콘다를 받을 수 있습니다.

Anaconda Prompt를 열고 다음과 같이 명령을 입력합니다.

Terminal window
set CONDA_FORCE_32BIT=1
conda create -n py37_32 python=3.7 anaconda

설치하는데 시간이 꽤 걸릴 것이니 차분히 기다립니다. 여기서 python=3.7부분을 수정하여 원하는 버전을 설치할 수 있습니다.

설치가 완료되면 이제 32bit 버전의 파이썬을 사용할 수 있습니다. 다음과 같이 좀전에 설치한 파이썬 32bit 환경을 활성화 시키면 됩니다.

Terminal window
activate py37_32

프롬프트 커맨드 라인 처음에 (py37_32)가 표시되어 있으면 활성화가 제대로 된 것입니다. 이 환경을 비활성화 시키려면 다음과 같이 명령하면 됩니다.

Terminal window
deactivate

참고로 리눅스 운영체제에서는 source activate py37_32, source deactivate와 같이 명령을 줘야 합니다.

Windows 10에서 아나콘다 32bit 사용시 에러 해결

Anaconda3 x86(32bit) 버전을 아무 문제 없이 잘 쓰고 있었다. 굳이 32bit 버전을 사용한 이유는 HTS API를 사용하기 위해서 였습니다. 증권사 장단점 비교 글에서 그 이유를 설명했습니다. 그런데 윈도우 업데이트가 되었는지 갑자기 Anaconda3 x86의 python.exe를 실행하려 하면 다음과 같은 에러 메시지가 표시되며 실행되지 않았습니다.

error

먼저 이 에러 메시지를 검색해서 이런 저런 방법을 써봤지만 해결되지 않았습니다. 관리자 권한으로 실행해봐도 안되고, “기본 제공 관리자 계정에 대한 관리자 승인 모드” 사용을 해봐도 안됐습니다. Anaconda 버전이 낮아서 그런가 해서 다시 설치해 봤는데도 안되었습니다. 결국 Anaconda3 x64 버전에 x86 환경을 생성하는 것으로 해결는데 그 방법은 다음과 같습니다. 먼저 cmd prompt를 엽니다. 그리고 다음과 같이 환경 설정 후 x86 환경을 설치합니다.

Terminal window
set CONDA_FORCE_32BIT=1
conda create -n py37_32 python=3.7 anaconda
activate py37_32

파이썬 메인 모듈에서 인자(argument) 파싱

파이썬 main을 실행할 때 인자(argument)를 주고싶은 경우 다음과 같이 할 수 있습니다. 인자는 positional argument와 optional argument로 구성할 수 있습니다. (자세한 정보는 https://docs.python.org/3.7/library/argparse.html 참고하세요.)

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('parg1') # positional argument
parser.add_argument('parg2') # positional argument
parser.add_argument('--oarg1') # optional argument
parser.add_argument('--oarg2') # optional argument
args = parser.parse_args()
print(args)

다음은 실행 커맨드에 따라 달라지는 args를 보여줍니다.

  • python main.py 1 2
    • Namespace(oarg1=None, oarg2=None, parg1='1', parg2='2')
  • python invalidanc.py 1 2 --oarg1=3
    • Namespace(oarg1='3', oarg2=None, parg1='1', parg2='2')
  • python invalidanc.py 1 2 --oarg1=3 --oarg2=4
    • Namespace(oarg1='3', oarg2='4', parg1='1', parg2='2')

자주쓰는 git 명령 정리

git을 안쓰는 개발자가 거의 없을 정도로 git은 버전 컨트롤 시스템 (Version Control System)을 대표하게 되었습니다. 예전에는 ftp, svn을 많이 사용했었던 기억이 납니다.

이 포스트에서는 자주 사용하는 git 명령들을 정리해 보았습니다.

  • add remote git remote add origin <url>

  • change remote git remote set-url origin <url>

  • remove remote git remote -v git remote rm <destination>

  • push forcely git push origin master --force git push origin master -f

  • pull forcely git fetch --all git reset --hard origin/master

​- fetch file forcely git fetch git checkout origin/master <filepath> ​

  • cancel commit git reset HEAD~
  • cache git config --global credential.helper "cache --timeout=86400"

  • logout echo -e "host=github.com\nprotocol=https\n" | git credential-osxkeychain erase git config --global --unset user.name git config --global --unset user.email

git add ./\*.py

git checkout -b [name_of_your_new_branch] ​

git push <REMOTENAME> <LOCALBRANCHNAME>:<REMOTEBRANCHNAME> ​

git branch -d the_local_branch ​

git push origin :the_remote_branch

git init git remote add -f origin <url> git config core.sparseCheckout true

echo "some/dir/" >> .git/info/sparse-checkout

git checkout <branch> git pull

git push origin <branch_from>:<branch_to>

파이썬 리플렉션 (동적 클래스 인스턴스 생성 및 함수 호출)

리플렉션(Reflection)이란 런타임에서 프로그램의 구조를 파악하고 동적 객체 생성 및 함수 호출 등의 행위를 수행할 수 있게 해주는 장치 입니다.

이 포스트에서는 파이썬에서의 간단한 리플렉션 예제를 소개합니다.

class MyClass:
my_var = 1
def __init__(self):
pass
def my_fnc(self, arg):
print('my_fnc({}) called.'.format(arg))
# 클래스 및 클래스변수 접근
my_class = globals()['MyClass']
print(my_class)
print(my_class.my_var)
# 객체 생성
my_inst = my_class()
print(my_inst)
# 함수 접근 및 호출
func = getattr(my_inst, 'my_fnc')
print(func)
func(2)

Python Tornado로 HTTP 및 웹소켓(Web Socket) 요청 처리하기

Tornado는 비동기(Asynchronous) 통신을 지원하는 Python 웹프레임워크 입니다. 아는 바로는 (확실하진 않지만) Facebook에서 만들었고 페북의 채팅 기능에서 활용하고 있다고 합니다. Tornado는 특히 웹소켓(Web Socket) 개발에 유용하게 사용할 수 있습니다. 다른 Python 웹프레임워크인 Django, Flask 등에서 보다 간단하게 웹소켓을 사용할 수 있는 장점이 있습니다.

기본적인 요청 처리는 다음과 같이 RequestHandler를 상속하여 구현하면 됩니다.

import tornado
__author__ = 'mkk'
class MyRequestHandler(tornado.web.RequestHandler):
def get(self):
# GET Params
self.get_argument("name")
# Response
self.write("OK")
application = tornado.web.Application([
(r"/", MyRequestHandler),
])
application.listen(8888)
tornado.ioloop.IOLoop.current().start()

웹소켓 요청을 처리하기 위해서는 다음과 같이 작성하면 됩니다.

import tornado
__author__ = 'mkk'
class EchoWebSocket(tornado.websocket.WebSocketHandler):
def open(self):
print("WebSocket opened")
def on_message(self, message):
self.write_message(u"You said: " + message)
def on_close(self):
print("WebSocket closed")
application = tornado.web.Application([
(r"/ws", EchoWebSocket),
])
application.listen(8888)
tornado.ioloop.IOLoop.current().start()