SEC/i — Securities / Investment Portfolio Accounting

SteelFrame X application operation manual  ·  ← back to Operation Manuals  ·  Sign On

SEC/i is a securities and investment-portfolio accounting application: it books buy and sell trades, opens and recursively relieves tax lots under FIFO/LIFO, tracks positions and multi-currency cash, applies corporate-action stock splits, rolls sub-accounts up a hierarchy, marks the portfolio to market, and posts every money movement to a self-balancing general ledger. Unlike the RPG-driven applications, SEC/i is pure SQL PL: there is no DDS display file and no 5250 screen. Every routine — procedure, function, and trigger — is invoked directly, through STRSQL, an EXEC SQL CALL from any host program, or a JDBC/CLI client. The application was built SQL-first specifically to exercise the hard corners of SQL PL: procedural and WITH RECURSIVE recursion, handler-in-handler RESIGNAL across CALL frames, nested cursors, SAVEPOINT/ROLLBACK TO SAVEPOINT, MERGE, high-precision DECIMAL, GENERATED columns, global temporary tables, and trigger cascades. This manual is grounded entirely in the committed source (sqlpl-app-sec/src/schema.sql, routines.sql, seed.mjs, and the test/sec_daily.mjs driver). Everything runs in library SECLIB.

Contents

A. Overview & Architecture ↑ top

A.1 What it does

SEC/i keeps the books for an investment portfolio across a hierarchy of sub-accounts:

A.2 A SQL-PL-only application (no 5250 screen)

SEC/i has no display file, no subfile, and no interactive program. All business logic lives in DB2 for i functions, stored procedures, and table triggers in routines.sql; all state lives in the SECLIB tables in schema.sql. There is nothing to "sign on to" — the application is its SQL-PL surface. It is driven three equivalent ways:

The benefit is that the rules are one auditable place and the same procedure is reachable from batch, ad hoc, and embedded contexts identically. Integrity is enforced in the data layer by triggers (position cannot go negative; every lot adjustment is audited; a cascade counts the audits) rather than by any calling program.

A.3 Component & flow

  TRADE ENTRY            PROCESSING                    VALUATION / REPORTING
  -----------            ----------                    ---------------------
  INSERT INTO TRADE      SP_BOOK_TRADE (one trade)     SP_MTM_RUN   -> MTMSNAP
    (TSTAT='E')            buy  -> open LOT               (price x FX, per position)
        |                  sell -> SP_RELIEVE_LOT        SP_MTM_REPORT (2 result sets)
        v                          (RECURSIVE, FIFO/LIFO)
  SP_TRADE_BATCH          -> LOTREL / REALIZEDPL       SP_ROLLUP_ACCT (RECURSIVE)
    books all 'E' trades  -> CASHLEDG + GLENTRY        SP_ROLLUP_ALL_MASTERS -> ROLLUPCACHE
    GTT + SAVEPOINT       -> SP_UPSERT_POSITION (MERGE) FN_SUBTREE_QTY (WITH RECURSIVE)
    LEAVE/ITERATE            -> POSITION

  CORPORATE ACTION       SP_PROCESS_SPLIT (nested cursor: accounts x lots)
  INSERT INTO CORPACT       rewrites every open LOT of the security, re-MERGEs POSITION

  triggers on LOT/POSITION:  TR_LOT_AUDIT -> (SECAUDIT) -> TR_LOT_AUDIT_CASCADE -> CASCADEMARK
                             TR_POSITION_VETO (BEFORE UPDATE, QTYHELD < 0 veto)

A single event — booking one sell — flows: SP_BOOK_TRADE takes a SAVEPOINT, calls SP_RELIEVE_LOT which recurses lot-by-lot writing LOTREL rows and updating LOT.OPENQTY (firing TR_LOT_AUDIT, which cascades into TR_LOT_AUDIT_CASCADE); then it posts CASHLEDG and the balanced GLENTRY set, inserts REALIZEDPL, and MERGEs the position via SP_UPSERT_POSITION. If relief runs out of shares it SIGNALs 75020, the outer handler ROLLBACKs to the savepoint, and the trade is marked rejected — leaving the lots exactly as they were.

A.4 Object inventory

ObjectTypeRole
ACCTTableChart of sub-accounts (self-referencing hierarchy).
SECMASTTableSecurities master (incl. DECIMAL(31,6) FACEVAL).
PRICEHISTTableDaily close prices (drives MTM).
FXHISTTableFX rate history (CCY→USD).
TRADETableTrade blotter (entered/booked/rejected).
LOTTableOpen/relieved tax lots (identity PK).
LOTRELTablePer-(sell,lot) relief audit & realized-P&L basis.
POSITIONTableDenormalized qty/avg-cost per (account,security), MERGE-maintained.
CASHLEDGTableAppend-only cash ledger (idempotency key SRCDOC+SRCSEQ).
CORPACTTableCorporate actions (stock splits).
REALIZEDPLTableRealized P&L (GENERATED NETGAIN column).
MTMSNAPTableMark-to-market snapshot per (account,security,date).
GLACCT / GLENTRYTableGL balances + balanced entry ledger.
SECAUDITTableDiagnostics / reject / lot-adjust audit trail.
ROLLUPCACHETableRecursive roll-up totals per master account.
CASCADEMARKTableTrigger-cascade hit counter.
FN_* (2)SQL functionsSettlement-date math; recursive subtree quantity.
SP_* (9)SQL proceduresBook/relieve/upsert/batch/rollup/split/MTM/report.
TR_* (3)TriggersLot audit, cascade counter, position veto.

The full catalogue is 2 functions + 9 procedures + 3 triggers over 15 base tables. Sections D and F expand each. All names are the application's own, grounded in the committed source.

B. Online / Access ↑ top

B.1 STRSQL CALL access (there is no screen)

Honest statement: SEC/i ships no 5250 display file and no interactive program. There is no menu, no subfile, no command-key handling — nothing to sign on to. The "online" surface of this application is the SQL-PL routines themselves, reached from an interactive SQL session (STRSQL), from an EXEC SQL CALL in a host program, or from a JDBC/CLI client. The operator equivalent of "open a screen and press Enter" is "type a CALL or SELECT and press Enter". Before invoking anything, the job's library list must include SECLIB — the tested job runs with LIBL = QSYS QGPL SECLIB QTEMP and CURLIB = SECLIB.

To do thisType in STRSQL (or CALL)
Book one entered tradeCALL SECLIB.SP_BOOK_TRADE('T00001', ?, ?)
Book every entered trade in a runCALL SECLIB.SP_TRADE_BATCH('BATCH1', 100, ?, ?, ?)
Apply a corporate-action splitCALL SECLIB.SP_PROCESS_SPLIT('CA00001', ?, ?)
Mark the portfolio to market for a dateCALL SECLIB.SP_MTM_RUN(20260802, ?, ?)
Report MTM (two result sets)CALL SECLIB.SP_MTM_REPORT(20260802)
Roll a master account's holdings upCALL SECLIB.SP_ROLLUP_ACCT('MASTER01','AAPL000001', ?)
Rebuild the whole roll-up cacheCALL SECLIB.SP_ROLLUP_ALL_MASTERS('RUN1', ?)
Ask a subtree quantity (scalar function)VALUES SECLIB.FN_SUBTREE_QTY('MASTER01','AAPL000001')

The ? placeholders are the procedures' OUT parameters. In STRSQL, host-variable OUTs are shown after the call returns; from a client (as in sec_daily.mjs) they come back as the call's outs[]. A trade is staged by inserting a row into SECLIB.TRADE with TSTAT='E'; booking flips it to 'B' (booked) or 'R' (rejected). SEC/i has no notion of "the current user's session state" — each CALL is a complete unit of work.

Two procedures return dynamic result sets rather than only OUT parameters: SP_MTM_REPORT (DYNAMIC RESULT SETS 2 — a per-position detail set and an account-level summary set) and SP_TRADE_BATCH (DYNAMIC RESULT SETS 1 — the per-trade batch outcome via a WITH RETURN WITH HOLD cursor over a session temporary table). In STRSQL these display as result grids; from a client they arrive as resultSets[].

B.2 Controls & audit

Honest statement: SEC/i does not implement a four-eyes maker–checker or separate-authorization workflow — there is no "one user enters, a second approves" gate. A trade inserted as 'E' is booked immediately by whoever calls SP_BOOK_TRADE. The control posture the application does have is entirely data-layer:

In sum the control model is audit + idempotency + state gating + a balanced-ledger invariant, enforced in the data layer, not a segregation-of-duties approval workflow.

C. Batch & Cycle Procedures ↑ top

SEC/i has no time-triggered "nightly job" wrapper program; its cycle is a fixed sequence of procedure CALLs an operator or scheduler drives in order. There is no control-row idiom (no LNCTL analogue) — the as-of date and run tokens are passed as CALL parameters. A typical day is: stage trades → SP_TRADE_BATCH → process any corporate actions (SP_PROCESS_SPLIT) → rebuild roll-ups (SP_ROLLUP_ALL_MASTERS) → mark to market (SP_MTM_RUN) → report (SP_MTM_REPORT) → verify the GL invariant.

-- stage trades onto the blotter (TSTAT='E'), then run the day:
CALL SECLIB.SP_TRADE_BATCH('BATCH1', 100, ?, ?, ?);   -- book all entered trades
CALL SECLIB.SP_PROCESS_SPLIT('CA00001', ?, ?);        -- apply any split
CALL SECLIB.SP_ROLLUP_ALL_MASTERS('RUN1', ?);         -- rebuild ROLLUPCACHE
CALL SECLIB.SP_MTM_RUN(20260802, ?, ?);               -- snapshot MTMSNAP for the as-of date
CALL SECLIB.SP_MTM_REPORT(20260802);                  -- detail + summary result sets

C.1 Full procedure / trigger table

RoutinePurposeCalls / firesInputsOutputs / effectsFrequency
SP_BOOK_TRADE Book one trade: buy opens a lot, sell relieves lots recursively; post cash + GL; MERGE position. SP_RELIEVE_LOT, SP_UPSERT_POSITION; fires TR_LOT_AUDIT. P_TRDNO. OUT P_RC (0 booked / 1 already / 2 halted / 3 insufficient / 9 unexpected), P_MSG; LOT/LOTREL/REALIZEDPL/CASHLEDG/GLENTRY/POSITION rows. Per trade / on demand.
SP_RELIEVE_LOT Recursively relieve one lot at a time (FIFO/LIFO) until the sell is covered. Self-CALL; fires TR_LOT_AUDIT. P_SELLTRD, P_ACCTNO, P_SECID, P_NEEDQTY, P_SELLPX, P_METHOD, P_COMMPERSH. LOTREL rows; LOT.OPENQTY/LSTAT updates; SIGNAL 75020 when exhausted. Called by SP_BOOK_TRADE.
SP_UPSERT_POSITION Recompute qty + weighted avg cost from open lots and MERGE into POSITION. MERGE; may fire TR_POSITION_VETO. P_ACCTNO, P_SECID. POSITION row upserted. Called by booking / split.
SP_TRADE_BATCH Book every ENTERED trade in TRDNO order (GTT + SAVEPOINT + labeled LEAVE/ITERATE + result set). SP_BOOK_TRADE per trade. P_RUNTOKEN, P_MAXBOOK. OUT P_BOOKED, P_REJECTED, P_SKIPPED; SECAUDIT BATCH_RUN row; 1 result set (per-trade outcome). Daily.
SP_PROCESS_SPLIT Apply a stock split to every open lot of the security across every holding account (nested cursor). SP_UPSERT_POSITION; fires TR_LOT_AUDIT. P_CAID. OUT P_LOTSADJ, P_ACCTSADJ; LOT rewrites; CORPACT→'P'. On corporate action.
SP_ROLLUP_ACCT Sum a security's open qty across an account and its subtree (procedural recursion, OUT threaded). Self-CALL per hierarchy level. P_ACCTNO, P_SECID. OUT P_QTY. On demand.
SP_ROLLUP_ALL_MASTERS Drive SP_ROLLUP_ACCT for every master × every held security; write ROLLUPCACHE. SP_ROLLUP_ACCT; MERGE. P_ASOFTOK. OUT P_WRITTEN; ROLLUPCACHE rows. Daily.
SP_MTM_RUN Price every open position at latest px on/before as-of, apply FX, write MTMSNAP. MERGE per position. P_ASOF. OUT P_MARKED, P_NOPRICE; MTMSNAP rows. Daily (nightly).
SP_MTM_REPORT Return two result sets for an as-of date: per-position detail and per-account summary. P_ASOF. 2 dynamic result sets. On demand / reporting.

Triggers TR_LOT_AUDIT, TR_LOT_AUDIT_CASCADE and TR_POSITION_VETO are not called directly; they fire automatically off the DML the procedures above perform (section F.3).

C.2 Daily cycle detail

SP_TRADE_BATCH

Books every trade with TSTAT='E' in TRDNO order. Its BATCH_LOOP FOR-loop LEAVEs once booked+rejected reaches P_MAXBOOK (a safety cap), and ITERATEs past any trade whose account is closed (ACCT.ASTAT='C'), counting it as skipped. Each booking is wrapped in its own SAVEPOINT SP_TRADE: a genuinely unexpected RC=9 rolls back only that trade's work without aborting the batch; handled business outcomes (halted / insufficient) are counted as rejected and left in place. A DECLARE GLOBAL TEMPORARY TABLE SESSION.BATCHSTAGE ... WITH REPLACE stages each trade's outcome, surfaced as the call's one dynamic result set via a WITH RETURN WITH HOLD cursor.

Expected OUT parms (seed batch: 1 buy books, 1 oversell rejects, 1 closed-acct skipped):
  P_BOOKED   = 1
  P_REJECTED = 1     the oversell (insufficient shares)
  P_SKIPPED  = 1     the closed-account trade, ITERATEd past
  result set : 3 rows in BATCHSTAGE (BOOKED / REJECTED-3 / SKIPPED-CLOSED-ACCT)

SP_MTM_RUN

For every POSITION with QTYHELD > 0, looks up the security's latest PRICEHIST.PXCLOSE on or before P_ASOF and the currency's latest FXHIST.RATE, then MERGEs a snapshot: MKTVALUE = QTYHELD × PX × FX, COSTBASIS = QTYHELD × AVGCOST, UNREALGN = MKTVALUE - COSTBASIS. A position with no price on/before the date is skipped and counted in P_NOPRICE (via a labeled ITERATE POS_CUR_LBL).

Expected (SUB0003/AAPL after split: 60 sh @96.05, px 195.75, USD fx 1.0):
  COSTBASIS = 60 x 96.05  = 5763.00
  MKTVALUE  = 60 x 195.75 = 11745.00
  UNREALGN  = 11745.00 - 5763.00 = 5982.00

SP_ROLLUP_ALL_MASTERS

Nested FOR-loop: outer over every master account (PARENTACC IS NULL), inner over every security with an open lot anywhere. For each pair it calls the recursive SP_ROLLUP_ACCT and, when the total is non-zero, MERGEs it into ROLLUPCACHE. On the seed set it writes exactly the three non-zero (master, security) combos: MASTER01/AAPL=30, MASTER01/MSFT=10, STANDALONE/AAPL=5 — proving one master's subtree never leaks into a sibling's.

C.3 Ordering & dependencies

D. Data Files (data dictionary) ↑ top

All tables are in library SECLIB, grounded in schema.sql. Dates are stored as INT in YYYYMMDD form. Quantities are DECIMAL(19,4) (fractional shares allowed for funds); prices and cost bases are DECIMAL(19,6); cash and GL amounts are DECIMAL(19,2); SECMAST.FACEVAL is a deliberately wide DECIMAL(31,6).

ACCT — Chart of sub-accounts (PK ACCTNO)

FieldTypeMeaning
ACCTNOVARCHAR(10)Account number (PK).
PARENTACCVARCHAR(10)Parent account; NULL = top-level (master) account.
ACCTNMVARCHAR(40)Account name.
BASECCYCHAR(3)Base reporting currency (default USD).
ASTATCHAR(1)A active, C closed (batch skips a closed account's trades).

Seeded hierarchy: MASTER01 → {SUB0001 → SUB0003, SUB0002}; STANDALONE is an independent master (EUR base) used to prove subtree isolation.

SECMAST — Securities master (PK SECID)

FieldTypeMeaning
SECIDVARCHAR(12)CUSIP-like identifier (PK).
SECNMVARCHAR(40)Security name.
SECTYPECHAR(1)E equity, B bond, F fund.
CCYCHAR(3)Trading/pricing currency.
LOTSIZEINTRound-lot size (informational).
FACEVALDECIMAL(31,6)Bond face value — the wide-DECIMAL probe field.
SSTATCHAR(1)A active, H halted (a halted security rejects booking, RC=2).

PRICEHIST — Daily price history (PK SECID, PXDATE)

FieldTypeMeaning
SECID / PXDATEVARCHAR(12) / INTSecurity + price date YYYYMMDD (PK).
PXCLOSEDECIMAL(19,6)Close price. MTM uses the latest on/before the as-of date.

FXHIST — FX rate history (PK CCY, FXDATE)

FieldTypeMeaning
CCY / FXDATECHAR(3) / INTCurrency + rate date YYYYMMDD (PK).
RATEDECIMAL(15,8)Units of USD per 1 unit CCY (USD = 1.0).

TRADE — Trade blotter (PK TRDNO)

FieldTypeMeaning
TRDNOVARCHAR(12)Trade number (PK).
ACCTNO / SECIDVARCHAR(10) / VARCHAR(12)Account and security traded.
TRDSIDECHAR(1)B buy, S sell.
TRDQTYDECIMAL(19,4)Quantity (fractional allowed).
TRDPXDECIMAL(19,6)Trade price per share.
TRDCCYCHAR(3)Trade currency (default USD).
TRDDATE / SETTDATEINTTrade / settlement date (YYYYMMDD).
COMMISHDECIMAL(13,2)Commission (loaded into buy cost / netted from sell proceeds).
RELMETHCHAR(1)F FIFO, L LIFO (sell relief method).
TSTATCHAR(1)E entered, B booked, R rejected.

LOT — Tax lots (identity LOTID; unique OPENTRD)

FieldTypeMeaning
LOTIDINT identityGenerated-always lot id.
ACCTNO / SECIDVARCHAROwning account and security.
OPENTRDVARCHAR(12)The buy TRDNO that opened this lot (unique index LOT_TRD).
OPENDATEINTLot open date (FIFO/LIFO ordering key).
ORIGQTY / OPENQTYDECIMAL(19,4)Original / remaining unrelieved quantity.
COSTPXDECIMAL(19,6)Unit cost basis (commission-loaded; split-adjusted).
LSTATCHAR(1)O open, X fully relieved.

LOTREL — Lot-relief detail (identity RELID)

FieldTypeMeaning
RELIDINT identityRelief id.
SELLTRD / LOTIDVARCHAR / INTThe sell trade and the lot it consumed.
RELQTYDECIMAL(19,4)Quantity taken from that lot.
RELCOSTDECIMAL(19,6)Cost basis relieved (RELQTY × lot COSTPX).
RELPROCDECIMAL(19,6)Proceeds allocated (RELQTY × sell px net of pro-rated commission).
REALGAINDECIMAL(19,6)RELPROC − RELCOST.

POSITION — Current holdings (PK ACCTNO, SECID)

FieldTypeMeaning
ACCTNO / SECIDVARCHARAccount + security (PK).
QTYHELDDECIMAL(19,4)Sum of open-lot quantities (MERGE-maintained).
AVGCOSTDECIMAL(19,6)Cost-weighted average of open lots.

CASHLEDG — Cash ledger (identity CSEQ; unique SRCDOC, SRCSEQ)

FieldTypeMeaning
CSEQINT identityLedger sequence.
ACCTNO / CCYVARCHAR / CHAR(3)Account and currency of the posting.
AMTDECIMAL(19,2)Signed amount (cash in +, cash out −).
CDESCVARCHAR(60)Description.
SRCDOC / SRCSEQVARCHAR(12) / INTIdempotency key (unique index CASHLEDG_TOKEN).
CDATEINTPosting date.

CORPACT — Corporate actions (PK CAID)

FieldTypeMeaning
CAIDVARCHAR(12)Corporate-action id (PK).
SECIDVARCHAR(12)Affected security.
CATYPECHAR(1)S split.
RATIONEW / RATIOOLDDECIMAL(9,4)Split ratio (2-for-1 ⇒ NEW=2, OLD=1). Factor = NEW/OLD.
CADATEINTEffective date.
CASTATCHAR(1)E entered, P processed (re-run guard).

REALIZEDPL — Realized P&L (identity RPID; GENERATED NETGAIN)

FieldTypeMeaning
RPIDINT identityRow id.
ACCTNO / SECID / SELLTRDVARCHARAccount, security, sell trade.
RELQTYDECIMAL(19,4)Quantity relieved.
PROCEEDS / COSTBASISDECIMAL(19,6)Allocated proceeds / relieved cost.
FEESDECIMAL(13,2)Fees applied to this relief (0 in the shipped path).
NETGAINDECIMAL(19,6)GENERATED ALWAYS AS (PROCEEDS − COSTBASIS − FEES) — never written directly.

MTMSNAP — Mark-to-market snapshot (PK ACCTNO, SECID, MTMDATE)

FieldTypeMeaning
ACCTNO / SECID / MTMDATEVARCHAR / INTPosition + as-of date (PK).
QTYHELDDECIMAL(19,4)Quantity marked.
COSTBASISDECIMAL(19,6)QTYHELD × AVGCOST.
MKTVALUEDECIMAL(19,6)QTYHELD × price × FX.
UNREALGNDECIMAL(19,6)MKTVALUE − COSTBASIS.

GLACCT / GLENTRY — General ledger

FieldTypeMeaning
GLACCT.GLCODEVARCHAR(10)GL account code (PK) — CASH, SECURITIES, REALGAIN, COMMISH.
GLACCT.GLDESC / BALVARCHAR / DECIMAL(19,2)Description / running balance.
GLENTRY.GSEQINT identityEntry sequence.
GLENTRY.GLCODE / AMTVARCHAR / DECIMAL(19,2)Posted account and signed amount.
GLENTRY.SRCDOC / SRCSEQVARCHAR / INTIdempotency key (unique index GLENTRY_TOKEN).
GLENTRY.GLTEXTVARCHAR(60)Narrative.

Invariant: every posted document nets to zero, and SUM(GLENTRY.AMT)=0 across the whole run; the GLACCT balances mirror it.

SECAUDIT — Audit / diagnostics (identity AUDSEQ)

FieldTypeMeaning
AUDSEQINT identityAudit sequence.
EVTTYPEVARCHAR(20)LOT_ADJUST, TRADE_REJECT, CLEANUP_FAIL, BATCH_RUN.
REFDOCVARCHAR(12)Referenced trade / run token.
EVTTEXTVARCHAR(100)Human-readable detail.

ROLLUPCACHE / CASCADEMARK

FieldTypeMeaning
ROLLUPCACHE (MASTERACC, SECID)PKMaster + security; TOTQTY subtree total, ASOFTOK run token.
CASCADEMARK (MARKID, HITS)PK MARKIDSingle-row counter TR_LOT_AUDIT_CASCADE bumps once per LOT_ADJUST audit.

Relationships

E. Operations Runbook ↑ top

E.1 The daily cycle

SEC/i's day is a fixed sequence of CALLs in STRSQL (or from a scheduler that opens an SQL session). There is no control row to advance and no parameterless job — the as-of date and a run token are passed on the CALL.

  1. Stage trades. Insert the day's trades into SECLIB.TRADE with TSTAT='E'.
  2. Book the batch. CALL SECLIB.SP_TRADE_BATCH('BATCH-yyyymmdd', 1000, ?, ?, ?). Confirm P_BOOKED + P_REJECTED + P_SKIPPED equals the entered-trade count and inspect the returned per-trade result set.
  3. Process corporate actions. For each entered CORPACT: CALL SECLIB.SP_PROCESS_SPLIT('CAxxxxx', ?, ?); confirm P_LOTSADJ/P_ACCTSADJ and that CASTAT flipped to 'P'.
  4. Rebuild the roll-up cache. CALL SECLIB.SP_ROLLUP_ALL_MASTERS('RUN-yyyymmdd', ?).
  5. Mark to market. CALL SECLIB.SP_MTM_RUN(<YYYYMMDD>, ?, ?); note P_MARKED and P_NOPRICE (positions lacking a price on/before the date).
  6. Report. CALL SECLIB.SP_MTM_REPORT(<YYYYMMDD>) for the detail + summary result sets.
  7. Verify the GL invariant (below).

Pre-checks: the job's library list includes SECLIB; the day's PRICEHIST/FXHIST rows are loaded (else positions count as P_NOPRICE).

E.2 Reconciling figures

These are the same figures the volume driver checks against an independent hand-derived oracle (test/sec_daily.mjs). Worked against the seed set:

-- GL invariant (must be exactly zero):
SELECT COALESCE(SUM(AMT),0) FROM SECLIB.GLENTRY;                       -- 0
SELECT SRCDOC, SUM(AMT) FROM SECLIB.GLENTRY GROUP BY SRCDOC
  HAVING ABS(SUM(AMT)) > 0.01;                                        -- no rows

-- realized P&L for a FIFO sell across two lots (T00003, sell 120 @200 comm 12):
SELECT SELLTRD, SUM(REALGAIN) FROM SECLIB.LOTREL WHERE SELLTRD='T00003'
  GROUP BY SELLTRD;                                                    -- 1136.00 (980.00 + 156.00)

E.3 Failure & re-run rules

Each procedure reports success by SQLCODE 0 and its OUT parameters; a business rejection surfaces as an RC/return code or an application SQLSTATE (section F.5), not a crash.

SituationBehaviourAction
Re-book an already-booked tradeSP_BOOK_TRADE returns RC=1, opens no second lot.Safe no-op — naturally idempotent per trade (TSTAT terminal at B).
Sell exceeds open sharesRecursion SIGNALs 75020; outer EXIT handler ROLLBACK TO SAVEPOINT SP_RELIEF, marks trade R, writes a TRADE_REJECT audit. RC=3.No partial LOTREL/lot change survives. Correct the quantity and re-stage as a new trade.
Security haltedBooking returns RC=2, marks trade R.Un-halt (SSTAT='A') and re-stage.
Re-run SP_TRADE_BATCHOnly TSTAT='E' trades are considered; booked/rejected are terminal.Books/rejects nothing new; a closed-account trade is legitimately re-skipped each run until the account reopens.
Re-run SP_PROCESS_SPLIT on a processed actionCASTAT='P' guard returns 0/0.Safe no-op — a split is never applied twice.
Re-run SP_MTM_RUN for the same dateMERGE upserts the same MTMSNAP rows.Idempotent per (account, security, date).
Re-run SP_ROLLUP_ALL_MASTERSMERGE overwrites ROLLUPCACHE totals.Idempotent — recompute freely.
Position would go negativeTR_POSITION_VETO raises SQLSTATE 75021.Last-line defense; indicates upstream lot data is inconsistent — investigate LOT/LOTREL.
Because every lot adjustment is audited (SECAUDIT via TR_LOT_AUDIT, counted by the cascade), every money movement is journaled to CASHLEDG/GLENTRY with idempotency tokens, and every relief is detailed in LOTREL, any run's effect is fully reconstructable after the fact for reconciliation and recovery.

F. Developer Reference ↑ top

The complete SQL-PL surface, from routines.sql. All objects are in library SECLIB. Signatures are given exactly as declared.

F.1 Functions (2)

FN_SETTLE_DATE (P_TRDDATE INT, P_DAYS INT) RETURNS INT
Adds P_DAYS to a YYYYMMDD trade date and returns the settlement date as YYYYMMDD. It splits the integer into Y/M/D, builds an ISO string, adds P_DAYS DAYS via DATE(), and re-packs to an integer. Month/year boundaries are handled by the DATE arithmetic (e.g. 20260830 + 3 → 20260902). The date math is done through a DATE() round-trip rather than a labeled-duration operator on the raw integer — a deliberate calendar-safe pattern noted in the source.
FN_SUBTREE_QTY (P_MASTERACC VARCHAR(10), P_SECID VARCHAR(12)) RETURNS DECIMAL(19,4)
Total open-lot quantity for one security across an account and all descendant sub-accounts, via WITH RECURSIVE. The recursive CTE (SUBACC) seeds with the master account and walks children through PARENTACC; the outer query sums LOT.OPENQTY for LSTAT='O' over the subtree. Assigned as a scalar-subquery expression (SET V_TOTAL = ( WITH RECURSIVE ... SELECT ... )) rather than SELECT ... INTO — the source notes this is required because a compound body's SELECT-INTO dispatcher only recognizes text that starts with the literal keyword SELECT, so a statement led by WITH must be wrapped as a scalar subquery.

F.2 Procedures (9)

SP_BOOK_TRADE (IN P_TRDNO VARCHAR(12); OUT P_RC INT, P_MSG VARCHAR(100))
Books one trade. Buy: opens a LOT at COSTPX = TRDPX + COMMISH/TRDQTY, posts cash out and a SECURITIES/CASH GL pair. Sell: takes SAVEPOINT SP_RELIEF, calls the recursive SP_RELIEVE_LOT, then posts cash in and a three-leg GL set (CASH / SECURITIES cost-out / REALGAIN), inserts one REALIZEDPL row per relieved lot, and the cost-out uses SUM(LOTREL.RELCOST) (exact relieved basis). Both sides then MERGE the position and mark the trade 'B'. Nested handler-in-handler: the outer EXIT handler for SQLSTATE 75020 ROLLBACKs to the savepoint and marks the trade rejected; an inner CONTINUE handler inside that handler's own body guards the cleanup UPDATE and records CLEANUP_FAIL if it fails. RC: 0 booked, 1 already booked, 2 security halted, 3 insufficient shares, 9 unexpected.
SP_RELIEVE_LOT (IN P_SELLTRD, P_ACCTNO, P_SECID, P_NEEDQTY DECIMAL(19,4), P_SELLPX DECIMAL(19,6), P_METHOD CHAR(1), P_COMMPERSH DECIMAL(19,6))
Recursive. Picks the oldest (P_METHOD='F', ORDER BY OPENDATE ASC) or newest ('L', DESC) open lot, takes MIN(P_NEEDQTY, OPENQTY), writes a LOTREL row (RELCOST=take×COSTPX, RELPROC=take×(sellpx−commpersh), REALGAIN=RELPROC−RELCOST), updates the lot (zero+'X' if fully consumed, else decrement), and self-CALLs for the remainder. Base cases: P_NEEDQTY≤0 (return) or no open lot left (SIGNAL SQLSTATE '75020').
SP_UPSERT_POSITION (IN P_ACCTNO VARCHAR(10), P_SECID VARCHAR(12))
Recomputes QTYHELD = SUM(OPENQTY) and AVGCOST = SUM(OPENQTY×COSTPX)/SUM(OPENQTY) over open lots, then MERGEs into POSITION (UPDATE on match, INSERT otherwise).
SP_TRADE_BATCH (IN P_RUNTOKEN VARCHAR(20), P_MAXBOOK INT; OUT P_BOOKED, P_REJECTED, P_SKIPPED INT) DYNAMIC RESULT SETS 1
Books every TSTAT='E' trade in TRDNO order. Labeled BATCH_LOOP FOR-cursor: LEAVE at the P_MAXBOOK cap, ITERATE past a closed-account trade (counted skipped). Each booking wrapped in SAVEPOINT SP_TRADE; an unexpected RC=9 rolls back only that trade. Stages outcomes in a DECLARE GLOBAL TEMPORARY TABLE SESSION.BATCHSTAGE ... WITH REPLACE, surfaced by a WITH RETURN WITH HOLD cursor as the one result set; writes a BATCH_RUN audit row.
SP_PROCESS_SPLIT (IN P_CAID VARCHAR(12); OUT P_LOTSADJ, P_ACCTSADJ INT)
Applies a split (FACTOR = RATIONEW/RATIOOLD) to every open lot of the security across every holding account. Nested cursor-per-row: outer FOR over distinct holding accounts, inner FOR (opened fresh per account) over that account's open lots, each set to OPENQTY×FACTOR, ORIGQTY×FACTOR, COSTPX/FACTOR; re-MERGEs the position per account. Guarded by CASTAT='P' (no-op if already processed); flips CORPACT to 'P' at the end.
SP_ROLLUP_ACCT (IN P_ACCTNO VARCHAR(10), P_SECID VARCHAR(12); OUT P_QTY DECIMAL(19,4))
Recursive (procedural self-CALL, distinct from FN_SUBTREE_QTY's CTE). Sets P_QTY to the account's own open-lot sum, opens a cursor over child accounts (PARENTACC=P_ACCTNO), and for each child self-CALLs and adds the returned quantity. Base case: an account with no children contributes only its own lots. A CONTINUE HANDLER FOR NOT FOUND ends the child fetch loop.
SP_ROLLUP_ALL_MASTERS (IN P_ASOFTOK VARCHAR(20); OUT P_WRITTEN INT)
Nested FOR loop: outer over masters (PARENTACC IS NULL), inner over distinct securities with an open lot; calls SP_ROLLUP_ACCT and MERGEs each non-zero total into ROLLUPCACHE, counting rows written.
SP_MTM_RUN (IN P_ASOF INT; OUT P_MARKED, P_NOPRICE INT)
Labeled POS_CUR_LBL FOR-cursor over positions with QTYHELD>0. Per row: a scalar-subquery lookup of the latest PXCLOSE on/before P_ASOF (no price ⇒ ITERATE POS_CUR_LBL, bump P_NOPRICE), the currency's latest FX rate, then a MERGE into MTMSNAP with market value, cost basis, and unrealized gain.
SP_MTM_REPORT (IN P_ASOF INT) DYNAMIC RESULT SETS 2
Opens two WITH RETURN cursors: (1) per-position detail from MTMSNAP for the date, (2) an account-level aggregate (SUM of cost/market/unrealized, GROUP BY account).

F.3 Triggers (3)

TR_LOT_AUDIT — AFTER UPDATE OF OPENQTY ON LOT, FOR EACH ROW
Writes a SECAUDIT 'LOT_ADJUST' row recording the old→new open quantity. Fires from both the recursive relief UPDATE and the split's inner-cursor UPDATE (BEGIN ATOMIC body).
TR_LOT_AUDIT_CASCADE — AFTER INSERT ON SECAUDIT, WHEN (N.EVTTYPE='LOT_ADJUST'), FOR EACH ROW
A trigger fired by another trigger's own write: on each LOT_ADJUST audit row it inserts or increments the single CASCADEMARK counter row. The WHEN filter on EVTTYPE keeps it from re-firing itself (proving a controlled trigger-fires-trigger cascade).
TR_POSITION_VETO — BEFORE UPDATE ON POSITION, WHEN (N.QTYHELD < 0), FOR EACH ROW
Raises SIGNAL SQLSTATE '75021' to refuse any update that would drive a position negative — the last line of defense behind the MERGE-based upsert.

F.4 SQL-PL patterns

The routines deliberately exercise these SQL-PL constructs; developers maintaining SEC/i will see each:

F.5 Application SQLSTATE table

SQLSTATERaised byMeaning
75020SP_RELIEVE_LOTInsufficient open-lot shares to relieve the sell (caught by SP_BOOK_TRADE’s outer handler → RC=3).
75021TR_POSITION_VETOA position update would drive QTYHELD negative (vetoed).

Business outcomes that are not SQLSTATE-signalled are reported through SP_BOOK_TRADE's P_RC return code instead: 1 already booked, 2 security halted, 3 insufficient shares (the caught 75020), 9 unexpected. The GL-nets-to-zero invariant is not a SQLSTATE but a post-run reconciliation check (section E.2).

G. Glossary ↑ top

Tax lot (LOT)
One purchase parcel of a security at a known unit cost. Sells relieve lots; a lot is O (open) until fully consumed (X).
FIFO / LIFO relief
The order lots are consumed on a sell: First-In-First-Out (oldest lot first) or Last-In-First-Out (newest first), selected per trade by RELMETH and walked recursively by SP_RELIEVE_LOT.
Realized vs unrealized P&L
Realized gain/loss is locked in when a lot is relieved (proceeds − cost basis, in LOTREL/REALIZEDPL); unrealized is the paper gain on still-held positions from mark-to-market (MTMSNAP.UNREALGN).
Mark-to-market (MTM)
Revaluing open positions at current market price (× FX), snapshotting cost, market value, and unrealized gain per position for an as-of date (SP_MTM_RUN).
Corporate action / split
An issuer event that adjusts holdings. A stock split rewrites every open lot's quantity and cost by the split ratio (SP_PROCESS_SPLIT).
Sub-account roll-up
Summing a holding across a master account and all its descendant sub-accounts, computed by procedural recursion (SP_ROLLUP_ACCT) and by a WITH RECURSIVE CTE (FN_SUBTREE_QTY).
Position (POSITION)
The denormalized current quantity and weighted-average cost per (account, security), kept in sync from open lots by a MERGE (SP_UPSERT_POSITION).
General ledger invariant
Every trade posts a balanced set of GL entries so SUM(GLENTRY.AMT)=0 across the run and per document — the master reconciliation control.
Idempotency token
A unique (SRCDOC, SRCSEQ) key on CASHLEDG/GLENTRY preventing a trade's legs from being double-posted.
SAVEPOINT / ROLLBACK TO SAVEPOINT
A nested transaction marker; rolling back to it undoes work since the marker without aborting the whole unit — scopes a failed sell or batch trade to its own effects.
Dynamic result set
Rows a procedure returns to its caller through a WITH RETURN cursor (DYNAMIC RESULT SETS n), distinct from OUT parameters.
GENERATED column
A column whose value the engine always derives from an expression (here REALIZEDPL.NETGAIN), never written by the application.
STRSQL
The IBM i interactive SQL session — SEC/i's operator surface, since it has no 5250 screen.
SQLSTATE / SQLCODE / return code (RC)
SQL status signals. SEC/i signals application SQLSTATEs 75020/75021 and additionally reports handled business outcomes through SP_BOOK_TRADE's P_RC.