Omni-Folio 개발기: 연구 결과가 원장을 바꾸기까지의 경계

목차

포트폴리오 앱에 연구 기능을 붙이면 계산 결과를 어느 순간 실제 상태로 받아들일지 결정해야 한다. 직접 개발하는 Omni-Folio에서는 Go가 거래 원장과 주문 상태를 소유하고 Python은 검토할 연구 결과와 목표 수량 제안을 만든다. 개인용 앱에서도 이 경계를 두면 “어떤 입력으로 계산했는가”와 “지금 실행해도 되는가”를 따로 확인할 수 있다.

Omni-Folio는 Flutter 화면, Go 코어, Python 연구 도구로 구성한 local-first 프로젝트다. 아래 이야기는 공개 main의 dec1101 판본을 기준으로 한다. 이 판본의 실거래와 브로커 연결은 비활성이고 여기서 따라갈 실행은 로컬 시험 데이터로 동작하는 모의 실행이다.

포트폴리오의 기준을 한 곳에 두기

화면에 보이는 잔액은 무엇을 근거로 만들어야 할까. Omni-Folio의 출발점은 가져온 거래 기록이다. CSV를 미리보기로 검증하고 확인한 미리보기를 적용하면 Go가 원장을 변경하고 적용 결과를 돌려준다. 미리보기만으로는 잔액이 바뀌지 않는다. 적용한 기록에서 현금, 보유 수량과 원가를 다시 계산한다.

이 구조에서는 잘못 가져온 현금 기록을 고칠 때도 원본을 지우지 않는다. 같은 계좌·통화의 원본 금액을 정확히 상쇄하는 정정 이벤트를 추가한다. 현재 잔액뿐 아니라 그 값에 도달한 이유까지 남기려는 선택이다. 원장 입력과 정정 계약

원장이 소유하는 것은 앱에 기록된 거래 상태다. 외부 증권사의 현재 잔고와 일치한다는 보증까지 포함하지 않는다. 같은 이유로 연구에서 “이 종목을 10주 보유하자”는 결과가 나와도 포트폴리오에 10주를 바로 더할 수는 없다. 제안, 주문, 체결, 원장의 기록은 각기 다른 사실이다. 현재 로컬 모의 회계도 일반 거래 원장에 쓰지 않고 별도 체결 기록을 재생해 계산한다. 모의 회계의 경계

초기 저장소는 Go와 같은 호스트의 SQLite이며 쓰기 연결은 하나로 제한했다. 개인용 단계에서 DB 서버 운영을 먼저 늘리지 않기 위해서다. 여러 서버가 필요해지면 이 선택도 바뀐다. 두 번째 API 복제본이나 독립 worker 확장 전에 PostgreSQL로 이관하고 복구를 검증하도록 아키텍처 결정에 조건을 적어 두었다.

같은 입력을 다시 계산할 수 있어야 한다

Python 연구 도구는 이동평균 교차 전략의 후보를 평가하고 결과 파일을 만든다. 이동평균 교차는 짧은 기간의 평균 가격과 긴 기간의 평균 가격의 위치가 바뀌었는지 확인하는 규칙이다. 후보 탐색은 학습 구간을 점차 늘리는 검증과 마지막에 따로 둔 평가 구간을 사용한다. 결과의 승격 대상은 로컬 모의 후보까지다.

후보 결과에는 입력 데이터 해시, 전략 매개변수, 평가 정책과 결과 해시가 들어간다. Go는 이를 추가만 가능한 연구 기록에 등록하고 현재 선택한 결과를 별도 이벤트로 남긴다. 나중에 제안을 받을 때는 결과 해시뿐 아니라 정확히 어느 선택 이벤트에 속하는지도 확인한다. 같은 전략을 다시 선택했더라도 이전 선택의 실행 권한이 그대로 따라오게 하지 않는 것이다. 연구 결과 등록과 선택

여기서 파일 경로만 전달하면 문제가 남는다. Python이 latest.csv를 읽어 계산한 뒤 다른 프로세스가 그 파일을 갱신할 수 있다. Go가 같은 경로를 나중에 열면 계산에 쓰인 자료와 다른 자료를 읽는다. 경로가 같다는 사실은 입력이 같다는 증거가 되지 않는다.

그래서 현재 제안 생성기는 CSV를 바이트로 한 번 읽고 그 내용에서 계산과 해시 생성을 함께 수행한다. 선택적인 bundle 출력에는 제안과 계산에 사용한 시세 CSV·연구 CSV를 넣는다. Go는 원래 경로를 다시 여는 대신 묶음 안의 내용을 받는다. 연구 결과 자체는 Go에 등록된 결과와 대조한다. Python 입력 묶음 생성 · Go 입력 묶음 검증

입력 하나가 모의 주문에 도달하는 과정

현재 구현에서 이 묶음은 다음 순서로 이동한다. 아래는 소스를 따라 정리한 흐름이며 글을 쓰며 새로 실행한 관측 결과는 아니다.

  1. Python이 등록 후보의 연구 입력과 새 시세 입력을 읽는다. 종목, 입력 해시, 전략 매개변수와 연구 종료 이후의 데이터인지 확인하고 목표 수량을 제안한다.
  2. --bundle --capture-dir를 지정하면 직렬화한 묶음을 먼저 파일에 보관한 뒤 표준 출력으로 내보낸다. 보관 실패 시 묶음을 출력하지 않는다.
  3. 사용자가 명시적으로 모의 실행을 활성화해 보관 파일을 Go에 전달한다. Go는 묶음의 형식과 입력 해시, DB에 등록된 연구 결과 및 현재 선택을 대조한다.
  4. Go는 저장한 가격 이력에서 이동평균 교차를 독립적으로 확인한다. 현재 실행 프로세스의 유효한 임대와 안전 정책도 검사한다. 임대는 일정 시간 동안 실행 주체를 식별하는 권한이며, 오래된 실행자가 상태를 바꾸지 못하도록 번호도 함께 검증한다.
  5. 기존 미완료 주문의 모의 체결과 안전 정책을 필요한 봉 순서대로 처리한 뒤 새 제안을 접수한다. 중간 정책이 중지를 결정하면 새 제안 접수까지 진행하지 않는다.

순서의 의미는 마지막 두 단계에 있다. Python이 만든 제안에 올바른 해시가 있어도 현재 선택과 맞지 않으면 실행할 수 없다. 해시는 입력의 일관성을 검사하는 수단이고 현재 권한과 안전 상태는 Go가 다시 결정한다. 모의 실행 순서 · 제안 접수 검증

보관 파일을 작업 큐로 만들지 않은 이유

표준 출력은 소비자가 받기 전에 끊길 수 있다. 출력 전에 입력을 보관하면 그때 관찰한 자료는 남아서 명시적으로 다시 전달할 수 있다. 하지만 보관 성공은 Go의 처리 완료를 알려주지 않는다.

이 차이는 다음과 같은 실패 상황에서 선택을 바꾼다. 표의 상황은 구현의 경계를 설명하기 위한 예시다.

상황보관 파일만으로 판단하면현재 구분해야 할 사실
파일 저장 뒤 출력 연결이 끊김파일이 있으니 처리했다고 볼 수 있다입력 보관만 확인됐다. Go 접수 여부를 따로 확인해야 한다
같은 파일을 다시 전달함매번 새 작업으로 실행한다Go의 기존 기록과 현재 권한을 다시 검증한다
Go가 안전 정책으로 중지한 뒤 정상 반환함오류가 없으니 새 제안도 완료됐다정책 처리와 새 제안 접수는 다르다
오래된 파일을 처음 전달함해시가 맞으니 최신 자료다내용 동일성과 자료의 신선도는 다르다

보관 파일을 자동 재실행 큐로 만들려면 처리 완료를 어디에 기록할지도 정해야 한다. 파일 옆에 완료 표시만 두면 DB를 이전 백업으로 복구했을 때 문제가 생긴다. 완료 표시는 남아 있는데 DB에는 해당 실행이 없을 수 있다. 현재 구현은 이 완료 프로토콜과 자동 재활성화를 추가하지 않았다.

신선도에도 같은 구분이 필요하다. 현재 제안 기한은 캡처 시점이 아니라 Go가 처음 가져온 시점에서 시작한다. 오래 보관한 미접수 파일도 첫 접수 시 새 기한을 얻을 수 있으므로 보관 기능이 오래된 자료의 실행을 막아 준다고 해석하면 안 된다. 또한 주기적으로 파일을 읽는 방식은 관찰 사이에 교체된 모든 입력의 전달을 보장하지 않는다. 이 한계와 다음 계약은 입력 보관 검증 기록에 남겼다.

두 언어를 쓰는 비용을 어디까지 감수할까

Python이 계산부터 DB 쓰기까지 맡으면 언어 경계를 넘는 계약은 줄어든다. 대신 연구 코드가 운영 상태를 바꿀 수 있고 연구 결과 검증과 실행 승인도 같은 코드에 묶인다. Omni-Folio에서는 이 편의보다 상태 변경 권한을 Go에 모으는 쪽을 택했다.

비용은 분명하다. 두 언어 사이의 입력 형식을 유지해야 하고 Go에서도 제안의 계산을 확인해야 한다. 현재 이동평균 전략은 Go가 저장된 가격으로 교차 여부를 다시 계산할 만큼 범위가 좁다. 전략이 복잡해지면 이 검증 비용도 다시 판단해야 한다. Python과 Go 중 어느 언어가 더 빠르다는 측정으로 선택한 구조는 아니다.

이번 글의 대표 근거도 성능 수치보다 복구 가능한 입력에 있다. 공개된 통합 테스트는 실제 Python CLI로 묶음을 생성하고 원래 CSV와 결과 파일을 다른 내용으로 교체한 뒤 보관 파일을 Go 명령 처리기에 전달한다. 기대값은 수량 10의 OPEN 모의 주문, 같은 입력 재전달 시 추가 접수 기록 없음, 종료 뒤 실행 임대 정리다. Python은 실제 자식 프로세스로 실행하지만 Go 명령 처리기는 테스트 프로세스 안에서 호출한다. 통합 테스트

2026년 10월 23일의 공개 검증 기록에는 Go 전체 검사와 Python 39개 테스트 통과가 남아 있다. 이는 해당 개발 시점의 로컬 검증 기록이다. 이번 글에서는 테스트를 재실행하지 않았으며 현재 브로커 연결이나 장기 운영을 확인한 결과로 확장하지 않는다.

Omni-Folio를 개발하며 먼저 확정한 것은 계산 결과를 보관하는 방식과 상태를 바꿀 주체다. 비슷한 연구·추천 기능을 앱에 붙일 때도 결과 파일이 남았다는 사실, 현재 선택한 결과라는 사실, 실행이 완료됐다는 사실을 각각 어디서 확인할지 정해 보면 된다. 셋의 근거가 분리되어 있으면 재시도와 복구가 새 계산 결과를 잘못 실행하는 통로가 되는 일을 줄일 수 있다.

프로젝트 소스와 계획은 Omni-Folio 저장소에서 볼 수 있다. 이 판본은 별도 오픈소스 라이선스를 부여하지 않은 공개 저장소다. 본문은 동작을 설명하며 소스나 이미지를 복제하지 않았다.