Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chatport

Chatport is a small, self-hosted collector for Toss Invest portfolio snapshots. It stores every data API response in SQLite as a BLOB and commits it before any JSON parsing or portfolio logic runs.

Raw capture and normalization are separate commands. A parser or analytics failure cannot undo or block an already committed capture.

Collected endpoints:

  • GET /api/v1/accounts
  • GET /api/v1/holdings for each account
  • GET /api/v1/prices for held symbols
  • GET /api/v1/candles for one adjusted daily candle per held symbol
  • GET /api/v1/exchange-rate for USD/KRW

Successful OAuth token response bodies are intentionally never retained. Non-success OAuth error bodies are retained for diagnosis because they do not contain an issued access token.

Setup

uv sync --dev
uv run --env-file .env chatport collect

Optional environment variables:

Variable Default
CHATPORT_DB_PATH data/chatport.sqlite
CHATPORT_LOG_PATH logs/chatport.jsonl
CHATPORT_LOG_LEVEL INFO
CHATPORT_MAX_ATTEMPTS 3
CHATPORT_BENCHMARKS empty

Run records and API attempts use UTC timestamps. Exit code 0 means complete, 2 means partial, and 1 means failed or invalid configuration.

Normalize captured data

Normalize every finished complete or partial capture that has not been processed by the current parser version:

uv run --env-file .env chatport derive

This command does not call Toss and does not require TOSS_CLIENT_ID or TOSS_CLIENT_SECRET. It reads the immutable api_attempts rows and writes rebuildable account, position, current-price, adjusted daily-close, FX, and provisional valuation snapshots. Decimal values remain SQLite TEXT after validation so calculations use Python Decimal without a binary floating-point conversion.

The provisional valuation is explicitly securities_only: cash, receivables, liabilities, dividends, fees, taxes, and external cash flows are excluded. USD positions use the captured USD/KRW midRate. total_value_krw is NULL unless the source capture is complete and every position has a supported, sufficiently fresh close and any required FX observation. known_value_krw exists only for diagnosis and must not be treated as total portfolio value.

Manual cash and external flows

Toss exposes cash-based buying power, not an authoritative cash-balance snapshot, so Chatport does not silently treat buying power as portfolio cash. Record an explicit KRW and USD balance for every account, including zero balances:

uv run --env-file .env chatport cash set \
  --account 1 --currency KRW --amount 250000 \
  --at 2026-07-10T07:00:00+09:00

uv run --env-file .env chatport cash set \
  --account 1 --currency USD --amount 0 \
  --at 2026-07-10T07:00:00+09:00

uv run --env-file .env chatport cash list --active-only

Each cash set appends a new as-of observation. Correct a mistaken entry without editing history:

uv run --env-file .env chatport cash correct ENTRY_ID --amount 275000

Record deposits and withdrawals separately from balance observations:

uv run --env-file .env chatport flow add \
  --account 1 --currency KRW --type deposit --amount 1000000 \
  --at 2026-07-10T12:00:00+09:00

uv run --env-file .env chatport flow reverse FLOW_ID --note "duplicate entry"
uv run --env-file .env chatport flow list

Flows are append-only accounting evidence. They do not change NAV; they adjust the return calculation so deposits and withdrawals are not mistaken for investment performance.

Derivation selects the latest active balance whose effective timestamp is at or before each valuation. portfolio_nav_snapshots.total_nav_krw is populated only when the securities valuation is complete and every account has both required cash balances. After adding or correcting historical cash, run chatport rebuild, or force one affected capture with chatport derive --run-id RUN_ID --force.

Unitized performance

Every derive run rebuilds the performance series from complete NAV snapshots and external flows. It uses Modified Dietz time weights based on the exact flow timestamps, then links period returns through a unit-price index initialized at KRW 1,000:

period return = (ending NAV - beginning NAV - net flow)
                / (beginning NAV + time-weighted flow)

Inspect or explicitly recalculate the series without Toss credentials:

uv run --env-file .env chatport performance rebuild
uv run --env-file .env chatport performance list --limit 30

For USD flows, the conversion uses the ending valuation's captured USD/KRW midRate and records its source attempt. This is a documented approximation because Chatport does not have an FX observation at every intraday flow. Performance is calculated only from complete NAV snapshots. An invalid period, such as a nonpositive Modified Dietz denominator, blocks subsequent periods until the inputs are corrected and the series is rebuilt.

This is an approximate linked time-weighted return, not a claim of GIPS compliance or a true intraday TWR. Returns shorter than one year are not annualized.

Benchmark proxies

Configure optional Toss-supported symbols as explicitly named investable proxies:

CHATPORT_BENCHMARKS=sp500=SPY,nasdaq100=QQQ,gold=GLD

These examples are ETF proxies, not the official S&P 500, Nasdaq-100, or spot gold indices. Fund expenses, tracking differences, taxes, distributions, and the exact meaning of Toss's adjusted candles can make their returns differ from official total-return indices. No benchmark is enabled by default.

For each configured proxy, collect stores the latest 30 adjusted daily candles. Benchmark failures are nonblocking: they are captured and logged but cannot make an otherwise complete portfolio run partial. derive calculates each daily proxy return from adjacent candles in the same response, allowing a later response to restate adjusted-price history without creating a false split or distribution jump. A gap beyond the rolling recovery window invalidates that proxy series rather than inventing a return.

Each proxy index starts at 1,000. Comparisons are rebased at the first portfolio-performance observation that has a same-run benchmark capture and include portfolio return, proxy return, arithmetic excess return, and relative wealth return. The proxy index and local-currency return are retained as captured. For the actual comparison, USD proxy wealth is converted to the KRW reporting currency with each run's USD/KRW midRate; both the current and baseline rates and their raw source-attempt IDs are stored. A USD proxy capture without same-run FX evidence still contributes to its local index, but its portfolio comparison is omitted and a warning is logged rather than silently mixing currencies.

Schema migration 7 removes only legacy mixed-currency comparison rows. After upgrading an existing database, run chatport benchmark rebuild once to recreate them with the stored same-run FX observations.

uv run --env-file .env chatport benchmark rebuild
uv run --env-file .env chatport benchmark list --limit 30
uv run --env-file .env chatport benchmark list --key sp500 --limit 30

Bitcoin, CPI, housing prices, and deposit rates are intentionally not forced through this daily Toss-symbol pipeline. They require separate sources, frequencies, licensing checks, and alignment policies.

Target allocation and rebalancing

Target policies are append-only, effective-dated inputs. Percentages are entered as percentage points, and the cash target may be explicit or inferred as the remainder to 100%:

uv run --env-file .env chatport allocation set \
  --target AAPL=40 --target SPY=35 --target GLD=15 \
  --cash-target 10 --threshold 5 \
  --at 2026-07-11T07:00:00+09:00 \
  --note "initial target policy"

uv run --env-file .env chatport allocation policies
uv run --env-file .env chatport allocation rebuild
uv run --env-file .env chatport allocation list --out-of-band

If --cash-target is omitted, Chatport assigns the unused percentage to cash. If it is provided, security and cash targets must total exactly 100%. The threshold is an absolute percentage-point drift; a row is out of band when its absolute drift is greater than or equal to the threshold.

Calculations use only complete KRW NAV snapshots. Current security values are aggregated across accounts by symbol, combined KRW and converted USD cash is a single CASH allocation, and any held symbol absent from the policy has a 0% target. Suggested rebalance_amount_krw is positive for a buy and negative for a sell. The cash row uses raise_cash or deploy_cash. Historical NAVs before the first policy's effective time are not backfilled.

These are value-level planning amounts, not executable orders. They do not account for whole-share constraints, spreads, fees, taxes, settlement, or minimum cash buffers beyond the configured cash target. Chatport never places orders. Every daily derive refreshes these snapshots; allocation rebuild is for immediate recalculation after adding or correcting a historical policy.

Useful recovery commands:

# Reprocess one run with the current parser.
uv run --env-file .env chatport derive --run-id RUN_ID --force

# Delete normalized rows and deterministically recreate them from raw captures.
uv run --env-file .env chatport rebuild

rebuild never deletes or updates runs or api_attempts. Run a backup first anyway when using it operationally. A run captured by an older Chatport version without /api/v1/candles remains incomplete after rebuilding because derivation never invents or retroactively fetches missing raw evidence.

Cron

Use the same environment as the interactive command. Chatport also takes its own nonblocking lock, so a second invocation exits rather than overlapping an active run.

0 7 * * * cd /path/to/chatport && /path/to/uv run --no-sync --env-file /path/to/chatport.env chatport collect

Backup

Create a consistent SQLite backup before upgrades or schema changes:

uv run --env-file .env chatport backup /path/to/backups/chatport.sqlite

The command refuses to overwrite an existing backup. Toss credentials are not required when CHATPORT_DB_PATH is already present in the process environment.

Production sequence

Keep collection as the primary independent job. On the NAS, run normalization as a second job shortly afterward:

07:00  chatport collect
07:10  chatport derive

If the second process is interrupted, the raw response remains available and the next derive invocation retries the unfinished run. A response rejected by the strict parser remains recorded as a failed derivation for diagnosis; after fixing the parser, use --force for one run or rebuild for the full history. After installing a release that changes the parser version, back up the database and run chatport rebuild once before enabling analytics based on the normalized tables.

The initial valuation assumptions for future analytics are documented in docs/valuation-policy.md.

Tests

uv run pytest

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages