60: Add initial AssetAZ Ticker service
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-05
|
||||
@@ -0,0 +1,31 @@
|
||||
## Context
|
||||
|
||||
See `proposal.md` for motivation. `TickerService` and `TickerServiceImpl` already exist as empty shells in AssetAZ-owned packages. `AssetPrice` wraps `ΩPriceΩ` and `TradingPair`; the latter identifies currencies structurally rather than by raw symbol. The repository temporarily hosts AssetAZ code as Cauldron, and NenjimHub's current autorun list directly constructs services.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** Establish a small reusable ticker contract, deterministic file-backed startup, atomic persist-before-publish behavior, and temporary autorun wiring while retaining AssetAZ ownership.
|
||||
|
||||
**Non-Goals:** Datasource plugins, real market data, pairs other than `EVE_USDC`, consumers, subscriptions/callbacks, history queries, retention, compaction, or automated tests.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Define `PriceObservation` in `com.r35157.assetaz.core.service.ticker` exactly as a record of `AssetPrice price` and `Instant observedAt`. Extend `TickerService` with `start()` and `PriceObservation getLatestPrice(TradingPair tradingPair)`. Use clear exceptions for unsupported/inactive pairs and enabled pairs without a persisted observation; `Optional` and `null` were rejected because issue #60 explicitly requires exceptions for unavailable prices. Raw symbol strings were rejected because callers must use the established ValueTypes.
|
||||
- Add stable EVE and USDC currency definitions and an `EVE_USDC` well-known `TradingPair`, then construct the sample price with `new ΩPriceΩ("14.85")`. This keeps identity consistent with other typed prices instead of parsing pair names at the API boundary.
|
||||
- Keep the public API and `PriceObservation` in the AssetAZ API package and implementation details in `com.r35157.assetaz.core.service.ticker.impl.ref`. Their current repository location is temporary Cauldron placement and does not imply Evelyn or NenjimHub ownership.
|
||||
- Use the fixed path `data/assetaz/ticker/EVE_USDC.prices`; do not create its directory or file. Check existence when initializing the pair. Missing means inactive, emits a warning containing the pair and expected path, and does not prevent the rest of Ticker startup; an existing zero-length, blank-only, or comment-only file is active with no latest value.
|
||||
- Store one observation per data line as `<uuuuMMddHHmmssSSS'Z'>:<decimal-price>`, using a strict formatter fixed to UTC. Read line by line, remove text from the first `#`, trim, and skip empty results. Split the remaining data at the required colon and parse it into `Instant`, `ΩPriceΩ`, and the fixed `EVE_USDC` pair. Wrap any structural, timestamp, or price failure with the filename, one-based physical line number, and identifying invalid content.
|
||||
- Load history synchronously during `start()` and select the parsed observation with the greatest `observedAt` timestamp rather than assuming the file is chronologically ordered. After loading, generate the first hardcoded observation immediately, then use a single-thread scheduled executor with fixed-delay scheduling: each subsequent attempt begins one minute after the preceding attempt completes. Fixed-rate scheduling was rejected because a long pause can cause rapid catch-up attempts. Do not schedule generation for an inactive pair. The scheduler is an implementation mechanism, not a public lifecycle or subscription API.
|
||||
- Serialize generation and publication, and keep the published latest value behind a thread-safe reference. Validate and encode every generated observation, append all encoded bytes through a `FileChannel`, and call `force(true)`. Only after successful persistence, compare its `observedAt` with the current latest and replace the in-memory reference only when the new timestamp is later. Thus concurrent callers see the successfully persisted observation with the greatest timestamp across loaded history and newly persisted observations, and a manually inserted future-dated observation remains latest until a later observation is persisted. A buffered-writer `flush()` alone was rejected because it does not satisfy the issue's explicit flush/force-to-persistence ordering. On open, write, or force failure, log/report the failure and retain the prior latest value; never publish the attempted observation.
|
||||
- Add a temporary `startAssetAZTickerService()` entry to `NenjimHubImpl` following its direct autorun pattern. It constructs and starts `TickerServiceImpl`; no Ticker dependency is passed to Evelyn, EMC, alarms, portfolios, or other services.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Filesystem or device guarantees can still limit crash durability after `FileChannel.force(true)`] → Force each complete appended record before publication and document that the implementation uses the strongest standard Java file-channel guarantee available here.
|
||||
- [An observation attempt can take arbitrarily long] → Fixed-delay scheduling waits until the attempt completes and then waits one minute, preventing overlapping or catch-up attempts.
|
||||
- [Malformed history prevents the service from starting] → Include exact filename and line number so operators can repair the activation file safely.
|
||||
- [Hardcoded identifiers and price are temporary] → Isolate them behind well-known ValueTypes and the reference implementation for later source replacement.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Operators opt in by creating `data/assetaz/ticker/EVE_USDC.prices`, optionally pre-populated in the documented format. Deploy the service and temporary autorun wiring together. Rollback removes the autorun call and ticker implementation while leaving the append-only data file intact.
|
||||
@@ -0,0 +1,29 @@
|
||||
## Why
|
||||
|
||||
AssetAZ has only empty ticker service shells, so callers cannot obtain a typed latest price or retain observations across restarts. A minimal reusable ticker establishes that service contract and a durable reference implementation before real market sources are introduced.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Define an AssetAZ ticker API that exposes the latest persisted observation by `TradingPair` using existing price ValueTypes and throws clear exceptions when no price is available.
|
||||
- Add a reference implementation for the single supported `EVE_USDC` pair, producing the hardcoded price `14.85` immediately at startup and starting each subsequent attempt one minute after the previous attempt completes.
|
||||
- Make an existing `.prices` file the explicit activation mechanism, load its valid history, append every new observation durably, and expose the successfully persisted observation with the greatest timestamp as latest.
|
||||
- Start the ticker through NenjimHub's current temporary autorun mechanism.
|
||||
- Leave the pair unavailable when its history file is missing, reject malformed history at startup, and retain the prior published observation when a new observation cannot be persisted safely.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `assetaz-ticker-service`: Typed latest-price access, file-backed activation and history loading, and durable periodic publication for the initial AssetAZ trading pair.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
None.
|
||||
|
||||
## Impact
|
||||
|
||||
- Extends the existing empty API and reference implementation under `com.r35157.assetaz.core.service.ticker` and adds `PriceObservation` there.
|
||||
- Reuses `AssetPrice`, `TradingPair`, and `ΩPriceΩ`; no datasource-plugin or consumer integration is introduced.
|
||||
- Adds temporary startup wiring to `NenjimHubImpl`, without transferring ownership of the ticker to NenjimHub.
|
||||
- Uses `data/assetaz/ticker/EVE_USDC.prices` as operator-controlled persistent activation and history.
|
||||
- Adds no automated tests.
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
## Purpose
|
||||
|
||||
Provide AssetAZ callers with typed latest prices backed by explicitly enabled, durable observation histories.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Ticker exposes typed latest prices
|
||||
The ticker SHALL expose the latest successfully persisted `PriceObservation` for a requested `TradingPair`. A `PriceObservation` SHALL contain exactly an `AssetPrice` and its observation `Instant`; the `AssetPrice` SHALL contain both its `ΩPriceΩ` value and `TradingPair`. The ticker SHALL initially support only `EVE_USDC`. A request for an unsupported or inactive pair, or for an enabled pair that has no persisted observation, SHALL throw a clear exception rather than return `null`.
|
||||
|
||||
#### Scenario: Latest supported price is available
|
||||
- **WHEN** a caller requests `EVE_USDC` after at least one observation has been loaded or published
|
||||
- **THEN** the ticker returns the latest observation with an `AssetPrice` identified by the `EVE_USDC` `TradingPair`
|
||||
|
||||
#### Scenario: Unsupported or inactive pair is requested
|
||||
- **WHEN** a caller requests a pair that is unsupported or not activated
|
||||
- **THEN** the ticker throws an exception that clearly identifies the unavailable trading pair
|
||||
|
||||
#### Scenario: Enabled pair has no persisted observation
|
||||
- **WHEN** a caller requests an enabled pair before any observation has been successfully persisted
|
||||
- **THEN** the ticker throws an exception that clearly states that no persisted price is available for that trading pair
|
||||
|
||||
### Requirement: Price history explicitly activates the pair
|
||||
The ticker SHALL treat the existence of `data/assetaz/ticker/EVE_USDC.prices` when the pair is initialized as activation of `EVE_USDC`. It SHALL load every valid observation and select the observation with the greatest timestamp as latest, regardless of file order. It SHALL accept empty lines, full-line comments whose first non-whitespace character is `#`, and inline comments beginning at the first `#` on a data line. A missing file SHALL produce a warning containing the trading pair and expected filepath, leave the pair unavailable, and SHALL NOT be created automatically; an existing file with no data observations SHALL enable the pair without an initial latest price.
|
||||
|
||||
#### Scenario: Existing history activates and restores the pair
|
||||
- **WHEN** the ticker starts with an existing history containing valid observations and permitted whitespace or comments
|
||||
- **THEN** `EVE_USDC` is available and the observation with the greatest timestamp is its latest persisted price
|
||||
|
||||
#### Scenario: Empty history activates without a price
|
||||
- **WHEN** the ticker starts with an existing empty history file
|
||||
- **THEN** `EVE_USDC` is enabled with no initial latest observation
|
||||
|
||||
#### Scenario: Missing history leaves the pair unavailable
|
||||
- **WHEN** the ticker starts without the `EVE_USDC` history file
|
||||
- **THEN** it logs a warning containing `EVE_USDC` and `data/assetaz/ticker/EVE_USDC.prices`, continues starting without that pair, and does not create the file or its parent directories
|
||||
|
||||
#### Scenario: Malformed history is rejected clearly
|
||||
- **WHEN** comment text is removed and a non-empty data line does not contain a valid UTC timestamp and price
|
||||
- **THEN** Ticker startup fails with an error containing the history filename, the one-based physical line number, and enough of the invalid content to identify it
|
||||
|
||||
### Requirement: History uses the human-editable observation format
|
||||
Each data line SHALL have the form `<UTC timestamp>:<price>`, where the timestamp uses `uuuuMMddHHmmssSSS'Z'` with millisecond precision in UTC. Automatic writes SHALL append plain data lines only and SHALL leave all existing comments and blank lines untouched.
|
||||
|
||||
#### Scenario: Observation is written in the required format
|
||||
- **WHEN** an observation at `2026-08-05T13:15:42.783Z` with price `14.85` is persisted
|
||||
- **THEN** the appended line is `20260805131542783Z:14.85`
|
||||
|
||||
#### Scenario: Existing operator annotations are preserved
|
||||
- **WHEN** the ticker appends an observation to a history containing comments or blank lines
|
||||
- **THEN** those existing lines remain unchanged and the appended observation contains neither a comment nor extra annotation
|
||||
|
||||
### Requirement: New observations are durable before publication
|
||||
For enabled `EVE_USDC`, the ticker SHALL generate price `14.85` immediately at startup. After that startup attempt completes, and after each subsequent attempt completes, the ticker SHALL wait one minute before beginning the next attempt. It SHALL NOT use fixed-rate scheduling or perform catch-up attempts after a delay. For each generated observation it SHALL validate the observation, append it to history, and flush or force the data to persistent storage; every generated observation SHALL be persisted unless that persistence attempt fails. Only after persistence succeeds SHALL the ticker replace the in-memory latest observation, and then only if the newly persisted observation's `observedAt` timestamp is later than the current latest timestamp. If persistence fails, it SHALL report the failure clearly, SHALL NOT publish the attempted observation, and SHALL preserve the previously persisted latest observation. Concurrent reads SHALL never observe an unpersisted price, and latest SHALL always be the successfully persisted observation with the greatest `observedAt` timestamp across loaded history and newly persisted observations.
|
||||
|
||||
#### Scenario: Startup observation is persisted
|
||||
- **WHEN** the ticker starts with an enabled writable history
|
||||
- **THEN** it appends and forces a `14.85` observation to persistent storage before considering it for latest
|
||||
|
||||
#### Scenario: Periodic observation is persisted
|
||||
- **WHEN** one minute has elapsed after the preceding observation attempt completed while the enabled ticker is running
|
||||
- **THEN** it begins another attempt and appends and forces its `14.85` observation to persistent storage before considering it for latest
|
||||
|
||||
#### Scenario: Future-dated history remains latest
|
||||
- **WHEN** history contains a successfully persisted observation whose timestamp is later than a newly generated observation
|
||||
- **THEN** the newly generated observation is still persisted and the future-dated history observation remains latest
|
||||
|
||||
#### Scenario: Persistence fails
|
||||
- **WHEN** appending or flushing a new observation fails
|
||||
- **THEN** the failure is reported clearly, the failed observation is not published to any reader, and the previously persisted latest observation remains available
|
||||
@@ -0,0 +1,21 @@
|
||||
## 1. AssetAZ Ticker API
|
||||
|
||||
- [x] 1.1 Add stable EVE and USDC currency ValueTypes and the well-known `EVE_USDC` `TradingPair`.
|
||||
- [x] 1.2 Add immutable `PriceObservation` with a timestamp and `AssetPrice` in the ticker API package.
|
||||
- [x] 1.3 Extend `TickerService` with `start()` and `PriceObservation getLatestPrice(TradingPair)`, using clear exceptions for unsupported/inactive pairs and enabled pairs without a persisted observation.
|
||||
|
||||
## 2. File-Backed Reference Implementation
|
||||
|
||||
- [x] 2.1 Implement explicit activation from the existing `data/assetaz/ticker/EVE_USDC.prices` file without creating a missing file or directory.
|
||||
- [x] 2.2 Implement the strict UTC `uuuuMMddHHmmssSSS'Z':<price>` format and line-by-line history parsing with empty-line and full-line/inline comment support.
|
||||
- [x] 2.3 Report malformed data with filename, one-based physical line number, and identifying invalid content, and restore the valid observation with the greatest `observedAt` timestamp as latest.
|
||||
- [x] 2.4 Implement thread-safe append-and-`FileChannel.force(true)` behavior that persists every generated observation, never exposes an unpersisted observation, preserves the prior latest observation on persistence failure, and replaces latest after successful persistence only when the new `observedAt` timestamp is later.
|
||||
- [x] 2.5 Generate the hardcoded `14.85` startup observation immediately and use fixed-delay scheduling only for the enabled pair so each subsequent attempt begins one minute after the preceding attempt completes.
|
||||
|
||||
## 3. Temporary Autorun Wiring
|
||||
|
||||
- [x] 3.1 Start `TickerServiceImpl` from NenjimHub's current direct autorun mechanism without wiring it to any consumer.
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 Compile the project and manually verify missing, empty/comment-only, out-of-order valid, future-dated, malformed, and unwritable history-file behavior, required exception cases, timestamp/price formatting, fixed-delay timing without catch-up attempts, persist-before-publish ordering, and greatest-`observedAt` latest selection without adding automated tests.
|
||||
@@ -0,0 +1,71 @@
|
||||
# assetaz-ticker-service Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide AssetAZ callers with typed latest prices backed by explicitly enabled, durable observation histories.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Ticker exposes typed latest prices
|
||||
The ticker SHALL expose the latest successfully persisted `PriceObservation` for a requested `TradingPair`. A `PriceObservation` SHALL contain exactly an `AssetPrice` and its observation `Instant`; the `AssetPrice` SHALL contain both its `ΩPriceΩ` value and `TradingPair`. The ticker SHALL initially support only `EVE_USDC`. A request for an unsupported or inactive pair, or for an enabled pair that has no persisted observation, SHALL throw a clear exception rather than return `null`.
|
||||
|
||||
#### Scenario: Latest supported price is available
|
||||
- **WHEN** a caller requests `EVE_USDC` after at least one observation has been loaded or published
|
||||
- **THEN** the ticker returns the latest observation with an `AssetPrice` identified by the `EVE_USDC` `TradingPair`
|
||||
|
||||
#### Scenario: Unsupported or inactive pair is requested
|
||||
- **WHEN** a caller requests a pair that is unsupported or not activated
|
||||
- **THEN** the ticker throws an exception that clearly identifies the unavailable trading pair
|
||||
|
||||
#### Scenario: Enabled pair has no persisted observation
|
||||
- **WHEN** a caller requests an enabled pair before any observation has been successfully persisted
|
||||
- **THEN** the ticker throws an exception that clearly states that no persisted price is available for that trading pair
|
||||
|
||||
### Requirement: Price history explicitly activates the pair
|
||||
The ticker SHALL treat the existence of `data/assetaz/ticker/EVE_USDC.prices` when the pair is initialized as activation of `EVE_USDC`. It SHALL load every valid observation and select the observation with the greatest timestamp as latest, regardless of file order. It SHALL accept empty lines, full-line comments whose first non-whitespace character is `#`, and inline comments beginning at the first `#` on a data line. A missing file SHALL produce a warning containing the trading pair and expected filepath, leave the pair unavailable, and SHALL NOT be created automatically; an existing file with no data observations SHALL enable the pair without an initial latest price.
|
||||
|
||||
#### Scenario: Existing history activates and restores the pair
|
||||
- **WHEN** the ticker starts with an existing history containing valid observations and permitted whitespace or comments
|
||||
- **THEN** `EVE_USDC` is available and the observation with the greatest timestamp is its latest persisted price
|
||||
|
||||
#### Scenario: Empty history activates without a price
|
||||
- **WHEN** the ticker starts with an existing empty history file
|
||||
- **THEN** `EVE_USDC` is enabled with no initial latest observation
|
||||
|
||||
#### Scenario: Missing history leaves the pair unavailable
|
||||
- **WHEN** the ticker starts without the `EVE_USDC` history file
|
||||
- **THEN** it logs a warning containing `EVE_USDC` and `data/assetaz/ticker/EVE_USDC.prices`, continues starting without that pair, and does not create the file or its parent directories
|
||||
|
||||
#### Scenario: Malformed history is rejected clearly
|
||||
- **WHEN** comment text is removed and a non-empty data line does not contain a valid UTC timestamp and price
|
||||
- **THEN** Ticker startup fails with an error containing the history filename, the one-based physical line number, and enough of the invalid content to identify it
|
||||
|
||||
### Requirement: History uses the human-editable observation format
|
||||
Each data line SHALL have the form `<UTC timestamp>:<price>`, where the timestamp uses `uuuuMMddHHmmssSSS'Z'` with millisecond precision in UTC. Automatic writes SHALL append plain data lines only and SHALL leave all existing comments and blank lines untouched.
|
||||
|
||||
#### Scenario: Observation is written in the required format
|
||||
- **WHEN** an observation at `2026-08-05T13:15:42.783Z` with price `14.85` is persisted
|
||||
- **THEN** the appended line is `20260805131542783Z:14.85`
|
||||
|
||||
#### Scenario: Existing operator annotations are preserved
|
||||
- **WHEN** the ticker appends an observation to a history containing comments or blank lines
|
||||
- **THEN** those existing lines remain unchanged and the appended observation contains neither a comment nor extra annotation
|
||||
|
||||
### Requirement: New observations are durable before publication
|
||||
For enabled `EVE_USDC`, the ticker SHALL generate price `14.85` immediately at startup. After that startup attempt completes, and after each subsequent attempt completes, the ticker SHALL wait one minute before beginning the next attempt. It SHALL NOT use fixed-rate scheduling or perform catch-up attempts after a delay. For each generated observation it SHALL validate the observation, append it to history, and flush or force the data to persistent storage; every generated observation SHALL be persisted unless that persistence attempt fails. Only after persistence succeeds SHALL the ticker replace the in-memory latest observation, and then only if the newly persisted observation's `observedAt` timestamp is later than the current latest timestamp. If persistence fails, it SHALL report the failure clearly, SHALL NOT publish the attempted observation, and SHALL preserve the previously persisted latest observation. Concurrent reads SHALL never observe an unpersisted price, and latest SHALL always be the successfully persisted observation with the greatest `observedAt` timestamp across loaded history and newly persisted observations.
|
||||
|
||||
#### Scenario: Startup observation is persisted
|
||||
- **WHEN** the ticker starts with an enabled writable history
|
||||
- **THEN** it appends and forces a `14.85` observation to persistent storage before considering it for latest
|
||||
|
||||
#### Scenario: Periodic observation is persisted
|
||||
- **WHEN** one minute has elapsed after the preceding observation attempt completed while the enabled ticker is running
|
||||
- **THEN** it begins another attempt and appends and forces its `14.85` observation to persistent storage before considering it for latest
|
||||
|
||||
#### Scenario: Future-dated history remains latest
|
||||
- **WHEN** history contains a successfully persisted observation whose timestamp is later than a newly generated observation
|
||||
- **THEN** the newly generated observation is still persisted and the future-dated history observation remains latest
|
||||
|
||||
#### Scenario: Persistence fails
|
||||
- **WHEN** appending or flushing a new observation fails
|
||||
- **THEN** the failure is reported clearly, the failed observation is not published to any reader, and the previously persisted latest observation remains available
|
||||
Reference in New Issue
Block a user