Ozakboy.TradeKit.Capital
0.3.0
dotnet add package Ozakboy.TradeKit.Capital --version 0.3.0
NuGet\Install-Package Ozakboy.TradeKit.Capital -Version 0.3.0
<PackageReference Include="Ozakboy.TradeKit.Capital" Version="0.3.0" />
<PackageVersion Include="Ozakboy.TradeKit.Capital" Version="0.3.0" />
<PackageReference Include="Ozakboy.TradeKit.Capital" />
paket add Ozakboy.TradeKit.Capital --version 0.3.0
#r "nuget: Ozakboy.TradeKit.Capital, 0.3.0"
#:package Ozakboy.TradeKit.Capital@0.3.0
#addin nuget:?package=Ozakboy.TradeKit.Capital&version=0.3.0
#tool nuget:?package=Ozakboy.TradeKit.Capital&version=0.3.0
Ozakboy.TradeKit.Capital
A .NET 10 client for the overseas quotes of Capital Securities' (群益證券) SKCOM API: international FX and overseas futures; since 0.2.0 also domestic futures historical K-lines (TAIEX, mini and micro TAIEX futures, near month). Version 0.3.0 adds domestic futures and options live quotes, the overseas best 5 and best 10, overseas option quotes and read-only futures account queries. The package cannot place, amend or cancel orders and cannot move funds. It does not support features that need a securities account.
SKCOM is a COM component that must live on an STA thread with a running message loop. This package therefore ships
a small x64 host process, Ozakboy.TradeKit.Capital.Host, that loads SKCOM and talks to its parent over
newline-delimited JSON on stdio. The client, CapitalHostSession, starts and supervises that process, sends
requests, reads quote and tick events, turns raw integer prices into decimal and exchange-local times into UTC. A
crash or a hang in SKCOM never takes your application down.
Current status: the SKCOM calls, formats and time zones this package relies on were measured with real credentials against SKCOM 2.13.59 on 2026-11-06 (see "Measured and unmeasured"). The
serveprotocol, the supervision and the reconnection logic have so far been tested only against the fake host intests/; the 3002 reconnect in particular has never been seen live. Read that section before relying on it. The 0.3.0 features follow Capital's V2.13.59 manual and their formats were measured against real SKCOM with the host'sprobe-fullcommand on 2026-11-09, then corrected (a futures-only account without positions or orders). The "Measured" column below says what was measured and what only partly.
What this release does
| Scope | Status | Measured |
|---|---|---|
Start and supervise the host (CapitalHostSession), credentials only through child-process environment variables |
Done | Fake host only |
A failed login is reported once and never retried (capital.login.failed, Data["code"], Data["locked"]) |
Done | Codes from the manual |
Unexpected host exit: restart with 1, 2, 5, 10, 30 s backoff up to MaxRestarts, every subscription re-sent |
Done | Fake host only |
| Quote-server disconnect (3002): the host reconnects with 5, 10, 30, 60 s backoff and re-sends every subscription | Done | Not measured |
Overseas product list merged with product details (GetProductsAsync) |
Done | Measured |
Overseas quotes and ticks (Events, bounded channel, drops the oldest and counts DroppedEvents) |
Done | Measured |
Overseas historical K-lines: minute, day, week, month (GetKLinesAsync) |
Done | Measured |
Connection status (GetConnectionStatusAsync, with the domestic and overseas option states and the SKCOM version) |
Done | Overseas part measured |
Domestic futures historical K-lines: minute and daily, full or AM session (GetDomesticKLinesAsync, 0.2.0) |
Done | Formats measured; serve not measured |
Domestic state kept apart from the overseas one (DomesticState, DomesticStateChanged, 0.2.0) |
Done | Fake host only |
| No orphaned host: Job Object (KILL_ON_JOB_CLOSE), ends on stdin EOF or when the parent disappears (0.2.0) | Done | Fake host only |
SKCOM version label and start-up check (SkcomVersion, IsTestedSkcomVersion, 0.3.0) |
Done | Measured (2.13.59_x64, readable before login); serve not measured |
Domestic futures and options product lists (GetDomesticProductsAsync, 0.3.0) |
Done | Measured (six fields, paired day-only "AM" codes) |
Domestic futures and options live quotes, ticks, best 5 and live minute bars (SubscribeDomesticAsync, 0.3.0) |
Done | Formats and times measured (night session); after midnight and the 10-code cap not measured |
Overseas best 5 and market depth best 10 (SubscribeDepthAsync, 0.3.0) |
Done | Formats measured; what triggers the best 5 not settled |
Overseas option products, quotes, ticks, best 5 and best 10 (OverseasOptionsState, SubscribeOverseasOptions*Async, 0.3.0) |
Done | Connect, list, quotes and books measured; ticks and time zones not measured |
Read-only futures account queries: accounts, rights, open interest, order and fill reports (GetAccountsAsync and others, 0.3.0) |
Done | Accounts and the empty-account replies measured; rows with positions not measured; Chinese arrives garbled |
| Placing, amending, reducing, re-pricing or cancelling orders, smart orders, fund transfers | Not provided, and never will be | — |
| Features that need a securities account: Taiwan stock quotes, listed K-lines, stock orders, holdings, sub-brokerage | Not in scope | — |
Compatible SKCOM versions
This package was measured against SKCOM 2.13.59 x64. The host's COM interfaces are generated from that version's
type library (tools/SkComInteropGen). COM dual interfaces are called by method order. If a new Capital build
reorders the methods, the host calls the wrong method. Therefore:
- At start-up the host reads
SKCenterLib_GetSKAPIVersionAndBit.CapitalHostSession.SkcomVersionholds the value (for example2.13.59_x64) andIsTestedSkcomVersiontells whether it is inCapitalSkcomVersion.Tested. - For a version that was not measured, the session logs one warning (
capital.skcom.version_untested, event 4015) and keeps running. - Before you upgrade SKCOM, run the host's
probe-fullprobe with the new build. Then run the generator against the newInterop.SKCOMLib.dlland confirm that the method order did not change.
Requirements
- Windows x64. The host ships self-contained in the package, with its own .NET and Windows Desktop runtime. You do not need to install a .NET runtime for the host. Your own app needs the .NET 10 runtime as usual.
- SKCOM x64, downloaded and registered by you. This package does not contain or redistribute any Capital file.
Download the SKCOM API from Capital's official site, put the
x64folder somewhere permanent, and run itsinstall.batas an administrator. If you later move or delete that folder, registration points at a missing DLL (capital.skcom.file_missing); register it again. - A Capital Securities account with overseas futures quote access, and Capital's certificate installed as Capital instructs.
- Domestic futures K-lines need a Capital futures account; without a securities account listed stocks are not
available (a futures-only account asking for
2330was measured to get 9999). This package only commits to futures.
Installation
dotnet add package Ozakboy.TradeKit.Capital
The package carries the host in tools/capital-host/, and its buildTransitive targets copy it to
$(OutDir)capital-host\ (and to the publish folder) of any project that references the package, directly or
transitively. CapitalHostOptions.DefaultHostPath(AppContext.BaseDirectory) points there.
Quick start
using Ozakboy.TradeKit.Capital;
var options = new CapitalHostOptions
{
HostExecutablePath = CapitalHostOptions.DefaultHostPath(AppContext.BaseDirectory),
// Optional: SKCOM's own log folder. Its logs may contain the login ID; keep them private.
LogDirectory = null,
};
await using var session = new CapitalHostSession(options, logger);
// The ID number is upper-cased; Validate() checks "one letter + nine digits" and echoes nothing.
var credentials = new CapitalCredentials(loginId, password);
var started = await session.StartAsync(credentials, cancellationToken);
if (started.IsFailure)
{
// capital.login.failed -> never retried; Data["code"] = 300/306/307/600/604, Data["locked"] for 307
// capital.skcom.not_registered / capital.skcom.file_missing -> register SKCOM x64 as an administrator
// capital.connect.timeout -> no OnConnect(3001) within 60 s
return;
}
await session.SubscribeAsync([CapitalSymbol.Parse("FX,SUSDTWD"), CapitalSymbol.Parse("CME,NQ0000")]);
await foreach (var item in session.Events.ReadAllAsync(cancellationToken))
{
switch (item)
{
case CapitalQuote quote:
// quote.Close is decimal; quote.TradingDay is Capital's trading day
break;
case CapitalTick tick:
// tick.TimeUtc is UTC; tick.IsReplay marks the day's replay sent at subscription;
// de-duplicate on (Symbol, Sequence) — replays repeat after a restart
break;
}
}
K-lines:
var bars = await session.GetKLinesAsync(
CapitalSymbol.Parse("FX,SUSDJPY"), CapitalKLineType.Minute,
new DateOnly(2026, 11, 2), new DateOnly(2026, 11, 5), decimals: 2);
// Minute bars: OpenTimeUtc is the start of the minute (Capital gives the end; one minute is subtracted).
// Day / week / month bars: TradingDay only.
Domestic futures historical K-lines (0.2.0)
// Codes: TX00 TAIEX futures, MTX00 mini, TM0000 micro — near month, rolled like Sinopac's continuous months.
// The dates are trading days: Full includes the night session from 15:00 of the previous trading day,
// so a Monday starts at Friday 15:01.
var tx = await session.GetDomesticKLinesAsync(
"TX00", CapitalDomesticKLineType.Minute, CapitalTradeSession.Full,
new DateOnly(2026, 11, 2), new DateOnly(2026, 11, 6));
if (tx.IsFailure)
{
// capital.domestic.unavailable -> domestic side unavailable or disconnected; Data["reason"], see CapitalDomesticReasons
// capital.kline.rejected -> unknown code or no permission; Data["code"] = Capital's code (9999, 3023 measured)
// capital.kline.timeout -> no OnKLineComplete within 60 s
return;
}
foreach (CapitalDomesticBar bar in tx.GetValueOrThrow())
{
// bar.OpenTimeUtc = the Taipei end time minus one minute, in UTC; daily bars have bar.TradingDay only.
// Volume == 0 is an empty bar (a minute without trades), returned as is.
}
session.DomesticStateChanged += (_, e) => { /* e.Current, e.Reason, e.Code */ };
Worth knowing:
- The domestic side connects after the overseas side is ready (
NotStarted→Connecting→Ready, about 0.5 s); a request sent whileConnectingwaits in the host. A rejected domesticEnterMonitorLONGor no 3003 within 60 s makes itUnavailable(never retried) while the overseas side stays ready andStartAsyncstill succeeds; a domestic disconnect (3002) is reconnected with 5, 10, 30, 60 s backoff. Domestic changes raise onlyDomesticStateChanged, neverStateChanged. - A close may be the settlement price. The last day-session minute bar of a full-session query (13:45, or 13:30 on settlement days) and the full-session daily close were measured to be TAIFEX's settlement price; only the AM-session daily close is the last traded price (TAIEX futures on 2026-11-06: last 39786, settlement 39652). The data is returned unchanged.
- One request holds at most 60,000 rows; beyond that Capital keeps the newest and drops the oldest without an error. Minute bars go back about a year, daily bars to 2015. Split long ranges and check the first bar.
- Unreadable rows are counted and skipped (one warning log), never failing the batch.
- A known Capital data error: the volume of the Friday 2026-10-30 night session (belonging to 11/02) is twice TAIFEX's
official figure; see
docs/DESIGN.md§6.
Domestic live quotes, overseas best 10 and overseas options (0.3.0)
// Domestic futures and options: quotes, ticks (the day's replay first), best 5 with the extended level, live
// minute bars. The whole set holds at most 10 codes (manual 4-4-1). Every code comes in a pair: the one ending in
// "AM" (TX00AM) is day session only, so subscribe the full-session code (TX00) for the night session.
IReadOnlyList<CapitalDomesticProduct> futures = (await session.GetDomesticProductsAsync(CapitalDomesticMarket.Futures)).GetValueOrThrow();
await session.SubscribeDomesticAsync(["TX00", "MTX00"]);
// Overseas best 10 (market depth). The overseas best 5 is assumed to come with the ticks of SubscribeAsync.
await session.SubscribeDepthAsync([CapitalSymbol.Parse("CME,NQ0000")]);
// Overseas options (SKOOQuoteLib) have their own state; an unavailable side does not affect the other feeds.
if (session.OverseasOptionsState == CapitalOverseasOptionsState.Ready)
{
var products = (await session.GetOverseasOptionProductsAsync()).GetValueOrThrow();
// products[i].Symbol is the "exchange,code" to subscribe with (e.g. CME,ES00100X6); Name and LastTradeDate too
}
await foreach (var item in session.Events.ReadAllAsync(cancellationToken))
{
switch (item)
{
case CapitalDomesticQuote quote: break; // price = raw integer / 10^Decimals; a zero ("no value") is null
case CapitalDomesticTick tick: break; // IsSimulated marks a pre-open match; nDate is the calendar day
case CapitalDomesticLiveBar bar: break; // OpenTimeUtc = Capital's end-of-minute label minus 1 minute
case CapitalBest5 best5: break; // best5.Market tells the feed
case CapitalBest10 best10: break;
case CapitalOverseasOptionQuote optionQuote: break;
case CapitalOverseasOptionTick optionTick: break; // TimeUtc is null for an unknown exchange
}
}
- The symbol of a domestic event always has the exchange
CapitalSymbol.DomesticExchange(TAIFEX). It only labels the event. Do not pass it toSubscribeAsync. - Every subscription merges with the current set. The host re-sends the whole set after a reconnect, and the session re-sends it after a host restart. During the first domestic or overseas option connect, the host holds requests until the connect has an outcome.
- A value that is not available is not set to zero. A zero price in a domestic quote or tick or an overseas option quote is null (the first quote after subscribing was measured to be partly zero), live bar prices are null when the decimals are unknown, and empty 0 / 0 book levels stay as Capital sends them, so drop zero prices before use.
- Measured on 2026-11-09: a night-session tick's
nDateis the calendar day, futures ticks carry milliseconds only, and a live bar's time is the end of its minute. Not measured yet: dates after midnight.
Read-only futures account queries (0.3.0)
// The host initialises SKOrderLib on the first account request (Initialize, GetUserAccount, ReadCertByID).
IReadOnlyList<CapitalAccount> accounts = (await session.GetAccountsAsync()).GetValueOrThrow();
string futuresAccount = accounts.First(a => a.Kind == CapitalAccountKind.DomesticFutures).FullAccount!;
var rights = await session.GetFutureRightsAsync(futuresAccount, CapitalCoinType.All); // one row per currency
var positions = await session.GetOpenInterestAsync(futuresAccount); // "no data" is an empty list
var orders = await session.GetOrderReportAsync(futuresAccount, CapitalOrderReportFormat.All); // 5.5 s between report queries
- Queries only. In the host's
ISKOrderLibCOM definition, every method that could change an order, a position or funds is an uncallable placeholder. A reflection test checks it. This package will not add order functions. - Each record type has named properties parsed by the manual's field order and
RawFields. A named property is null when it cannot be read, never zero. The accounts, the rights (41 domestic fields, 31 overseas) and the "no data" replies were measured on 2026-11-09 with an account without positions; rows with positions or orders are not measured yet, so readRawFieldswhen in doubt. - Chinese arrives garbled: Capital's account replies (SKOrderLib) turn Chinese into irreversible mojibake ("查無資料"
becomes
?dμL﹐eRA). The package restores the measured fixed phrases only (CapitalSkcomText.Repair, already applied to currencies and error messages); for other text such as the holder's name useCapitalSkcomText.LooksGarbled. Chinese in the quote feeds is fine. - Do not send the same query back to back: the same query function sent again 1-2 seconds after the previous one
ended is rejected with 1019 (
capital.account.query_rejected,Data["code"]= 1019). Wait a few seconds. - Failures:
capital.account.unavailable,capital.account.init_failedandcert_failed(with Capital's code and the step; the next request tries again),capital.account.query_rejected,query_failedandquery_timeout. - Account numbers, holder names and IDs never reach the host's stderr or your log. They are masked as
<ACCT>,<NAME>and<ID>as soon as they arrive.ToString()of the record types shows only the last 3 characters of the account.CapitalAccount.RawFieldsholds the holder's ID and name, so keep it private.
Design decisions worth knowing
Why a child process on stdio rather than HTTP
The host has exactly one caller, the process that started it. stdio is reachable by that parent only: no port, no
token, nothing else on the machine can connect. The protocol has the shape of MCP's stdio transport and is easy to
test. See docs/DESIGN.md §2 for the full protocol.
Credentials travel only through the child process's environment
StartAsync puts the ID and password into ProcessStartInfo.Environment (CAPITAL_LOGIN_ID, CAPITAL_PASSWORD) and
nowhere else — never a command-line argument, a file or a log. The host reads them and removes them from its own
environment at once. Both are registered on the SecretMasker, and their literal values are additionally replaced in
everything the host writes to stderr (<ID> / ***), because the masker refuses values shorter than 8 characters.
The host itself also replaces the ID with <ID> before writing a line. CapitalCredentials.ToString() carries no
content and the type is deliberately not a record.
Login is never retried
Too many wrong passwords lock a Capital account (307), and repeated logins do not unlock it. A failed login stops the
session in LoginFailed; nothing retries it, including the restart loop. Only connection-class failures are retried:
a host crash (restart, which logs in again on a fresh process) and a quote-server disconnect (the host calls
EnterMonitorLONG again; it never logs in again).
Exit codes of the host
| Code | Meaning | Client behaviour |
|---|---|---|
| 0 | shutdown, end of stdin, or the parent is gone | none (expected) |
| 1 | internal host defect | restart |
| 2 | CAPITAL_LOGIN_ID / CAPITAL_PASSWORD missing; Login never called |
Faulted, no restart |
| 3 | SKCOM objects cannot be created | Faulted, no restart |
| 4 | login failed | LoginFailed, no restart |
| 5 | no OnConnect(3001) within 60 s |
restart (counts towards MaxRestarts) |
No domestic failure changes the exit code.
The host never becomes an orphan (0.2.0)
A host that outlives its parent stays logged in to Capital. So there are three safety nets: the client puts the host
into a Windows Job Object (JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, the handle held by CapitalHostSession until
DisposeAsync), so the system ends the host however the parent ends, even when killed; the host leaves both monitors
and exits 0 on stdin EOF; and every 5 s it checks the parent PID the client passes in CAPITAL_PARENT_PID, ending the
same way once it is gone. If an orderly shutdown has not finished 15 s after that decision (an STA stuck in a
synchronous SKCOM call, say), the host exits forcibly.
Numbers and time
- Quote and tick prices are raw integers;
CapitalPriceScale.FromRawdivides by 10^decimals indecimal. Nodoubleorfloattakes part anywhere. - K-line prices arrive as single-precision float text (
157.889999); they are read asdecimaland rounded away from zero to the decimals you pass (157.89). nDenominatorwas always 1. A quote with another denominator is still scaled by decimals and logscapital.price.fraction_unsupportedonce per symbol.- Times are the exchange's local time, converted by
CapitalExchangeClockwith daylight saving. Measured:FXandNYMare US Eastern,CMEandCBOTare Chicago. Other exchanges are inferred from their location and markedIsVerified = false;INDEXandICShave no mapping and fail withcapital.clock.unknown_exchange.
Measured and unmeasured
Measured (SKCOM 2.13.59 x64, real credentials, 2026-11-06, with the host's probe command):
OnReplyMessagemust be attached before login and answer -1; a successful login returns 0.- After
EnterMonitorLONGonlyOnConnect(3001)arrives — never 3003.GetQuoteStatusreports 0 connections, not over the limit. - The product list (
exchange,exchangeName,code,name,lastTradeDate,firstNoticeDate, 1,345 rows) and the details (…,orderExchange,orderCode,lastTradeDate,firstNoticeDate,kind) each end with a row starting with##. FX has last trading day99991231and first notice day0; a front-month future is the root plus0000(CME,NQ0000). - Subscriptions:
RequestStocks(-1, "FX,SUSDTWD#CME,NQ0000")and oneRequestTicksper symbol. - Quote prices divided by 10^
sDecimal(USD/TWD 3, JPY 2, EUR and CNY 4, NQ 2, GC 1, YM 0); for FXnTQtycounts quote updates, not volume. - Ticks: a subscription first replays the whole trading day (about 208,000 ticks for 7 symbols), then live ticks;
nPtrincreases per symbol; ticks may arrive before the first quote. - Time zones of FX, NYM, CME, CBOT (tick times against receive times, under 1 s of delay).
- K-lines: types 0 minute, 1 day, 2 week, 3 month (4–7 answer 1042); format
yyyy/MM/dd HH:mm, O, H, L, C, V(no time for day bars); a minute's time is its end; data stops at the previous trading day; there is no end event.
Domestic futures K-lines, measured (SKCOM 2.13.59 x64, a futures-only account, 2026-11-07, with the host's
probe-tw command):
SKQuoteLib_EnterMonitorLONGanswers 0, thenOnConnection(3001), then 3003 about 0.5 s later (product data loaded; queries work only after it). Running the overseasEnterMonitorLONGin the same process works.RequestKLineAMByDate(code, type, 1, session, from, to, 1): 0 means sent; an unknown code or a missing permission answers non-zero at once (2330gave 9999,TX00AM3023). Each bar arrives as oneOnNotifyKLineData, and every request ends with exactly oneOnKLineComplete("##"), even with no rows.- Type 0 minute, 4 daily; session 0 full (including the previous night), 1 AM; the dates are trading days.
- Formats: minute
yyyy/MM/dd HH:mm, O, H, L, C, V, dailyyyyy/MM/dd, O, H, L, C, V; a minute's time is its end, in Taipei time, dated by calendar day; day session 08:46–13:45, night session 15:01–05:00; empty zero-volume bars are sent.
Not measured:
- The
servemode as a whole against real SKCOM (including 0.2.0's domestic state machine,kline-tw, the domestic 3002 reconnect, the Job Object and the parent watchdog). Every call it makes was measured in the probe, in the same order, butserveitself has run only with the fake source. - The 3002 disconnect and the reconnect (whether
EnterMonitorLONGagain withoutLeaveMonitorworks, and whether stock indexes change afterwards; the host drops its index cache on reconnect to be safe). - Login failure codes (300, 306, 307, 600, 604) come from Capital's manual, not from a live failure.
- The time zones of exchanges other than FX, NYM, CME and CBOT.
- Capital's daily login limit and subscription limit (around 100 symbols by hearsay) are unconfirmed and not enforced.
- Quotes with
nDenominatorother than 1, and the meaning of the trading day forINDEXproducts. - The parts of 0.3.0 the 2026-11-09
probe-fullrun could not reach: account rows with positions or orders and the meaning of their amounts, rights coin types 1 / 2 and open-interest formats 2 / 3 (blocked by 1019), whetherReadCertByIDis required, tick and live bar dates after midnight, the domestic 10-code cap, what triggers the overseas best 5, overseas option ticks and time zones, and the text encoding of report strings. The item-by-item results are indocs/DESIGN.md§7.
0.3.0 measured (SKCOM 2.13.59 x64, a futures-only account, the 2026-11-09 night session, with the host's
probe-full command; details in DESIGN §5.3):
- The version string
2.13.59_x64, readable before login. - Domestic product lists have six fields (option series rows five), and every code comes as an "AM" day-only code and a
full-session code; quote
sDecimalis 2 for futures and options; the first quote after subscribing is partial. - Domestic tick
nDateis the calendar day, futures carry milliseconds only, and the replay starts with the pre-open simulated match (nSimulate= 1); a live bar's time is the end of its minute; empty book levels are 0 / 0. - Overseas best 5 and best 10 prices divide by 10^decimals and the best 10's first five equal the best 5; overseas
options send 3001 only, list five fields ending with
##,##,##,##,##, accept#-joined subscriptions and send an unscaled strike. - Accounts are
TF/OF; queries take the broker code plus the account; the status event arrives before the rows; without positions the domestic open interest returns001,…, the overseas one@@,001,…and the reportsM003; overseas rights have 31 fields.
The host's probe command
Ozakboy.TradeKit.Capital.Host probe --out <folder> logs in interactively (the password is masked) and records raw
SKCOM output (product lists, quotes, ticks, K-lines) to measure formats. It reads quotes only. The output folder may
contain the login ID in SKCOM's own logs (sklog\); keep it private. probe-tw --out <folder> is the domestic
counterpart, running a fixed set of RequestKLineAMByDate queries (it produced 0.2.0's domestic measurements).
probe-full --out <folder> [--seconds 180] [--accounts-only] measures the 0.3.0 formats: the version, the account
queries, domestic futures and options, the overseas best 5 and best 10, and overseas options. Its output masks account
numbers, holder names and the login ID (except sklog\), and its account part calls read-only queries only. 0.3.0 was
checked item by item against its 2026-11-09 output; the items it could not reach (see above) need another run under
other conditions (with positions, after midnight, with overseas option trades). Known issue: it does not exit on its
own after writing done; close it by hand (DESIGN §6).
Testing
No test touches SKCOM or needs credentials:
tests/FakeCapitalHostExelinks the host's real protocol layer (Serve/*.cs) and replaces SKCOM with a simulation of the measured behaviour:##-terminated products, only 3001, replay before live ticks with increasingnPtr, measured K-line text with minute-end times, exchange-local times, and a stray K-line row for another code. Environment variables make it fail the login (300 / 307), fail to create SKCOM, never connect, crash once or every time, or drop the connection and recover.Ozakboy.TradeKit.Capital.TestsdrivesCapitalHostSessionagainst it. Since 0.2.0 it also simulates the domestic side as measured on 2026-11-07 (3001 then 3003, trading-day dates with the previous night session, minute-end times, empty bars, the 60,000-row cap keeping the newest, 9999 for unknown codes) and can make it unavailable, disconnect or never send the end event; further tests start the fake host directly and check that it ends on stdin EOF, when its parent disappears, and when its Job Object closes. Since 0.3.0 it also simulates domestic live quotes, the overseas best 5 and best 10, overseas options and the account queries: what the 2026-11-09 run measured follows the measurement (product lists and AM codes, the partial first quote, millisecond ticks, the "no data" replies and garbled Chinese of the account queries, 31 overseas rights fields, the overseas option list format), and the rest (account rows with positions, overseas option tick time zones) follows the manual. It can make the overseas options unavailable, fail to create SKOrderLib, fail the initialisation or the certificate, fail the rights query with the account in the message (to check masking), and report another SKCOM version.Ozakboy.TradeKit.Capital.Host.Testsdrives the protocol layer in memory with a scriptableISkQuoteSource.
dotnet test -c Release
Security
- No default credentials; none is written to a file, a log, a command line or a
ToString. - The host's stderr is masked line by line before it reaches your logger; stdout carries protocol messages only.
- SKCOM's own log folder (
LogDirectory) may contain the login ID; it is your folder, keep it out of version control. - 0.3.0: account numbers, holder names and IDs are registered for masking as soon as they arrive, in the host and in
the client. They never reach stderr or a log.
ToString()of the account record types shows only the last 3 characters of the account. - 0.3.0: no order capability. Every method of the host's
ISKOrderLibthat could change an order, a position or funds is a placeholder that takes onlyIntPtr. The generator rejects forbidden names, andSkComOrderInteropSafetyTestschecks by reflection that the callable methods are exactly the reviewed read-only queries.
Dependency policy
Only the .NET BCL, first-party Microsoft packages and Ozakboy.* packages, judged by the transitive graph. No
Capital file is referenced or redistributed: tools/SkComInteropGen generates the COM interfaces from the metadata of
Capital's type library on your own machine, and Capital's DLL is not in the repository.
| Package | Version | Used for |
|---|---|---|
Ozakboy.Core.Abstractions |
0.3.0 | Result / Error |
Ozakboy.Security |
0.1.1 | SecretMasker |
Microsoft.Extensions.Logging.Abstractions |
10.0.14 | logging |
The test projects additionally use MSTest on Microsoft.Testing.Platform.
License
MIT, see LICENSE. SKCOM is Capital Securities' software under Capital's own terms.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Ozakboy.Core.Abstractions (>= 0.3.0)
- Ozakboy.Security (>= 0.1.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
0.3.0 —Built from Capital's V2.13.59 manual, then measured against real SKCOM 2.13.59 x64 with probe-full on 2026-11-09 (a futures-only account without positions or orders) and corrected; account rows with positions, dates after midnight, the 10-code cap and overseas option ticks are still unmeasured.SKCOM version: SkcomVersion, IsTestedSkcomVersion and CapitalSkcomVersion.Tested (2.13.59_x64); an unmeasured version logs capital.skcom.version_untested and keeps running. Domestic futures and options live quotes: GetDomesticProductsAsync, SubscribeDomesticAsync (up to 10 codes) with CapitalDomesticQuote, CapitalDomesticTick, CapitalBest5 and CapitalDomesticLiveBar. Overseas best 5 and best 10: SubscribeDepthAsync with CapitalBest10. Overseas options: OverseasOptionsState, OverseasOptionsStateChanged, GetOverseasOptionProductsAsync and the subscriptions, with quote, tick, best 5 and best 10 events. Read-only futures account queries: GetAccountsAsync, GetFutureRightsAsync, GetOpenInterestAsync, GetOverseaFutureRightsAsync, GetOverseaOpenInterestAsync, GetOrderReportAsync and GetFulfillReportAsync; SKOrderLib is initialised on the first account request, report queries are spaced 5.5 s apart, records have nullable named properties and RawFields, and account numbers are masked and never logged. No order, amend, cancel or fund-transfer capability exists. Host protocol: new methods, events, status members and error codes; a 0.2.x client still works with the 0.3.0 host. Measured and corrected: zero prices in domestic quotes and ticks and overseas option quotes are null (the first quote after subscribing is partial); CapitalDomesticLiveBar.OpenTimeUtc is the end-of-minute label minus one minute; product lists use fixed positions and every domestic code has a day-only "AM" twin; overseas rights have 31 fields (IbCode, FloatingProfitLoss added); an "@@,001" overseas open-interest reply is an empty list; CapitalSkcomText detects SKOrderLib's irreversibly garbled Chinese and restores the measured fixed phrases; the same account query sent again within seconds is rejected with code 1019.0.2.0 — Domestic futures historical K-lines: CapitalHostSession.GetDomesticKLinesAsync (SKQuoteLib RequestKLineAMByDate; TX00, MTX00, TM0000; minute and daily bars; full or AM session; trading-day dates; minute open time = Taipei end time minus one minute in UTC; empty bars kept; the 60,000-row cap keeps the newest), a domestic state with DomesticStateChanged kept apart from the overseas state (an unavailable domestic side never affects overseas quotes), and the kline-tw host method with capital.kline.timeout and capital.domestic.unavailable. Fixed: an orphaned host kept running after its parent ended; the client now puts the host in a KILL_ON_JOB_CLOSE Job Object and the host also ends when its parent PID disappears, with a forced exit if an orderly shutdown hangs. Docs: the host ships self-contained, so no .NET Desktop Runtime install is needed. Known: full-session closes are settlement prices; Capital's 2026-10-30 night-session volume is doubled; overseas minute bars go back about one month and one request has a row limit (oldest rows dropped silently), so record coverage from the first bar received. 0.1.0 — First release. CapitalHostSession starts and supervises the shipped SKCOM host (credentials only through child-process environment variables, never a file or a log), reads quote and tick events into a bounded channel that drops the oldest and counts, lists overseas products, subscribes, and fetches K-lines (minute, day, week, month). A failed login is never retried; an unexpected host exit is restarted with 1, 2, 5, 10 and 30 second backoff up to MaxRestarts and the subscriptions are re-sent. Measured against SKCOM 2.13.59 on 2026-11-06: product list and details, quotes, ticks with replay, K-lines, and the FX, CME, CBOT and NYM time zones. Not measured: the 3002 reconnect, login failure codes other than by the manual, and the time zones of other exchanges (IsVerified = false). Full notes: https://github.com/ozakboy/Ozakboy.TradeKit.Capital/blob/main/CHANGELOG.md