Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b9aea35
Tier1 build flags (#362)
yellowbean Oct 23, 2026
8090182
perf: avoid Rational round-trips in money math (#357 Tier 3)
yellowbean Oct 24, 2026
185c679
perf: use DList for period cashflow accumulation (#357 Tier 2)
yellowbean Oct 24, 2026
fc4f26b
perf: use IntSet for prorata residual lookup (#357 Tier 2)
yellowbean Oct 24, 2026
28ec30e
perf: single-pass getValByDates for sorted curves (#357 Tier 2)
yellowbean Oct 24, 2026
53267b8
style: remove duplicate exports in Asset/InterestRate (#356)
yellowbean Oct 24, 2026
69d982a
style: remove dead debug bindings and Debug.Trace imports (#356, item 2)
yellowbean Oct 24, 2026
be7302b
chore: purge dead internal code, stale hiding clauses, fix $-splice c…
yellowbean Oct 24, 2026
4761c4a
ci: publish stripped and unstripped release binaries
yellowbean Oct 24, 2026
8b1f7ac
ci: publish UPX-packed Linux and Windows variants
yellowbean Oct 24, 2026
fa0520f
fix(analytics): hold the last yield flat outside the pricing-curve ra…
yellowbean Oct 25, 2026
dfbca33
fix(analytics): require the pricing curve to cover the first cashflow…
yellowbean Oct 25, 2026
749f862
feat(analytics): one discount curve for PV, duration and convexity (#…
yellowbean Oct 25, 2026
452487d
fix(analytics): pv4 returns a typed Left for unsupported curve types …
yellowbean Oct 25, 2026
b1fe42b
feat: interest-index accessors and MultiIntLiable split
yellowbean Oct 25, 2026
41e9886
chore: purge dead internal code and stale import/export debt (#365)
yellowbean Oct 25, 2026
70b7ac4
chore: drop unused imports (#365, behavior-neutral)
yellowbean Oct 25, 2026
6fa52a0
chore: replace indentation tabs with spaces (#365, behavior-neutral)
yellowbean Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 128 additions & 1 deletion .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ name: Docker Image CI
# (e.g. `v0.52.12`), which:
# * builds native binaries for linux/amd64, linux/arm64, macOS/arm64 and
# Windows/amd64 and publishes them automatically to the GitHub "Releases"
# page for that tag (the release is created non-draft, with generated notes)
# page for that tag (the release is created non-draft, with generated notes);
# every target is also published stripped, and Linux/Windows additionally
# as a UPX-packed binary
# * builds and pushes the amd64 image to Docker Hub (:latest + semver tags)
# * publishes the tagged version to Hackage (the tag must match the cabal
# version, e.g. `v0.53.0` <-> `version: 0.53.0`)
Expand Down Expand Up @@ -90,6 +92,131 @@ jobs:
fi
mv "dist/Hastructure-exe${EXT}" "dist/Hastructure-${GITHUB_REF_NAME}-${{ matrix.name }}${EXT}"

# Publish three variants of every Linux/Windows target (two on macOS),
# all from the exact same build:
# Hastructure-<tag>-<target><ext> normal, full symbol table
# Hastructure-<tag>-<target>-stripped<ext> same code/data, no symbols
# Hastructure-<tag>-<target>-upx<ext> stripped + UPX (-9)
# Stripping here (instead of a second `cabal install`) guarantees the
# binaries differ only in the symbol/debug metadata, and avoids doubling
# the build time.
- name: Create stripped variant
id: variants
shell: bash
run: |
set -euo pipefail
EXT=""
if [ "$RUNNER_OS" = "Windows" ]; then EXT=".exe"; fi
base="dist/Hastructure-${GITHUB_REF_NAME}-${{ matrix.name }}"
full="${base}${EXT}"
slim="${base}-stripped${EXT}"
cp "$full" "$slim"
chmod +x "$slim"

if [ "$RUNNER_OS" = "macOS" ]; then
# Apple's strip does not know --strip-unneeded. Rewriting the Mach-O
# invalidates the linker's ad-hoc arm64 signature, so re-sign or the
# binary is killed at exec time.
/usr/bin/strip -x "$slim"
codesign --force --sign - "$slim"
codesign --verify --strict "$slim"
elif [ "$RUNNER_OS" = "Windows" ]; then
ghc_lib="$(ghc --print-libdir 2>/dev/null | sed 's|\\|/|g' || true)"
candidates=()
if [ -n "$ghc_lib" ]; then
candidates+=("$(dirname "$ghc_lib")/mingw/bin/strip.exe")
fi
candidates+=("C:/msys64/usr/bin/strip.exe" "C:/Strawberry/c/bin/strip.exe")
strip_bin=""
for c in "${candidates[@]}"; do
if [ -x "$c" ]; then strip_bin="$c"; break; fi
done
if [ -z "$strip_bin" ]; then strip_bin="$(command -v strip || true)"; fi
if [ -z "$strip_bin" ]; then
echo "No strip executable found (tried GHC mingw, MSYS2, Strawberry, PATH)" >&2
exit 1
fi
echo "Using $strip_bin"
"$strip_bin" --strip-unneeded "$slim"
else
strip --strip-unneeded "$slim"
fi

size_full=$(wc -c <"$full")
size_slim=$(wc -c <"$slim")
echo "unstripped: $size_full bytes, stripped: $size_slim bytes"
test "$size_slim" -lt "$size_full"
echo "stripped=$slim" >> "$GITHUB_OUTPUT"

# Third variant for Linux and Windows only: strip first, then let UPX
# pack a *copy* (the action compresses in place). macOS is excluded
# because UPX/ghaction-upx does not support Mach-O.
- name: Stage UPX variant
id: upx
if: runner.os != 'macOS'
shell: bash
run: |
set -euo pipefail
EXT=""
if [ "$RUNNER_OS" = "Windows" ]; then EXT=".exe"; fi
path="dist/Hastructure-${GITHUB_REF_NAME}-${{ matrix.name }}-upx${EXT}"
cp "${{ steps.variants.outputs.stripped }}" "$path"
chmod +x "$path"
echo "path=$path" >> "$GITHUB_OUTPUT"

- name: Compress UPX variant
if: runner.os != 'macOS'
uses: crazy-max/ghaction-upx@v4
with:
version: v5.2.1
files: ${{ steps.upx.outputs.path }}
args: -9

- name: Verify UPX variant
if: runner.os != 'macOS'
shell: bash
run: |
set -euo pipefail
size_stripped=$(wc -c <"${{ steps.variants.outputs.stripped }}")
size_upx=$(wc -c <"${{ steps.upx.outputs.path }}")
echo "stripped: $size_stripped bytes, upx: $size_upx bytes"
test "$size_upx" -lt "$size_stripped"

# The stripped and UPX assets are the ones at risk (symbols removed,
# macOS re-signed, UPX-packed), so start each and require a real HTTP
# answer before anything is published. Runs in `smoke/`, not `dist/`, to
# keep config.yml/swagger.json out of the release assets.
- name: Smoke test stripped and UPX variants
shell: bash
run: |
set -euo pipefail
EXT=""
if [ "$RUNNER_OS" = "Windows" ]; then EXT=".exe"; fi
variants=(stripped)
if [ "$RUNNER_OS" != "macOS" ]; then variants+=(upx); fi
mkdir -p smoke
cd smoke
port=18080
for variant in "${variants[@]}"; do
bin="Hastructure-${GITHUB_REF_NAME}-${{ matrix.name }}-${variant}${EXT}"
cp "../dist/$bin" .
printf 'port: %s\n' "$port" > config.yml
"./$bin" > "$variant.log" 2>&1 &
pid=$!
ok=0
for _ in $(seq 1 60); do
if curl -fsS "http://127.0.0.1:${port}/version" 2>/dev/null; then ok=1; break; fi
sleep 1
done
kill "$pid" 2>/dev/null || true
if [ "$ok" -ne 1 ]; then
echo "$variant binary failed to serve /version" >&2
cat "$variant.log" >&2
exit 1
fi
port=$((port + 1))
done

- name: Upload artifact
uses: actions/upload-artifact@v7
with:
Expand Down
6 changes: 3 additions & 3 deletions Hastructure.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ library
Paths_Hastructure
hs-source-dirs:
src
ghc-options: -Werror=missing-fields
ghc-options: -O2 -Werror=missing-fields -Werror=incomplete-record-updates
build-depends:
Decimal >= 0.5.2 && < 0.6,
base >= 4.18.0 && < 4.22,
Expand Down Expand Up @@ -118,7 +118,7 @@ executable Hastructure-exe
Paths_Hastructure
hs-source-dirs:
app
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Werror=missing-fields -Wincomplete-patterns -Werror=incomplete-patterns
ghc-options: -O2 -threaded -rtsopts -with-rtsopts=-N -Werror=missing-fields -Wincomplete-patterns -Werror=incomplete-patterns -Werror=incomplete-record-updates
build-depends:
Hastructure,
base,
Expand Down Expand Up @@ -180,7 +180,7 @@ test-suite Hastructure-test
Paths_Hastructure
hs-source-dirs:
test
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Werror=missing-fields
ghc-options: -O2 -threaded -rtsopts -with-rtsopts=-N -Werror=missing-fields
build-depends:
Hastructure,
base,
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
# I'm using language XXX

* :snake: [Python wrapper](https://github.com/yellowbean/PyABS) is in ``Beta`` now !
* :coffee: Easy integration with ``Java/C#/C++/JavaScript/Python`` with ``RESTful`` interface and Docker image are ready.
* C/Java : [here](https://github.com/absbox/Hastructure/issues/106)
* :coffee: _Protobuf in Progress_

### Installation

Expand Down Expand Up @@ -72,11 +71,17 @@ nix-build release.nix -A hastructure # build the engine -> result/bin/Hastruc

#### From release executables

Prebuilt executables for Linux/macOS/Windows are attached to [GitHub releases](https://github.com/absbox/Hastructure/releases) as `Hastructure-<version>-<OS>-ghc-<ghc-version>`. Download one, then:
Prebuilt executables for Linux/macOS/Windows are attached to [GitHub releases](https://github.com/absbox/Hastructure/releases) as `Hastructure-<tag>-<target>`, where `<target>` is one of `linux-amd64`, `linux-arm64`, `macos-arm64` and `windows-amd64`. Every target is published in two or three variants:

* `Hastructure-<tag>-<target>` — normal build, full symbol table (useful for debugging/profiling).
* `Hastructure-<tag>-<target>-stripped` — same binary with the symbol table removed (and, on macOS, re-signed). Much smaller and functionally identical; use this one to run the engine.
* `Hastructure-<tag>-<target>-upx` — Linux and Windows only: the stripped binary packed with UPX at level `-9`. Smallest download; it decompresses itself in memory at startup. UPX does not support macOS.

Download one, then:

```bash
chmod +x Hastructure-<version>-Linux-ghc-9.10.2
./Hastructure-<version>-Linux-ghc-9.10.2
chmod +x Hastructure-v0.52.14-linux-amd64-stripped
./Hastructure-v0.52.14-linux-amd64-stripped
```

Run it from a directory containing `config.yml`: the engine reads the file at startup and refuses to start when it is missing, malformed or contains an invalid value.
Expand Down
2 changes: 0 additions & 2 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ import Numeric.RootFinding
import qualified Data.Version as DV
import qualified Paths_Hastructure as Paths

import Debug.Trace
debug = flip Debug.Trace.trace

version1 :: Version
version1 = Version (DV.showVersion Paths.version)
Expand Down
26 changes: 14 additions & 12 deletions src/Accounts.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,25 @@ module Accounts (Account(..),ReserveAmount(..),draw,deposit
,transfer,depositInt ,InterestInfo(..),buildEarnIntAction
,accBalLens,buildRateResetDates,accrueInt,accTypeLens)
where
import qualified Data.Time as T
import Stmt (Statement(..),appendStmt,getTxnBegBalance,getDate
,TxnComment(..),QueryByComment(..),getTxnComment,getTxnAmt,weightAvgBalanceByDates)
import qualified Data.Time as T ()
import Stmt (Statement(..),appendStmt,getTxnBegBalance,QueryByComment(..),getTxnComment)
import Types
import Lib
import Lib ()
import Util
import DateUtil
import Interface
import Data.Aeson hiding (json)
import Language.Haskell.TH
import Data.Aeson
import Language.Haskell.TH ()
import Data.Aeson.TH
import Data.Aeson.Types
import Data.Aeson.Types ()
import GHC.Generics
import Control.Lens.Tuple
import Control.Lens.Tuple ()
import Control.Lens hiding (Index)
import qualified InterestRate as IR
import qualified Data.DList as DL

-- import Web.Hyperbole

import Debug.Trace
debug = flip trace

data InterestInfo
-- | fix reinvest return rate
Expand Down Expand Up @@ -139,8 +136,7 @@ buildRateResetDates ed Account{accName = n, accInterest = Just (InvestmentAccoun
buildRateResetDates _ _ = Nothing


makeLensesFor [("accBalance","accBalLens") ,("accName","accNameLens")
,("accType","accTypeLens") ,("accStmt","accStmtLens"),("accInterest","accIntLens")] ''Account
makeLensesFor [("accBalance","accBalLens") ,("accType","accTypeLens")] ''Account


instance IR.UseRate Account where
Expand All @@ -149,6 +145,12 @@ instance IR.UseRate Account where

getIndex (Account _ an (Just (InvestmentAccount idx _ _ _ _ _)) _ _) = Just idx
getIndex _ = Nothing

getIndexes (Account _ an (Just (InvestmentAccount idx _ _ _ _ _)) _ _) = Just [idx]
getIndexes _ = Nothing

getResetDates _ = [] -- ^ re-investment interest does not reset a contractual index
getSpread _ = Nothing


makePrisms ''InterestInfo
Expand Down
Loading
Loading