diff --git a/openspec/changes/adaptive-market-engine/.openspec.yaml b/openspec/changes/adaptive-market-engine/.openspec.yaml new file mode 100644 index 000000000..149631464 --- /dev/null +++ b/openspec/changes/adaptive-market-engine/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-17 diff --git a/openspec/changes/adaptive-market-engine/design.md b/openspec/changes/adaptive-market-engine/design.md new file mode 100644 index 000000000..4e779e116 --- /dev/null +++ b/openspec/changes/adaptive-market-engine/design.md @@ -0,0 +1,119 @@ +## Context + +EclipseTrader already has a feed seam (`StreamingConnector` / `FeedSubscription` with `setTrade`, `setBook`, `setQuote`, `setTodayOHL`) that the JESSX `BrokerConnector` currently populates by parsing XML messages. The new engine only needs to produce the same kinds of events; it does not need to reuse JESSX's server, sockets, or bots. + +## Goals / Non-Goals + +**Goals:** + +- A self-contained, in-process market engine with a full-depth matcher, an adaptive price process, and a simulated daily clock. +- Deterministic, replayable runs via a seeded random generator. +- Emit trade/depth/quote events into the existing feed seam so charts and Level2 work unchanged. + +**Non-Goals:** + +- FIX or other external protocol connectivity. +- Retiring or modifying the existing `org.eclipsetrader.jessx` bundle in this change. +- Game scoring, portfolio valuation, or player UI — those build on top of the engine and are tracked separately. + +## Decisions + +### 1. Clean-room reimplementation, not a port of webcurvesim + +Reimplement the matching and market-making ideas from webcurvesim from scratch under EPL. + +- **Rationale**: webcurvesim's license carries a commercial-registration clause and bundles LGPL 2.1, which is awkward for an EPL project; its code is Java 5/6 style (`Vector`, `Hashtable`, slf4j 1.5). The matching logic is well understood and small enough to rewrite cleanly with modern generics and immutable events. +- **Alternative considered**: vendor webcurvesim's `exchange`/`common` classes. Rejected for licensing and modernization reasons. + +### 2. In-process engine, no sockets + +Run the engine in the same JVM as EclipseTrader and deliver events via listener callbacks. + +- **Rationale**: the game is single-player in one process; JESSX's `NetworkCore`/XML-over-socket machinery exists only for its multi-machine lab origin and adds latency and complexity for no benefit. +- **Alternative considered**: keep a socket boundary between engine and UI. Rejected as unnecessary. + +### 3. New bundle `org.eclipsetrader.market.sim` + +Put the engine in its own OSGi bundle rather than modifying `org.eclipsetrader.jessx`. + +- **Rationale**: keeps JESSX intact as a fallback while the new engine proves out; isolates the new code with its own lifecycle and tests. +- **Alternative considered**: replace JESSX's `OrderMarket`/`Order` in place. Rejected — higher blast radius and loses the working JESSX path. + +### 4. Price model: fundamental value with emergent trend + +Maintain per-asset `Fₜ = Fₜ₋₁ + driftₜ + newsₜ + noiseₜ`, where `driftₜ` is estimated by agents from recent trades (momentum) rather than set as a fixed parameter, and `newsₜ` is a procedural shock proportional to sentiment × magnitude. + +- **Rationale**: the emergent drift makes "the market adapts to news and trend" a genuine mechanism rather than a scripted curve; the market maker anchors liquidity around `Fₜ`. +- **Alternative considered**: scripted per-scenario drift. Rejected — user wants emergent, not scripted. + +### 5. Agents: market maker + informed traders + +A market maker quotes `bid/ask = Fₜ ± spread/2` (spread widened by volatility/trading volume), and informed agents tilt quotes toward incoming news and the estimated trend. + +- **Rationale**: the market maker guarantees continuous trades (fixing the "few deals" problem); informed agents create the news/trend reaction the player observes. +- **Alternative considered**: reuse JESSX's `Discreet`/`NotDiscreet` bots on the new engine. Rejected — their spread-only quoting produces no trades. + +### 6. Simulated daily calendar + +Each period is a trading day on a simulated clock; trades are timestamped from that clock. The day's real-time length and the start date are engine parameters. + +- **Rationale**: gives charts meaningful day boundaries and removes the elapsed-vs-epoch ambiguity the JESSX path had (`fix-jessx-deal-timestamps`). +- **Alternative considered**: wall-clock time with fast-forward. Rejected — no clean day boundaries. + +### 7. Determinism + +All randomness (news timing, sentiment, magnitudes, agent noise) comes from a single seeded PRNG so a scenario can be replayed identically. + +- **Rationale**: essential for testing the engine and for reproducible game scenarios. +- **Alternative considered**: unseeded `Math.random()`. Rejected — not replayable. + +### 8. Feed integration + +Engine listeners push `Trade`/`Book`/`Quote`/`TodayOHL` into the existing `FeedSubscription` setter path (the same seam `BrokerConnector.objectReceived` uses), marshaling onto the SWT display thread for UI-safe delivery. + +- **Rationale**: no new feed plumbing; charts and Level2 behave as they do today. +- **Alternative considered**: a new `IFeedConnector`. Rejected — heavier, and the subscription seam already exists. + +### 9. Full order-type set with contingent (stop) orders + +Support LIMIT, MARKET, and the modern set: STOP, STOP-LIMIT, TRAILING-STOP, IOC, FOK, ICEBERG, and PEGGED. + +- **Contingent orders** (stop, stop-limit, trailing-stop) rest in a separate off-book trigger queue; a trigger monitor checks each trade/quote against every active trigger and submits the resulting market/limit order when crossed. Trailing stops update their trigger from the best observed price. +- **Time-in-force modifiers** (IOC, FOK) are handled by the matching loop: IOC cancels the unfilled remainder after the sweep; FOK verifies the full quantity is available before executing (no partial fill). +- **Iceberg** keeps a hidden total and a separate display quantity that replenishes as the visible portion fills. +- **Pegged** orders are re-priced on every book change relative to a reference (best bid/ask/mid) plus an offset. + +- **Rationale**: a trading game needs risk-management (stop/trailing) and execution-control (IOC/FOK) tools; iceberg and pegged add realistic depth without changing the core matcher. +- **Alternative considered**: limit + market only. Rejected — the game should expose the modern order types a real trading platform offers. + +### 10. Signed positions with short selling + +Model positions as a signed quantity per participant per asset (positive = long, negative = short). Buys open or increase long positions (and cover shorts first); sells reduce long positions (and open shorts beyond that) up to a configured per-participant borrow limit. Borrow availability is simplified to a numeric limit rather than a locate/borrow-cost model. + +- **Rationale**: short selling is a core expectation of a realistic trading game and pairs with the stop/trailing risk tools; a numeric limit keeps the engine simple. +- **Alternative considered**: forbid shorts (JESSX's current behavior, where asks are rejected without sufficient holdings). Rejected — the game should let players go short. + +### 11. Leverage via cash loans with margin enforcement + +Track each participant's cash balance as signed (negative = borrowed). Buying power = cash × a configurable leverage multiplier; positions require posting margin (a fraction of notional). Equity = cash + long market value − short market value. When equity drops below a maintenance-margin threshold, the engine force-liquidates positions (submits market orders to close) until equity is restored. Borrowed cash accrues interest per period. This composes with short selling: shorts borrow the asset (bounded by the borrow limit in `market-positions`), while leverage borrows cash (bounded by the margin/equity model). + +- **Rationale**: leverage is the "high risk" half of high-risk trading — the margin-call/liquidation loop is the payoff. A numeric equity/threshold model keeps it deterministic and testable. +- **Alternative considered**: unlimited leverage with no margin enforcement. Rejected — no risk, no game. + +## Risks / Trade-offs + +- [Risk] Forced liquidation feedback loop — liquidation sells move the price, which can trigger further margin calls. → Mitigation: evaluate and liquidate in a single pass per price update, and treat cascades as an intended, gameable dynamic rather than a bug. + +- [Risk] Momentum can run away into an unrealistic bubble/crash → Mitigation: cap drift magnitude and add a mean-reversion term toward a long-run anchor. +- [Risk] Engine thread and SWT UI thread synchronization (listener callbacks touching the display) → Mitigation: marshal all feed updates through `Display.asyncExec`, matching the existing connector pattern. +- [Risk] Overly aggressive informed agents flatten the spread and stop the market maker → Mitigation: size agent orders as fractions of displayed liquidity and let the market maker dominate quoting. +- [Risk] Determinism is broken by wall-clock dependencies (e.g., `System.currentTimeMillis()` in timestamps) → Mitigation: the simulated clock is the only time source inside the engine. + +## Migration Plan + +- Additive: a new bundle plus a small wiring point. JESSX remains selectable, so rollback is reverting the wiring change or not enabling the new engine. +- No data migration; the engine is stateless across restarts except for the seed and any scenario parameters. + +## Open Questions + +- Real-time duration of a simulated day (e.g., 60s vs 300s) — a tunable parameter, does not change specs or task breakdown. diff --git a/openspec/changes/adaptive-market-engine/proposal.md b/openspec/changes/adaptive-market-engine/proposal.md new file mode 100644 index 000000000..257a1130a --- /dev/null +++ b/openspec/changes/adaptive-market-engine/proposal.md @@ -0,0 +1,34 @@ +## Why + +EclipseTrader's bundled JESSX simulator produces a thin, unrealistic market: its matcher only ever matches a single best order, its bots place non-crossing orders inside the spread, and its news is a static scenario script. The result is very few trades, no price trend, and a market that never adapts to events — which defeats the purpose of a trading game in which the player adapts to a real, news-reactive market. + +## What Changes + +- Add a new OSGi bundle (`org.eclipsetrader.market.sim`) containing a clean-room, full-depth limit-order-book matching engine (inspired by webcurvesim's core, written under EPL). +- Add a procedural market layer: a fundamental-value process with an emergent trend regime, procedurally generated news (sentiment + magnitude), and adaptive agents (a market maker plus informed traders) that quote and trade continuously. +- Add a simulated daily market calendar: each period is a trading day; orders and trades carry simulated daily timestamps that flow into the existing EclipseTrader feed seam so charts show meaningful day boundaries. +- Support signed positions and short selling: participants may sell assets they do not hold up to a borrow limit, and buys cover existing shorts. +- Support leveraged trading via cash loans: buying power beyond available cash, margin requirements, and forced liquidation when equity falls below the maintenance margin. +- Integrate the engine with the existing feed pipeline (`StreamingConnector`/`FeedSubscription`) so trades, depth, and quotes render in charts and the Level2 view. +- Leave the existing JESSX bundle intact in this change. + +## Capabilities + +### New Capabilities + +- `market-matching`: the limit-order-book matching semantics — limit, market, stop, stop-limit, trailing-stop, IOC, FOK, iceberg, and pegged orders; price-time priority; full-depth execution; amend/cancel; VWAP; and price steps. +- `market-adaptation`: the adaptive market behavior — procedural news, the emergent trend regime, and the market-maker/informed agents that turn news and trend into price movement. +- `market-calendar`: the simulated daily calendar — daily periods, simulated timestamps, and how they map onto the feed. +- `market-positions`: position tracking and short selling — signed positions, short limits, and buy-to-cover semantics. +- `market-leverage`: leveraged trading — cash loans, buying power, margin requirements, and forced liquidation. + +### Modified Capabilities + +None. + +## Impact + +- **Code**: new bundle `org.eclipsetrader.market.sim` (matching engine, agents, news generator, calendar). Wiring into the existing feed seam that `StreamingConnector`/`FeedSubscription` already expose. +- **Behavior**: charts and Level2 show continuous, trending, news-reactive simulated market data with daily timestamps; player orders interact with the same book. +- **Dependencies**: none new (plain Java plus existing EclipseTrader feed APIs). +- **Related work**: supersedes the sparse-trade behavior of JESSX and the `fix-jessx-deal-timestamps` elapsed/epoch workaround (the new engine controls its own clock). diff --git a/openspec/changes/adaptive-market-engine/specs/market-adaptation/spec.md b/openspec/changes/adaptive-market-engine/specs/market-adaptation/spec.md new file mode 100644 index 000000000..69843bdc2 --- /dev/null +++ b/openspec/changes/adaptive-market-engine/specs/market-adaptation/spec.md @@ -0,0 +1,69 @@ +## Purpose + +Defines the adaptive market behavior: how a fundamental value, procedurally generated news, an emergent trend, and trading agents combine so the simulated market produces continuous, news-reactive, trending prices. + +## ADDED Requirements + +### Requirement: Fundamental value drives quoting + +The market SHALL maintain a fundamental value for each asset, and quoted prices SHALL be derived from that value rather than from disconnected random levels. + +#### Scenario: Quotes track the fundamental value + +- **WHEN** the fundamental value of an asset changes +- **THEN** subsequent quotes and trades move toward the new value + +### Requirement: Procedurally generated news + +News events SHALL be generated procedurally during the simulation, each carrying an asset, a sentiment (positive or negative), and a magnitude. + +#### Scenario: News event is generated + +- **WHEN** the simulation is running +- **THEN** news events appear over time with an associated asset, sentiment, and magnitude + +### Requirement: News reprices the market + +A news event SHALL shift the fundamental value of its asset by an amount proportional to its sentiment and magnitude, so the market visibly reacts. + +#### Scenario: Positive news moves the price up + +- **WHEN** a positive news event for an asset is generated +- **THEN** the asset's fundamental value increases and its traded price trends upward + +#### Scenario: Negative news moves the price down + +- **WHEN** a negative news event for an asset is generated +- **THEN** the asset's fundamental value decreases and its traded price trends downward + +### Requirement: Emergent trend + +The market SHALL exhibit a price trend that emerges from agent behavior (momentum and news reaction) rather than being a constant offset, and that trend SHALL be able to persist and later reverse. + +#### Scenario: Trend persists across many trades + +- **WHEN** the market enters an upward trend +- **THEN** prices remain elevated over many successive trades rather than reverting immediately + +#### Scenario: News reverses the trend + +- **WHEN** a sufficiently strong opposite news event arrives during a trend +- **THEN** the market can reverse direction + +### Requirement: Continuous market-making liquidity + +A market maker SHALL continuously quote both a bid and an ask around the current value so that trades occur without the player having to initiate every transaction. + +#### Scenario: Trades occur without player action + +- **WHEN** the simulation is running and no external order is present +- **THEN** trades still occur between market participants at a regular rate + +### Requirement: Informed agents adapt to news and trend + +Informed trading agents SHALL adjust their quoted prices in the direction of incoming news and the current trend, amplifying the market's reaction. + +#### Scenario: Agents lean with the trend + +- **WHEN** the market is trending upward +- **THEN** informed agents quote higher prices than they would in a flat market diff --git a/openspec/changes/adaptive-market-engine/specs/market-calendar/spec.md b/openspec/changes/adaptive-market-engine/specs/market-calendar/spec.md new file mode 100644 index 000000000..f1afc5b22 --- /dev/null +++ b/openspec/changes/adaptive-market-engine/specs/market-calendar/spec.md @@ -0,0 +1,46 @@ +## Purpose + +Defines the simulated daily calendar: each period is a trading day, and trades carry simulated daily timestamps that reach the platform's feed so charts show meaningful day boundaries. + +## ADDED Requirements + +### Requirement: Daily trading periods + +The simulation SHALL advance in daily periods, each representing one trading day with an open and a close. + +#### Scenario: Periods advance by day + +- **WHEN** a trading period ends +- **THEN** the next period begins as a new trading day + +### Requirement: Simulated timestamps + +Trades and quotes generated by the simulation SHALL carry timestamps derived from the simulated daily clock, not from epoch millis or wall-clock arrival time. + +#### Scenario: Trade carries a simulated day and time + +- **WHEN** a trade occurs during a trading day +- **THEN** it is timestamped with that day's date and an intraday time + +#### Scenario: No epoch dates + +- **WHEN** simulated market data is displayed or persisted +- **THEN** no date from the simulation resolves to the 1970 epoch + +### Requirement: Timestamps flow to the feed + +The simulated timestamps SHALL propagate into the platform's feed so that charts and history display the simulated dates. + +#### Scenario: Chart shows day boundaries + +- **WHEN** trades occur across multiple simulated days +- **THEN** charts show those trades under distinct simulated days rather than a single wall-clock session + +### Requirement: Day boundary reset + +At the end of a trading day the market SHALL close (stop accepting orders) and reopen for the next day, carrying forward any relevant state defined for the game. + +#### Scenario: Market closes and reopens + +- **WHEN** the end of a trading day is reached +- **THEN** order entry pauses, the day advances, and the market reopens for the next day diff --git a/openspec/changes/adaptive-market-engine/specs/market-leverage/spec.md b/openspec/changes/adaptive-market-engine/specs/market-leverage/spec.md new file mode 100644 index 000000000..1ad99e00e --- /dev/null +++ b/openspec/changes/adaptive-market-engine/specs/market-leverage/spec.md @@ -0,0 +1,59 @@ +## Purpose + +Defines how participants borrow capital to trade with leverage and how margin is enforced, including buying power, margin requirements, and forced liquidation. + +## ADDED Requirements + +### Requirement: Leveraged buying power + +A participant's maximum position size SHALL exceed their available cash by a configurable leverage multiplier. + +#### Scenario: Buying beyond cash + +- **WHEN** a participant has cash for one unit but a configured leverage multiplier of four +- **THEN** they may hold a position worth up to four times their cash + +### Requirement: Borrowed capital + +Buying beyond available cash SHALL be financed by a loan, leaving the participant's cash balance negative to represent the borrowed amount. + +#### Scenario: Cash goes negative on a leveraged buy + +- **WHEN** a leveraged buy exceeds available cash +- **THEN** the participant's cash balance becomes negative by the borrowed amount + +### Requirement: Margin requirement + +Opening a leveraged position SHALL require the participant to post margin equal to a configured fraction of the position's value. + +#### Scenario: Insufficient margin rejects the order + +- **WHEN** a position would require more margin than the participant can post +- **THEN** the order is rejected + +### Requirement: Equity determines margin availability + +A participant's equity SHALL be their cash plus the market value of long positions minus the market value of short positions, and it SHALL determine how much margin remains available. + +#### Scenario: Equity falls as prices move against a position + +- **WHEN** the price of a leveraged long position falls +- **THEN** the participant's equity falls by the same amount + +### Requirement: Margin call and forced liquidation + +When a participant's equity falls below a configured maintenance margin, their positions SHALL be force-liquidated until equity is restored. + +#### Scenario: Forced liquidation on a margin call + +- **WHEN** a leveraged position's loss drives equity below the maintenance margin +- **THEN** the engine force-closes positions to restore equity, realizing the loss + +### Requirement: Loan interest + +Borrowed cash SHALL accrue interest at a configured rate on a per-period basis. + +#### Scenario: Interest accrues on a loan + +- **WHEN** a participant holds a cash loan across a period boundary +- **THEN** interest is added to the borrowed amount diff --git a/openspec/changes/adaptive-market-engine/specs/market-matching/spec.md b/openspec/changes/adaptive-market-engine/specs/market-matching/spec.md new file mode 100644 index 000000000..13733bb2a --- /dev/null +++ b/openspec/changes/adaptive-market-engine/specs/market-matching/spec.md @@ -0,0 +1,152 @@ +## Purpose + +Defines the matching behavior of the simulated market's limit order book, so orders execute the way a real continuous double auction does. + +## ADDED Requirements + +### Requirement: Price-time priority + +Orders in the book SHALL queue by price then by arrival time: the best-priced order trades first, and among orders at the same price the earliest-arrived trades first. + +#### Scenario: Better price trades first + +- **WHEN** a new order can match multiple resting orders at different prices +- **THEN** it trades against the best-priced resting order before any worse-priced resting order + +#### Scenario: Same-price orders trade in arrival order + +- **WHEN** two resting orders share the same price +- **THEN** the one that arrived earlier is matched first + +### Requirement: Full-depth execution + +A single incoming order SHALL be able to trade against multiple resting orders across successive price levels until it is filled or no matching price remains. + +#### Scenario: Large order sweeps multiple levels + +- **WHEN** an incoming order's quantity exceeds the quantity available at the best price +- **THEN** the remainder continues to match the next price level, generating one trade per level crossed + +### Requirement: Market orders sweep the book + +A market order SHALL execute at whatever prices are available on the opposite side, filling as much quantity as the book provides. + +#### Scenario: Market buy exhausts available asks + +- **WHEN** a market buy order's quantity is larger than the resting ask quantity at the best price +- **THEN** it fills across the ask levels until the order is complete or the ask side is empty + +#### Scenario: Unfilled market order is cancelled + +- **WHEN** a market order cannot be fully filled because the opposite side is empty +- **THEN** its unfilled remainder is cancelled rather than resting in the book + +### Requirement: Limit order remainder rests in the book + +A limit order's unfilled quantity SHALL rest in the book at its limit price and remain eligible for future matching. + +#### Scenario: Partially filled limit order rests + +- **WHEN** a limit order is partially filled by an incoming opposite order +- **THEN** the unfilled quantity stays in the book at its limit price + +### Requirement: Order amendment and cancellation + +A resting order SHALL support cancellation and amendment of price and/or quantity, with amended orders re-queued according to the new price and, when the quantity increases, treated as a new arrival for the added quantity. + +#### Scenario: Cancel removes a resting order + +- **WHEN** a resting order is cancelled +- **THEN** it is removed from the book and no longer eligible for matching + +#### Scenario: Price amendment re-queues the order + +- **WHEN** a resting order's price is amended +- **THEN** it is re-queued at the new price with the amended quantity + +### Requirement: Every match emits a trade + +Each execution between a buy and a sell order SHALL produce a trade record carrying price, quantity, and the two counterparties. + +#### Scenario: Trade record on a match + +- **WHEN** an incoming order matches a resting order +- **THEN** a trade is emitted with the matched price, matched quantity, and the buy and sell counterparties + +### Requirement: Stop orders + +A stop order SHALL rest off-book until the market price crosses its trigger price, at which point it SHALL become a market order. + +#### Scenario: Stop triggers on a falling market + +- **WHEN** a sell stop order's trigger is crossed by a falling market price +- **THEN** the stop order becomes a market order and executes at the available price + +#### Scenario: Stop stays inactive below trigger + +- **WHEN** the market price has not crossed a stop order's trigger +- **THEN** the stop order does not rest in the visible book and does not trade + +### Requirement: Stop-limit orders + +A stop-limit order SHALL rest off-book until its trigger is crossed, then become a limit order at its specified limit price. + +#### Scenario: Stop-limit becomes a limit order + +- **WHEN** a stop-limit order's trigger is crossed +- **THEN** it enters the book as a limit order at its limit price and is matched under normal limit rules + +### Requirement: Trailing stop orders + +A trailing stop order SHALL maintain a trigger that follows the market by a fixed offset from the best price observed since activation, so the trigger moves in the favorable direction and stays put otherwise. + +#### Scenario: Trailing stop follows a rising market + +- **WHEN** a sell trailing stop is active and the market price rises +- **THEN** its trigger rises with the market while preserving the configured offset + +#### Scenario: Trailing stop triggers on reversal + +- **WHEN** the market reverses by more than the trailing offset from the best observed price +- **THEN** the trailing stop triggers and becomes a market order + +### Requirement: Immediate-or-cancel orders + +An immediate-or-cancel (IOC) order SHALL execute immediately against available liquidity and any unfilled remainder SHALL be cancelled. + +#### Scenario: IOC partial fill + +- **WHEN** an IOC order can be only partially filled from the book +- **THEN** the filled portion trades and the remainder is cancelled, never resting in the book + +### Requirement: Fill-or-kill orders + +A fill-or-kill (FOK) order SHALL execute immediately in its entirety or be cancelled with no fill; it SHALL NOT partially fill. + +#### Scenario: FOK cancels when not fully fillable + +- **WHEN** the book cannot fill an FOK order's full quantity immediately +- **THEN** the order is cancelled without any trade + +#### Scenario: FOK fills fully + +- **WHEN** the book can fill an FOK order's full quantity immediately +- **THEN** the entire quantity executes + +### Requirement: Iceberg orders + +An iceberg order SHALL expose only a display quantity in the book and SHALL replenish that display quantity from a hidden total as the visible portion is consumed, until the hidden total is exhausted. + +#### Scenario: Iceberg replenishes after a fill + +- **WHEN** the displayed quantity of an iceberg order is consumed by a trade +- **THEN** a new display quantity is revealed from the hidden total until it is exhausted + +### Requirement: Pegged orders + +A pegged order SHALL track a reference price (best bid, best ask, or mid) at a fixed offset and SHALL reprice when the reference moves. + +#### Scenario: Pegged order reprices with the book + +- **WHEN** the reference price of a pegged order changes +- **THEN** the pegged order's price is updated to the new reference plus its offset diff --git a/openspec/changes/adaptive-market-engine/specs/market-positions/spec.md b/openspec/changes/adaptive-market-engine/specs/market-positions/spec.md new file mode 100644 index 000000000..0e994c1b1 --- /dev/null +++ b/openspec/changes/adaptive-market-engine/specs/market-positions/spec.md @@ -0,0 +1,68 @@ +## Purpose + +Defines how the market tracks participant positions, including short positions, so a trader may sell an asset they do not currently hold. + +## ADDED Requirements + +### Requirement: Signed positions + +A participant's position in an asset SHALL be a signed quantity: positive for a long position and negative for a short position. + +#### Scenario: Position is negative when short + +- **WHEN** a participant sells more of an asset than they hold +- **THEN** their position in that asset becomes negative + +### Requirement: Short selling is permitted + +A sell order SHALL be accepted and executed even when the participant holds insufficient or no assets, creating or increasing a short position. + +#### Scenario: Sell without holdings + +- **WHEN** a participant submits a sell order with no existing holdings +- **THEN** the order executes and opens a short position + +### Requirement: Short position limit + +A participant's short position in an asset SHALL be bounded by a configured limit, beyond which further sell orders are rejected. + +#### Scenario: Short limit blocks excess selling + +- **WHEN** a sell order would push a participant's short position past its limit +- **THEN** the order is rejected + +### Requirement: Buys cover shorts + +A buy order SHALL first reduce an existing short position before increasing a long position. + +#### Scenario: Covering a short + +- **WHEN** a participant with a short position buys the asset +- **THEN** their position moves toward (and past) zero, covering the short + +### Requirement: Opening a long position + +A buy order SHALL open or increase a long position when the participant has no short position to cover. + +#### Scenario: Buying while flat opens a long + +- **WHEN** a participant with no position buys an asset +- **THEN** their position becomes positive, opening a long position + +### Requirement: Selling reduces a long position + +A sell order SHALL reduce an existing long position before opening a short position. + +#### Scenario: Selling from a long + +- **WHEN** a participant sells an asset they hold +- **THEN** their long position decreases; selling more than held opens a short for the excess + +### Requirement: Trades update both sides' positions + +Each trade SHALL adjust the buyer's position by the traded quantity and the seller's position by the negative traded quantity. + +#### Scenario: Position update on a trade + +- **WHEN** a trade executes for a quantity +- **THEN** the buyer's position increases by that quantity and the seller's decreases by it diff --git a/openspec/changes/adaptive-market-engine/tasks.md b/openspec/changes/adaptive-market-engine/tasks.md new file mode 100644 index 000000000..e9151deed --- /dev/null +++ b/openspec/changes/adaptive-market-engine/tasks.md @@ -0,0 +1,64 @@ +## 1. Bundle scaffold + +- [x] 1.1 Create `org.eclipsetrader.market.sim` OSGi bundle (MANIFEST.MF, build.properties, pom.xml, plugin activator) and add it to the Maven reactor +- [x] 1.2 Add the bundle to the product feature/`plugin_customization.ini` so it is included in builds + +## 2. Matching engine + +- [x] 2.1 Implement `Order`/`Trade` value types with side, type, quantity, price, trigger, display quantity, and fill state +- [x] 2.2 Implement the order book with price-time priority insertion (binary search) for bids and asks +- [x] 2.3 Implement full-depth matching: a crossing order sweeps successive price levels, emitting one trade per level (spec `market-matching`) +- [x] 2.4 Implement market-order semantics (sweep until filled, cancel unfilled remainder) (spec `market-matching`) +- [x] 2.5 Implement cancel and amend (price and quantity re-queue rules) (spec `market-matching`) +- [x] 2.6 Compute per-asset book statistics: best bid/ask, level volume, last trade, VWAP +- [x] 2.7 Implement IOC and FOK time-in-force execution (immediate-or-cancel, fill-or-kill with no partial) (spec `market-matching`) +- [x] 2.8 Implement stop and stop-limit orders with an off-book trigger monitor (spec `market-matching`) +- [x] 2.9 Implement trailing stop orders (offset that follows the best observed price) (spec `market-matching`) +- [x] 2.10 Implement iceberg orders (display quantity replenished from a hidden total) (spec `market-matching`) +- [x] 2.11 Implement pegged orders (reprice to best bid/ask/mid plus offset) (spec `market-matching`) + +## 3. Positions and short selling + +- [x] 3.1 Implement signed position accounting (long/short) updated on every trade (spec `market-positions`) +- [x] 3.2 Implement short-selling validity: sells beyond holdings allowed up to a configured short limit, rejected beyond it (spec `market-positions`) +- [x] 3.3 Implement buy-to-cover semantics: buys reduce shorts before opening longs (spec `market-positions`) +- [x] 3.4 Implement the symmetric long path: buys open/increase longs, sells reduce longs before shorting (spec `market-positions`) + +## 4. Leverage and margin + +- [x] 4.1 Implement a signed cash balance with borrowed-capital tracking (negative cash = loan) (spec `market-leverage`) +- [x] 4.2 Implement leveraged buying power and margin-requirement checks (spec `market-leverage`) +- [x] 4.3 Implement equity computation (cash + long market value − short market value) (spec `market-leverage`) +- [x] 4.4 Implement margin-call detection and forced liquidation to restore equity (spec `market-leverage`) +- [x] 4.5 Implement loan interest accrual per period (spec `market-leverage`) + +## 5. Price process, news, and agents + +- [x] 5.1 Implement the fundamental value process `Fₜ = Fₜ₋₁ + driftₜ + newsₜ + noiseₜ` with a seeded PRNG (spec `market-adaptation`) +- [x] 5.2 Implement procedural news generation: asset, sentiment, and magnitude drawn from the seeded PRNG on a configurable schedule +- [x] 5.3 Implement the market maker that quotes `bid/ask = Fₜ ± spread/2` and sizes orders from displayed liquidity (spec `market-adaptation`) +- [x] 5.4 Implement informed agents that tilt quotes toward recent trend and incoming news (spec `market-adaptation`) +- [x] 5.5 Implement emergent drift estimation from recent trades with magnitude cap and mean reversion (spec `market-adaptation`) + +## 6. Simulated daily calendar + +- [x] 6.1 Implement the simulated clock and daily-period scheduler (open/close, day advance) (spec `market-calendar`) +- [x] 6.2 Timestamp all orders/trades/quotes from the simulated clock (no `System.currentTimeMillis()` in the engine) (spec `market-calendar`) +- [x] 6.3 Implement day-boundary behavior: stop order entry, advance the day, reopen for the next day + +## 7. Feed integration + +- [x] 7.1 Emit `Trade`/`Book`/`Quote`/`TodayOHL` events from engine listeners into the existing `FeedSubscription` seam +- [x] 7.2 Marshal feed updates onto the SWT display thread (`Display.asyncExec`) for UI safety +- [x] 7.3 Wire the engine lifecycle (start/stop with the simulated day) and expose a seeded scenario entry point + +## 8. Tests and verification + +- [x] 8.1 Unit-test matching: price-time priority, full-depth sweep, market exhaust/cancel, cancel/amend, stop/stop-limit triggers, trailing stop, IOC/FOK, iceberg replenishment, pegged repricing (spec `market-matching`) +- [x] 8.2 Unit-test positions: signed positions, long open/close, short selling, short limit rejection, buy-to-cover (spec `market-positions`) +- [x] 8.3 Unit-test leverage: buying power, negative cash loan, margin rejection, margin call + forced liquidation, loan interest (spec `market-leverage`) +- [x] 8.4 Unit-test adaptation: news moves the price, trend persists and reverses, trades occur without player action (spec `market-adaptation`) +- [x] 8.5 Unit-test calendar: simulated timestamps, no 1970 dates, day boundary close/reopen (spec `market-calendar`) +- [x] 8.6 Wire `org.eclipsetrader.market.sim.tests` into the Maven reactor so tests run in GitHub Actions CI +- [x] 8.7 Run `mvn package` and confirm the new bundle compiles and tests pass +- [ ] 8.8 Visually verify charts show continuous trending trades with daily timestamps (simulated via the devcontainer virtual display) diff --git a/openspec/changes/fix-jessx-deal-timestamps/.openspec.yaml b/openspec/changes/fix-jessx-deal-timestamps/.openspec.yaml new file mode 100644 index 000000000..878dc3156 --- /dev/null +++ b/openspec/changes/fix-jessx-deal-timestamps/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-07 diff --git a/openspec/changes/fix-jessx-deal-timestamps/design.md b/openspec/changes/fix-jessx-deal-timestamps/design.md new file mode 100644 index 000000000..261ce87e5 --- /dev/null +++ b/openspec/changes/fix-jessx-deal-timestamps/design.md @@ -0,0 +1,54 @@ +## Context + +- See `proposal.md` for motivation and `specs/jessx-time-handling/spec.md` for the behavioral requirements. +- JESSX sends deal/order timestamps as *elapsed milliseconds within the current period* (`ExperimentManager.getTimeInPeriod()` = `now - periodBeginning`). EclipseTrader currently feeds them straight into `new Date(...)`, producing 1970 dates. +- Two consumption points in `org.eclipsetrader.jessx`: + 1. `BrokerConnector` — streaming `Deal` → `Trade` + OHLC history (this is what the closed PR #18 patched inline with a sub-2000 heuristic). + 2. `JessxTradeHistory` — persisted holding `PURCHASE_DATE` (still broken, unchanged by PR #18). + +## Goals / Non-Goals + +**Goals:** +- Absolute, current-era dates for JESSX deals/orders at both consumption points. +- One shared conversion so streaming charts and persisted holdings can never diverge. +- Supersede PR #18's inline workaround. + +**Non-Goals:** +- Changing what the JESSX server sends (the server is correct to send elapsed time; it is the client's interpretation that is wrong). +- Reconstructing the exact simulated wall-clock time from period start (live deals arrive in real time; arrival time is accurate enough). +- Migrating already-persisted 1970 holdings from old repositories. + +## Decisions + +### 1. Single shared converter with a magnitude-based interpretation + +Add one helper (e.g., `JessxTime.toAbsoluteDate(long timestamp)` in a new internal utility class) used by both `BrokerConnector` and `JessxTradeHistory`: + +- **Below threshold** (elapsed-in-period value, bounded by period duration, e.g. `< 86,400,000 ms` — one day): interpret as elapsed → return arrival time (`new Date(System.currentTimeMillis())`). +- **At or above threshold** (a plausible epoch millis): return `new Date(timestamp)` unchanged. + +The threshold is the maximum plausible period duration. Unlike PR #18's "before year 2000" check, this correctly preserves genuine epoch values while catching all realistic elapsed values (which are at most a few hours, i.e. millions of ms, never billions). + +- **Rationale**: deals are received live, so `arrival time ≈ periodStart + elapsed` within network latency. Tracking period start on the client would add state and message plumbing for no observable gain. +- **Alternative considered**: track `periodBeginning` on the client and compute `periodStart + elapsed`. Rejected — requires new state and event handling across the feed, and its output equals arrival time in practice. + +### 2. Apply at both consumption points + +- `BrokerConnector` Deal handling: replace the inline PR #18 heuristic with `JessxTime.toAbsoluteDate(...)`. +- `JessxTradeHistory`: route `finalDeal.getTimestamp()` through the same converter before `IPropertyConstants.PURCHASE_DATE` is set. + +### 3. Keep OHLC bar-merging behavior as-is + +The existing merge condition (`last.getDate().equals(tradeData.getTime())`) is left untouched. Consecutive deals get distinct arrival timestamps (ms precision), so each becomes its own bar — this matches current behavior and satisfies the spec's "current-era date" requirement. + +## Risks / Trade-offs + +- **[Risk] Threshold heuristic misclassifies an extreme value** → an elapsed period longer than one day would be misread as epoch. Mitigation: threshold is a single named constant, documented; simulation period durations in JESSX are far smaller. Can be tuned without API change. +- **[Risk] Arrival time instead of true sim time on persisted holdings** → purchase dates are "when the platform received the deal", not the simulated timestamp. Accepted for a live simulation; consistent between chart and holdings (which is what the spec requires). +- **[Risk] Pre-existing 1970 holdings remain in repositories** → out of scope; only new deals are corrected. Flagged in the migration plan so it is not mistaken for a regression. + +## Migration Plan + +- No data migration. Existing broken holdings stay as-is (documented limitation); newly saved deals get correct dates. +- Rollback: revert the change — the converter is additive and localized to the two call sites. +- This change is independent of `modernize-chart-rendering` and of the extracted PR #18 bug fixes. diff --git a/openspec/changes/fix-jessx-deal-timestamps/proposal.md b/openspec/changes/fix-jessx-deal-timestamps/proposal.md new file mode 100644 index 000000000..52daa39dd --- /dev/null +++ b/openspec/changes/fix-jessx-deal-timestamps/proposal.md @@ -0,0 +1,28 @@ +## Why + +The JESSX trading game sends deal/order timestamps as elapsed milliseconds within the current period (`ExperimentManager.getTimeInPeriod()` = `now - periodBeginning`), not epoch milliseconds. EclipseTrader misreads them as epoch millis and feeds them into `new Date(...)`, so streaming trades and persisted holdings show 1970 dates on charts. The closed PR #18 patched only the streaming path in `BrokerConnector` with a "before year 2000 → use now" heuristic; the persistence path (`JessxTradeHistory`) still writes 1970 purchase dates. + +## What Changes + +- Introduce a single, shared interpretation of JESSX deal/order timestamps so elapsed-in-period values are converted to absolute dates instead of being misread as epoch millis. +- Apply the conversion consistently at both consumption points: + - `BrokerConnector` streaming trade feed (charts/OHLC history). + - `JessxTradeHistory` persisted holdings (portfolio purchase dates). +- Absorb and replace the PR #18 workaround; the heuristic moves into the shared converter (or is superseded by a cleaner derivation) rather than living inline in `BrokerConnector`. + +## Capabilities + +### New Capabilities + +- `jessx-time-handling`: how JESSX deal/order timestamps are interpreted and converted to absolute times, applied consistently across the streaming feed and persisted trade history. + +### Modified Capabilities + +None — this repo has no existing specs yet; this capability is new. + +## Impact + +- **Code**: `org.eclipsetrader.jessx` — `BrokerConnector` (streaming `Deal` handling) and `JessxTradeHistory` (holding persistence). A shared timestamp converter utility. +- **Behavior**: charts no longer show 1970 timestamps; portfolio holdings get correct purchase dates. No change for non-JESSX data feeds. +- **Dependencies**: none new. +- **Related work**: independent of the chart modernization change; this stays out of the merged PR #18 bug-fix extraction. diff --git a/openspec/changes/fix-jessx-deal-timestamps/specs/jessx-time-handling/spec.md b/openspec/changes/fix-jessx-deal-timestamps/specs/jessx-time-handling/spec.md new file mode 100644 index 000000000..c4cbb9d31 --- /dev/null +++ b/openspec/changes/fix-jessx-deal-timestamps/specs/jessx-time-handling/spec.md @@ -0,0 +1,42 @@ +## Purpose + +Defines how JESSX deal and order timestamps are interpreted and converted to absolute dates, consistently across streaming charts and persisted trade history. + +## ADDED Requirements + +### Requirement: Absolute deal timestamps + +JESSX deal and order timestamps that represent elapsed time within the current period SHALL be converted to absolute dates when consumed by the platform, so trades and OHLC bars carry real-world dates. + +#### Scenario: Live deal with elapsed timestamp + +- **WHEN** a JESSX Deal message carries an elapsed-in-period timestamp +- **THEN** the resulting trade and OHLC bar carry an absolute date near the time the deal was received rather than the epoch (1970) + +#### Scenario: Already-absolute timestamp + +- **WHEN** a JESSX message carries a value that is already a valid absolute epoch millis +- **THEN** the value is used as-is without conversion + +### Requirement: Consistent interpretation across consumption paths + +The same timestamp interpretation SHALL apply to the streaming trade feed and to persisted trade history. + +#### Scenario: Persisted holding purchase date + +- **WHEN** a JESSX deal is saved to the repository as a holding +- **THEN** the purchase date is an absolute date consistent with the same deal's streaming chart time + +#### Scenario: Chart and holdings agree + +- **WHEN** the same JESSX deal appears both on a chart and in the portfolio holdings +- **THEN** both show the same absolute timestamp + +### Requirement: No epoch dates from elapsed timestamps + +The platform SHALL NOT display or persist dates derived from JESSX elapsed-in-period timestamps as 1970 epoch dates. + +#### Scenario: Chart shows current-era date + +- **WHEN** a JESSX deal is shown on a chart +- **THEN** the tooltip and summary bar show a date in the current era, not 1970 diff --git a/openspec/changes/fix-jessx-deal-timestamps/tasks.md b/openspec/changes/fix-jessx-deal-timestamps/tasks.md new file mode 100644 index 000000000..0650f04e1 --- /dev/null +++ b/openspec/changes/fix-jessx-deal-timestamps/tasks.md @@ -0,0 +1,12 @@ +## 1. Shared timestamp converter + +- [x] 1.1 Add `JessxTime.toAbsoluteDate(long)` in the jessx bundle's internal package: values below the elapsed threshold (one-day constant) resolve to the arrival time, values at or above it are treated as epoch millis +- [x] 1.2 Replace the inline sub-2000 heuristic in `BrokerConnector` Deal handling with the shared converter +- [x] 1.3 Route `JessxTradeHistory` holding `PURCHASE_DATE` through the same converter +- [x] 1.4 Add unit tests for the converter (elapsed value → current-era date, epoch value → unchanged, boundary at the threshold) wired into the Maven reactor so they run in GitHub Actions CI + +## 2. Verification + +- [x] 2.1 Run `mvn package` on the branch (GitHub Actions `maven.yml` on JDK 21) and confirm `org.eclipsetrader.jessx` compiles and converter tests pass +- [ ] 2.2 Live-check in the running product (Codespaces virtual display or a local machine): start a JESSX simulation and confirm chart tooltips/summary show current-era timestamps, not 1970 +- [ ] 2.3 Confirm a traded deal persists with a current-era purchase date in the portfolio holdings, and that chart and holdings agree (spec `jessx-time-handling`) diff --git a/openspec/changes/modernize-chart-rendering/.openspec.yaml b/openspec/changes/modernize-chart-rendering/.openspec.yaml new file mode 100644 index 000000000..878dc3156 --- /dev/null +++ b/openspec/changes/modernize-chart-rendering/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-07 diff --git a/openspec/changes/modernize-chart-rendering/design.md b/openspec/changes/modernize-chart-rendering/design.md new file mode 100644 index 000000000..1b6fd260c --- /dev/null +++ b/openspec/changes/modernize-chart-rendering/design.md @@ -0,0 +1,69 @@ +## Context + +- See `proposal.md` for motivation. See `specs/chart-rendering/spec.md` and `specs/chart-theme/spec.md` for the behavioral requirements this design satisfies. +- The chart renders by drawing onto an offscreen `Image` (`ChartCanvas.onPaint`, `DateScaleCanvas`, vertical scale canvas), then blits it to the canvas. The image is recreated only on resize, and `Image.getBounds()` returns *logical* size, so a zoom change alone never triggers recreation — an image created at one zoom stays stale when the device zoom changes (mixed-DPI monitors, OS scale changes). +- Chart colors are inline `new RGB(...)` literals scattered across the charts package, `MainChartFactory`, and `MainPropertiesPage`. +- Geometry is partially cached already (`valid`/`pointArray` pooling in `CandleStickChart`, etc.), but `setDataBounds` re-filters the full series O(n) and there is no downsampling, so zoomed-out large histories draw every point. +- Target platform is **Eclipse 2024-03 / SWT 4.31, Java 21** (see `org.eclipsetrader.releng/eclipsetrader.target`). On SWT 4.31, `new Image(device, w, h)` already creates a device-zoom-aware backing image: width/height are logical points auto-scaled to native pixels in the constructor, and `getBounds()` reports logical size. Per-monitor zoom is `Monitor.getZoom()`; the zoom captured at image creation is `DPIUtil.getDeviceZoom()`. `GC.setAntialias()`/`GC.setTextAntialias()` are available. (`Display.getZoom()` and `Image.setScaleFactor()` are **not** in 4.31 — they landed in SWT 3.104 / 2024-09, one release after the target.) + +## Goals / Non-Goals + +**Goals:** +- Crisp chart output on any display zoom (HiDPI) with minimal change to chart-object drawing code. +- One source of truth for chart colors, with user-saved color preferences still winning. +- Bounded rendering work per frame regardless of history size. + +**Non-Goals:** +- Dark mode UI / theme switching in the preferences UI (the theme layer enables it later; selecting themes is out of scope). +- Rewriting the `IChartObject` model or the axis classes. +- Overhauling gridline/`Calendar`-based tick computation (`ChartCanvas.paintBackground`) — tracked as a follow-up, not in this change. +- Changing chart templates or persisted layouts. + +## Decisions + +### 1. HiDPI via zoom-aware offscreen images + +Create offscreen images at the chart's logical `clientArea` size with `new Image(display, w, h)`. SWT 4.31's image/GC pipeline is already device-zoom-aware: the constructor auto-scales the backing store to native pixels, `getBounds()` reports logical size, and GC drawing auto-scales logical coordinates. All existing drawing code (which uses logical coordinates) keeps working unchanged and already lands at native resolution. + +The remaining defect is lifecycle, not size: canvases recreate the image only on resize, and because `getBounds()` returns logical size the resize check never fires on a zoom change. Fix with a single helper, `ChartUtils.createBackingImage(Canvas, Rectangle)`, that (a) creates the logical-size image and (b) records the zoom at creation — from the canvas's monitor via `Monitor.getZoom()`, falling back to `display.getDPI().x / 72` if unavailable — and lets the canvas detect on the next paint that the zoom differs and recreate the image. Used by the main canvas, the date scale canvas, and the vertical scale canvas. + +- **Alternative considered**: create the image at `clientArea * zoom/100` pixels and apply `image.setScaleFactor(zoom, zoom)`. Rejected: `Image.setScaleFactor` does not exist in SWT 4.31 (added in 3.104/2024-09), and the 4.31 `Image(Device, w, h)` constructor already auto-scales — multiplying by zoom again would double-scale at zoom > 100%. +- **Alternative considered**: `GC.setTransform(new Transform(...scale))` per paint. Rejected: more invasive, risks double-scaling on every draw call, and doesn't help the blit step. +- **Alternative considered**: keep resize-only recreation (status quo). Rejected — that is the stale-zoom defect this change fixes. +- Antialiasing (shapes + text) is enabled in the `Graphics` constructor, as proposed in the superseded PR. + +### 2. Theme layer: `ChartTheme` value object + default registry + +Introduce an immutable `ChartTheme` (RGB: line, positive, negative, outline, grid, background) and a `ChartThemes` holder exposing the default light theme (the Material palette proposed in PR #18: blue line `33,150,243`, teal `38,166,154`, red `239,83,80`, outline `64,64,64`). + +- Chart classes (`CandleStickChart`, `BarChart`, `HistogramBarChart`, `HistogramAreaChart`, `OHLCLineChart`, `LineChart`) replace their inline `new RGB(...)` field initializers/constructor fallbacks with lookups from `ChartThemes.getDefault()`. Their RGB constructor parameters stay — the `null`-fallback semantics are unchanged. +- `MainChartFactory` field defaults and `MainPropertiesPage` color-selector defaults read from the same theme. +- User preferences keep their existing path: `setParameters` → non-null RGB → `createObject(...)` passes them in, overriding theme defaults (spec `chart-theme`). +- **Alternative considered**: full dependency injection / OSGi service for themes. Rejected as over-engineering for this codebase; a plain value object + static default is enough to centralize the palette and later add a second theme. +- **Rationale for value-object over a live theme object**: RGB is a plain value in SWT; no listeners/observability are needed for this change. + +### 3. Downsampling via per-renderer aggregators, cached per visible range + +Extend the existing `setDataBounds`/`valid` pattern with an aggregation step and a cache keyed by `(firstDate, lastDate, pixelWidth)`: + +- When the visible points exceed `clientArea.width`, aggregate before building geometry. +- **OHLC renderers** (candles, bars, OHLC line): min/max binning per pixel column — preserve each column's high/low (and first-open/last-close for candles) so zoomed-out bars stay honest. +- **Scalar renderers** (line, area, histogram): min/max per column as well (keeps spikes visible); LTTB is an acceptable alternative for line aesthetics — implementation detail, spec only requires one aggregate per column. +- Implemented as shared helpers (e.g., `OHLCDownsampler.downsample(IOHLC[], int width)` and a scalar counterpart) so all chart classes benefit without duplicating logic. +- Cache lives with the chart object alongside `pointArray`; `invalidate()` clears it. Work per frame becomes O(pixels) to draw plus O(n) only when data/bounds actually change. +- **Alternative considered**: always-on LTTB for everything. Rejected — min/max binning is O(n) and correct for OHLC extremes; LTTB is a polish option for lines only. +- **Trade-off**: zoomed-out tooltips report the aggregated candle/bar rather than one specific tick. Accepted (standard practice); per-tick detail returns on zoom-in. + +## Risks / Trade-offs + +- **[Risk] A scale-canvas or image path misses the zoom-change recreation** → inconsistent crispness across mixed-DPI moves. Mitigation: single `createBackingImage` helper used by all three canvases; verify visually at 200% during implementation. +- **[Risk] Downsampling alters zoomed-out visuals and tooltips** → expected behavior change. Mitigation: spec already defines one-aggregate-per-column; confirm the min/max OHLC result with a large history in review. +- **[Risk] Theme defaults shift user-visible colors** for charts that had no explicit colors → intended by design (spec `chart-theme`), but only affects charts with no saved colors. +- **[Risk] Per-frame allocation regressions** → geometry and aggregation caches reuse objects; keep pooling behavior from the current `pointArray` pattern. +- **[Risk] SWT version drift** (zoom APIs) → guard with fallback to `getDPI()`; `Monitor.getZoom()` exists in the fixed 2024-03 target. + +## Migration Plan + +- Pure UI change; no persisted data or API breakage. Chart-object constructors and `MainChartFactory` keep their signatures. +- Rollback: revert the change — existing layouts and preferences are untouched. +- Land independently of the extracted PR #18 bug fixes (tooltip + resource disposal), which merge first on master. diff --git a/openspec/changes/modernize-chart-rendering/proposal.md b/openspec/changes/modernize-chart-rendering/proposal.md new file mode 100644 index 000000000..972403847 --- /dev/null +++ b/openspec/changes/modernize-chart-rendering/proposal.md @@ -0,0 +1,30 @@ +## Why + +The chart rendering pipeline dates to the 2004-era Eclipse Trader codebase. It draws to a pixel-sized offscreen image that is recreated only on resize and never re-synced to the display's zoom factor, so output can be stale or blurry when the device zoom changes (mixed-DPI monitors, OS scale changes). It scatters raw `RGB` literals across ~8 classes (no single palette to maintain, no path to dark mode), and rebuilds all point geometry on every repaint (janky on large histories). PR #18 only polished the surface (antialiasing + a one-off palette) without addressing these structural issues, so it has been closed and its durable bug fixes extracted separately. + +## What Changes + +- **HiDPI / device-zoom-aware rendering**: offscreen chart images are recreated when the display's zoom factor changes, so charts stay crisp across DPI/scale changes (SWT 4.31's image/GC pipeline already renders at native resolution). +- **Theme / palette service**: a theme provider centralizes all chart colors (line, positive/negative, outline, grid, background). The chart classes and `MainPropertiesPage` source their colors from it instead of inline `new RGB(...)`. Saved user color preferences continue to be honored. Dark mode becomes possible later without rework. +- **Performance: geometry caching + downsampling**: computed point geometry (candles/bars/points) is cached and invalidated only when data or bounds change; large series are downsampled (min/max binning) so the number of rendered points stays bounded by pixels. +- **Rendering quality**: antialiasing and text antialiasing enabled for all chart drawing (retained from the closed PR, re-applied on the new pipeline). +- Supersedes the cosmetic portions of the closed PR #18. Its durable bug fixes (Close-vs-High tooltips, `SummaryOHLCItem` resource disposal) land as a separate small bugfix PR first. + +## Capabilities + +### New Capabilities + +- `chart-rendering`: the chart drawing pipeline — device-zoom-aware output, geometry caching, and dataset downsampling so charts render correctly and smoothly at any display scale and data size. +- `chart-theme`: the centralized color/theme provider that all chart objects and chart property pages source their colors from, including honoring user-saved color preferences. + +### Modified Capabilities + +None — this repo has no existing specs yet; both capabilities are new. + +## Impact + +- **Code**: `org.eclipsetrader.ui` charts package — `Graphics`, `ChartCanvas`, `CandleStickChart`, `BarChart`, `OHLCLineChart`, `HistogramAreaChart`, `HistogramBarChart`, `LineChart`, axis classes — plus `MainChartFactory`, `MainPropertiesPage`, and `org.eclipsetrader.ui.charts.indicators` (`Util`). Chart templates unchanged (they already default to candles). +- **API**: new `ChartTheme`/`ChartThemes` provider API; chart object constructors keep their signatures and fall back to the shared default theme, so existing callers keep compiling. +- **Dependencies**: none new — SWT only. Antialiasing and zoom awareness are standard SWT/GC features. +- **Persistence**: existing saved chart color preferences remain valid and override theme defaults. +- **Related work**: closed PR #18 is not merged; its extracted bug fixes ship as a separate small PR. diff --git a/openspec/changes/modernize-chart-rendering/specs/chart-rendering/spec.md b/openspec/changes/modernize-chart-rendering/specs/chart-rendering/spec.md new file mode 100644 index 000000000..0748de7e6 --- /dev/null +++ b/openspec/changes/modernize-chart-rendering/specs/chart-rendering/spec.md @@ -0,0 +1,61 @@ +## Purpose + +Defines how chart rendering behaves across display scales and dataset sizes: crisp device-zoom-aware output, cached geometry, and bounded work per repaint on large histories. + +## ADDED Requirements + +### Requirement: Device-zoom-aware rendering + +The chart rendering SHALL recreate the offscreen image when the display's zoom factor (DPI scaling) changes, so chart output stays crisp across display-scale changes. + +#### Scenario: High-zoom display + +- **WHEN** a chart is displayed on a device with a zoom factor greater than 100% (e.g., 200%) +- **THEN** candles, bars, lines, grid, and axis text render at the correct scaled size without blur or aliasing artifacts + +#### Scenario: Zoom change after image creation + +- **WHEN** the display zoom factor changes while a chart is displayed (e.g., the window moves to a monitor with a different scale) +- **THEN** the offscreen image is recreated at the new zoom and the chart renders crisply + +#### Scenario: Standard display unchanged + +- **WHEN** a chart is displayed on a device with a zoom factor of 100% +- **THEN** rendering is equivalent in content and layout to current behavior + +### Requirement: Geometry caching + +Chart objects SHALL reuse cached point geometry across repaints and SHALL recompute geometry only when the underlying data or visible bounds change. + +#### Scenario: Repaint without data change + +- **WHEN** a chart repaints (for example on focus change or redraw request) without new data or a bounds change +- **THEN** the previously computed point geometry is reused and no full geometry rebuild occurs + +#### Scenario: Data or bounds change invalidates cache + +- **WHEN** new data arrives or the visible date range changes +- **THEN** the cached geometry is invalidated and recomputed on the next repaint + +### Requirement: Downsampling of large series + +When the number of data points in the visible range exceeds the available horizontal pixels, the renderer SHALL aggregate points per pixel column so the number of drawn elements stays bounded by the chart width and rendering remains responsive. + +#### Scenario: Zoomed-out large history + +- **WHEN** a series with more points than horizontal pixels is displayed +- **THEN** the chart draws at most one aggregate element (candle, bar, or point) per pixel column using a min/max style aggregation + +#### Scenario: Small history unchanged + +- **WHEN** the visible range contains fewer points than horizontal pixels +- **THEN** every data point is drawn individually without aggregation + +### Requirement: Antialiased drawing + +All chart drawing SHALL be performed with shape and text antialiasing enabled. + +#### Scenario: Chart rendering quality + +- **WHEN** a chart is drawn +- **THEN** diagonal lines and text edges are antialiased rather than jagged diff --git a/openspec/changes/modernize-chart-rendering/specs/chart-theme/spec.md b/openspec/changes/modernize-chart-rendering/specs/chart-theme/spec.md new file mode 100644 index 000000000..98daa8f29 --- /dev/null +++ b/openspec/changes/modernize-chart-rendering/specs/chart-theme/spec.md @@ -0,0 +1,42 @@ +## Purpose + +Provides a single source of truth for chart colors so the palette is maintainable, dark mode is possible, and user-saved color preferences keep working. + +## ADDED Requirements + +### Requirement: Centralized chart palette + +All chart colors (positive, negative, outline, line, grid, and background) SHALL be sourced from a theme provider rather than inline color literals inside chart objects. + +#### Scenario: Default-theme chart + +- **WHEN** a chart object is created without explicitly configured colors +- **THEN** it uses the colors defined by the active theme + +#### Scenario: Theme palette change + +- **WHEN** the active theme's palette is updated +- **THEN** charts rendering with theme colors reflect the new colors on their next repaint + +### Requirement: User color preferences override theme + +User-saved chart color preferences SHALL take precedence over theme defaults for the configured chart. + +#### Scenario: Custom candle colors + +- **WHEN** a user has saved custom candle colors for a chart +- **THEN** the chart renders with exactly those colors regardless of the active theme + +#### Scenario: Reverting to theme colors + +- **WHEN** a chart has no custom colors saved +- **THEN** it renders with the active theme's colors + +### Requirement: Theme covers every renderer + +The theme provider SHALL supply colors for all supported chart renderings: candlesticks, bars, OHLC lines, and histograms. + +#### Scenario: Style switch keeps theme colors + +- **WHEN** a user switches a chart's rendering style (for example from candles to bars) +- **THEN** the new style uses the theme's colors appropriate to that renderer diff --git a/openspec/changes/modernize-chart-rendering/tasks.md b/openspec/changes/modernize-chart-rendering/tasks.md new file mode 100644 index 000000000..7103e9f65 --- /dev/null +++ b/openspec/changes/modernize-chart-rendering/tasks.md @@ -0,0 +1,29 @@ +## 1. Theme layer + +- [x] 1.1 Add `ChartTheme` value object (line, positive, negative, outline, grid, background RGB) and `ChartThemes.getDefault()` exposing the default Material palette (line `33,150,243`, positive `38,166,154`, negative `239,83,80`, outline `64,64,64`) +- [x] 1.2 Replace inline `new RGB(...)` field/constructor fallbacks in `CandleStickChart`, `BarChart`, `HistogramBarChart`, `HistogramAreaChart`, `OHLCLineChart`, and `LineChart` with lookups from `ChartThemes.getDefault()`, keeping constructor color parameters and their null-fallback semantics +- [x] 1.3 Source `MainChartFactory` color field defaults and `MainPropertiesPage` color-selector defaults from the same theme +- [ ] 1.4 Verify: charts created without explicit colors render with the theme palette, and user-saved colors still override them (spec `chart-theme`) + +## 2. HiDPI rendering + +- [x] 2.1 Add a `ChartUtils.createBackingImage(Canvas, Rectangle)` helper that creates a logical-size offscreen image via `new Image(display, width, height)` (SWT 4.31 auto-scales it to native pixels), records the creation zoom from the canvas's monitor (`Monitor.getZoom()`, falling back to `display.getDPI().x / 72`), and exposes whether the image needs recreating when the zoom differs +- [x] 2.2 Migrate the main offscreen image in `ChartCanvas.onPaint` to the helper (dispose and recreate on resize or zoom change, as today) +- [x] 2.3 Migrate the date scale canvas and vertical scale canvas image creation to the same helper (recreate on zoom change too) +- [x] 2.4 Enable shape and text antialiasing in the `Graphics` constructor (spec `chart-rendering` — Antialiased drawing) +- [ ] 2.5 Verify charts render crisply at 100%, 150%, and 200% display zoom (simulate with `GDK_SCALE`) with no blur and no layout regression, and that a zoom change after image creation recreates the offscreen image (spec `chart-rendering` — Device-zoom-aware rendering) + +## 3. Downsampling and geometry caching + +- [x] 3.1 Add `OHLCDownsampler` with min/max binning per pixel column (preserving high/low, first-open/last-close for candles) returning an aggregated `IOHLC[]` +- [x] 3.2 Add the scalar equivalent for line/area/histogram renderers +- [x] 3.3 Wire downsampling into `setDataBounds`: when visible points exceed the chart width, aggregate before geometry build; cache keyed by (firstDate, lastDate, width) and cleared on `invalidate()` (spec `chart-rendering` — Geometry caching) +- [x] 3.4 Apply to `CandleStickChart`, `BarChart`, `OHLCLineChart`, `HistogramAreaChart`, `HistogramBarChart`, and `LineChart` (spec `chart-rendering` — Downsampling of large series) +- [ ] 3.5 Verify with a large history: zoomed-out draws at most one element per pixel column, pan/zoom remain responsive, and zoom-in restores full per-tick detail + +## 4. Verification + +- [ ] 4.1 Add pure-function unit tests for `OHLCDownsampler` (extreme preservation, one-per-column bound, boundary at width == points) and wire `org.eclipsetrader.ui.tests` into the Maven reactor so they run in GitHub Actions CI +- [ ] 4.2 Build the product with `mvn package` (the existing `.github/workflows/maven.yml` job on JDK 21) and confirm `org.eclipsetrader.ui` and `org.eclipsetrader.ui.charts.indicators` compile +- [ ] 4.3 Extend the Codespaces devcontainer (`.devcontainer/devcontainer.json`) with a virtual display (e.g., desktop-lite/VNC or Xvfb) and GTK so the SWT product can run headless +- [ ] 4.4 Visually verify summary bar, crosshair, chart export-to-image, and indicator rendering at 100%/150%/200% zoom (simulate with `GDK_SCALE`) using small and large histories diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 000000000..c4d34acea --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,32 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours + +# Per-operation guidance (optional) +# Add advisory guidance for how apply and archive work should be conducted. +# This is separate from artifact rules above. +# Example: +# operations: +# apply: +# guidance: +# - Keep test summaries concise +# archive: +# guidance: +# - Summarize the archive outcome before finishing diff --git a/org.eclipsetrader.core.modern.tests/src/org/eclipsetrader/core/charts/OHLCDownsamplerModernTest.java b/org.eclipsetrader.core.modern.tests/src/org/eclipsetrader/core/charts/OHLCDownsamplerModernTest.java new file mode 100644 index 000000000..3295531a6 --- /dev/null +++ b/org.eclipsetrader.core.modern.tests/src/org/eclipsetrader/core/charts/OHLCDownsamplerModernTest.java @@ -0,0 +1,126 @@ +package org.eclipsetrader.core.charts; + +import org.eclipsetrader.core.feed.IOHLC; +import org.eclipsetrader.core.feed.OHLC; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import java.util.Date; + +@RunWith(JUnitPlatform.class) +public class OHLCDownsamplerModernTest { + + @Test + void testNullInput() { + IOHLC[] result = OHLCDownsampler.downsample((IOHLC[]) null, 10); + Assertions.assertNotNull(result); + Assertions.assertEquals(0, result.length); + } + + @Test + void testEmptyInput() { + IOHLC[] result = OHLCDownsampler.downsample(new IOHLC[0], 10); + Assertions.assertEquals(0, result.length); + } + + @Test + void testWidthZeroOrNegativeReturnsSource() { + IOHLC[] source = new IOHLC[] { ohlc(0, 10.0, 11.0, 9.0, 10.5) }; + Assertions.assertSame(source, OHLCDownsampler.downsample(source, 0)); + Assertions.assertSame(source, OHLCDownsampler.downsample(source, -1)); + } + + @Test + void testWidthExceedsValuesReturnsSource() { + IOHLC[] source = new IOHLC[] { + ohlc(0, 10.0, 11.0, 9.0, 10.5), + ohlc(1, 11.0, 12.0, 10.0, 11.5), + }; + IOHLC[] result = OHLCDownsampler.downsample(source, 3); + Assertions.assertSame(source, result); + } + + @Test + void testWidthEqualsValuesReturnsSource() { + IOHLC[] source = new IOHLC[] { + ohlc(0, 10.0, 11.0, 9.0, 10.5), + ohlc(1, 11.0, 12.0, 10.0, 11.5), + }; + IOHLC[] result = OHLCDownsampler.downsample(source, 2); + Assertions.assertSame(source, result); + } + + @Test + void testBoundaryOnePerColumn() { + IOHLC[] source = new IOHLC[100]; + for (int i = 0; i < 100; i++) { + source[i] = ohlc(i, 10.0 + i, 20.0 + i, 5.0 + i, 15.0 + i); + } + IOHLC[] result = OHLCDownsampler.downsample(source, 7); + Assertions.assertEquals(7, result.length); + } + + @Test + void testPreservesExtremes() { + Date d1 = new Date(0); + Date d2 = new Date(1); + Date d3 = new Date(2); + Date d4 = new Date(3); + + IOHLC[] source = new IOHLC[] { + new OHLC(d1, 10.0, 15.0, 9.0, 12.0, null), + new OHLC(d2, 12.0, 20.0, 8.0, 14.0, null), + new OHLC(d3, 14.0, 18.0, 10.0, 13.0, null), + new OHLC(d4, 13.0, 16.0, 11.0, 15.0, null), + }; + IOHLC[] result = OHLCDownsampler.downsample(source, 2); + + Assertions.assertEquals(2, result.length); + + Assertions.assertEquals(d1, result[0].getDate()); + Assertions.assertEquals(10.0, result[0].getOpen(), 0.001); + Assertions.assertEquals(20.0, result[0].getHigh(), 0.001); + Assertions.assertEquals(8.0, result[0].getLow(), 0.001); + Assertions.assertEquals(14.0, result[0].getClose(), 0.001); + + Assertions.assertEquals(d3, result[1].getDate()); + Assertions.assertEquals(14.0, result[1].getOpen(), 0.001); + Assertions.assertEquals(18.0, result[1].getHigh(), 0.001); + Assertions.assertEquals(10.0, result[1].getLow(), 0.001); + Assertions.assertEquals(15.0, result[1].getClose(), 0.001); + } + + @Test + void testSingleValuePerBin() { + IOHLC[] source = new IOHLC[] { + ohlc(0, 10.0, 10.5, 9.5, 10.2), + ohlc(1, 11.0, 11.5, 10.5, 11.2), + ohlc(2, 12.0, 12.5, 11.5, 12.2), + }; + IOHLC[] result = OHLCDownsampler.downsample(source, 3); + Assertions.assertEquals(3, result.length); + Assertions.assertEquals(10.0, result[0].getOpen(), 0.001); + Assertions.assertEquals(12.2, result[2].getClose(), 0.001); + } + + @Test + void testOddBinningLastColumnSmaller() { + IOHLC[] source = new IOHLC[5]; + for (int i = 0; i < 5; i++) { + source[i] = ohlc(i, 10.0 + i, 15.0 + i, 9.0 + i, 12.0 + i); + } + IOHLC[] result = OHLCDownsampler.downsample(source, 2); + Assertions.assertEquals(2, result.length); + Assertions.assertEquals(10.0, result[0].getOpen(), 0.001); + } + + private static IOHLC ohlc(long time, double open, double high, double low, double close) { + return new OHLC(new Date(time), open, high, low, close, null); + } + + private static IOHLC ohlc(int time, double open, double high, double low, double close) { + return ohlc((long) time, open, high, low, close); + } +} diff --git a/org.eclipsetrader.core.modern.tests/src/org/eclipsetrader/core/charts/ScalarDownsamplerModernTest.java b/org.eclipsetrader.core.modern.tests/src/org/eclipsetrader/core/charts/ScalarDownsamplerModernTest.java new file mode 100644 index 000000000..43e09b6f7 --- /dev/null +++ b/org.eclipsetrader.core.modern.tests/src/org/eclipsetrader/core/charts/ScalarDownsamplerModernTest.java @@ -0,0 +1,117 @@ +package org.eclipsetrader.core.charts; + +import org.eclipse.core.runtime.IAdaptable; +import org.eclipsetrader.core.charts.NumberValue; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import java.util.Date; + +@RunWith(JUnitPlatform.class) +public class ScalarDownsamplerModernTest { + + @Test + void testNullInput() { + IAdaptable[] result = ScalarDownsampler.downsample(null, 10); + Assertions.assertNotNull(result); + Assertions.assertEquals(0, result.length); + } + + @Test + void testEmptyInput() { + IAdaptable[] result = ScalarDownsampler.downsample(new IAdaptable[0], 10); + Assertions.assertEquals(0, result.length); + } + + @Test + void testWidthZeroOrNegativeReturnsSource() { + IAdaptable[] source = new IAdaptable[] { nv(0, 10.0) }; + Assertions.assertSame(source, ScalarDownsampler.downsample(source, 0)); + Assertions.assertSame(source, ScalarDownsampler.downsample(source, -1)); + } + + @Test + void testWidthExceedsValuesReturnsSource() { + IAdaptable[] source = new IAdaptable[] { nv(0, 10.0), nv(1, 20.0) }; + IAdaptable[] result = ScalarDownsampler.downsample(source, 5); + Assertions.assertSame(source, result); + } + + @Test + void testWidthEqualsValuesReturnsSource() { + IAdaptable[] source = new IAdaptable[] { nv(0, 10.0), nv(1, 20.0) }; + IAdaptable[] result = ScalarDownsampler.downsample(source, 2); + Assertions.assertSame(source, result); + } + + @Test + void testBoundaryOnePerColumn() { + IAdaptable[] source = new IAdaptable[100]; + for (int i = 0; i < 100; i++) { + source[i] = nv(i, (double) i); + } + IAdaptable[] result = ScalarDownsampler.downsample(source, 13); + Assertions.assertEquals(13, result.length); + } + + @Test + void testPicksMaxDeviationPoint() { + Date d1 = new Date(0); + Date d2 = new Date(1); + Date d3 = new Date(2); + Date d4 = new Date(3); + + IAdaptable[] source = new IAdaptable[] { + new NumberValue(d1, 10.0), + new NumberValue(d2, 11.0), + new NumberValue(d3, 100.0), + new NumberValue(d4, 12.0), + }; + IAdaptable[] result = ScalarDownsampler.downsample(source, 2); + + Assertions.assertEquals(2, result.length); + Assertions.assertEquals(10.0, ((Number) result[0].getAdapter(Number.class)).doubleValue(), 0.001); + Assertions.assertEquals(100.0, ((Number) result[1].getAdapter(Number.class)).doubleValue(), 0.001); + } + + @Test + void testPreservesDateAndNumberAdapter() { + Date date = new Date(42); + IAdaptable[] source = new IAdaptable[] { + new NumberValue(date, 10.0), + new NumberValue(new Date(43), 11.0), + new NumberValue(new Date(44), 12.0), + new NumberValue(new Date(45), 13.0), + new NumberValue(new Date(46), 14.0), + }; + IAdaptable[] result = ScalarDownsampler.downsample(source, 2); + + Assertions.assertEquals(2, result.length); + for (int i = 0; i < result.length; i++) { + Assertions.assertNotNull(result[i].getAdapter(Date.class), "column " + i + " should adapt to Date"); + Assertions.assertNotNull(result[i].getAdapter(Number.class), "column " + i + " should adapt to Number"); + } + } + + @Test + void testEqualDeviationPicksFirst() { + IAdaptable[] source = new IAdaptable[] { + new NumberValue(new Date(0), 0.0), + new NumberValue(new Date(1), 100.0), + new NumberValue(new Date(2), 0.0), + }; + IAdaptable[] result = ScalarDownsampler.downsample(source, 1); + Assertions.assertEquals(1, result.length); + Assertions.assertEquals(100.0, ((Number) result[0].getAdapter(Number.class)).doubleValue(), 0.001); + } + + private static IAdaptable nv(long time, double value) { + return new NumberValue(new Date(time), value); + } + + private static IAdaptable nv(int time, double value) { + return nv((long) time, value); + } +} diff --git a/org.eclipsetrader.core/plugin.xml b/org.eclipsetrader.core/plugin.xml index 67a5c07ba..603132e7f 100644 --- a/org.eclipsetrader.core/plugin.xml +++ b/org.eclipsetrader.core/plugin.xml @@ -45,6 +45,13 @@ id="org.eclipsetrader.core.internal.repositories.DefaultElementFactory" name="Default Element Factory"> - + + + + + + diff --git a/org.eclipsetrader.core/src/org/eclipsetrader/core/charts/OHLCDownsampler.java b/org.eclipsetrader.core/src/org/eclipsetrader/core/charts/OHLCDownsampler.java new file mode 100644 index 000000000..cab30a6fa --- /dev/null +++ b/org.eclipsetrader.core/src/org/eclipsetrader/core/charts/OHLCDownsampler.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2004-2011 Marco Maccaferri and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Marco Maccaferri - initial API and implementation + */ + +package org.eclipsetrader.core.charts; + +import java.util.Date; + +import org.eclipse.core.runtime.IAdaptable; +import org.eclipsetrader.core.feed.IOHLC; +import org.eclipsetrader.core.feed.OHLC; + +/** + * Aggregates OHLC values into one bin per pixel column so that zoomed-out + * charts render at most one element per column while preserving each column's + * high, low, first open and last close. + */ +public final class OHLCDownsampler { + + private OHLCDownsampler() { + } + + /** + * Aggregates the given values into at most width bins. + * + * @param values the source values. + * @param width the maximum number of bins (pixel columns). + * @return the aggregated values, or the source array if no aggregation is needed. + */ + public static IOHLC[] downsample(IOHLC[] values, int width) { + if (values == null || values.length == 0) { + return new IOHLC[0]; + } + if (width <= 0 || values.length <= width) { + return values; + } + + IOHLC[] result = new IOHLC[width]; + for (int column = 0; column < width; column++) { + int start = (int) ((long) column * values.length / width); + int end = (int) ((long) (column + 1) * values.length / width); + if (end <= start) { + end = start + 1; + } + if (end > values.length) { + end = values.length; + } + + IOHLC first = values[start]; + IOHLC last = values[end - 1]; + Double high = first.getHigh(); + Double low = first.getLow(); + for (int i = start + 1; i < end; i++) { + if (values[i].getHigh() != null && (high == null || values[i].getHigh() > high)) { + high = values[i].getHigh(); + } + if (values[i].getLow() != null && (low == null || values[i].getLow() < low)) { + low = values[i].getLow(); + } + } + result[column] = new OHLC(first.getDate(), first.getOpen(), high, low, last.getClose(), null); + } + return result; + } + + /** + * Aggregates the given adaptable OHLC values into at most width + * bins, returning values that adapt to IOHLC, Date + * and Number. + * + * @param values the source values. + * @param width the maximum number of bins (pixel columns). + * @return the aggregated values, or the source array if no aggregation is needed. + */ + public static IAdaptable[] downsample(IAdaptable[] values, int width) { + if (values == null || values.length == 0) { + return new IAdaptable[0]; + } + if (width <= 0 || values.length <= width) { + return values; + } + + IOHLC[] source = new IOHLC[values.length]; + int count = 0; + for (int i = 0; i < values.length; i++) { + IOHLC ohlc = (IOHLC) values[i].getAdapter(IOHLC.class); + if (ohlc != null) { + source[count++] = ohlc; + } + } + if (count == 0) { + return new IAdaptable[0]; + } + if (count < source.length) { + IOHLC[] compact = new IOHLC[count]; + System.arraycopy(source, 0, compact, 0, count); + source = compact; + } + + IOHLC[] aggregated = downsample(source, width); + + IAdaptable[] result = new IAdaptable[aggregated.length]; + for (int i = 0; i < aggregated.length; i++) { + result[i] = new Value(aggregated[i]); + } + return result; + } + + private static class Value implements IAdaptable { + + private final IOHLC ohlc; + + public Value(IOHLC ohlc) { + this.ohlc = ohlc; + } + + /* (non-Javadoc) + * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class) + */ + @Override + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + public Object getAdapter(Class adapter) { + if (ohlc != null && adapter.isAssignableFrom(ohlc.getClass())) { + return ohlc; + } + if (adapter.isAssignableFrom(Date.class)) { + return ohlc != null ? ohlc.getDate() : null; + } + if (adapter.isAssignableFrom(Double.class) || adapter.isAssignableFrom(Number.class)) { + return ohlc != null ? ohlc.getClose() : null; + } + return null; + } + } +} diff --git a/org.eclipsetrader.core/src/org/eclipsetrader/core/charts/ScalarDownsampler.java b/org.eclipsetrader.core/src/org/eclipsetrader/core/charts/ScalarDownsampler.java new file mode 100644 index 000000000..596f34b98 --- /dev/null +++ b/org.eclipsetrader.core/src/org/eclipsetrader/core/charts/ScalarDownsampler.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2004-2011 Marco Maccaferri and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Marco Maccaferri - initial API and implementation + */ + +package org.eclipsetrader.core.charts; + +import java.util.Date; + +import org.eclipse.core.runtime.IAdaptable; +import org.eclipsetrader.core.charts.NumberValue; + +/** + * Aggregates scalar (date, number) values into one bin per pixel column so that + * zoomed-out charts render at most one element per column. Each bin keeps the + * value with the greatest deviation from the column mean, which preserves + * spikes in the rendered line, area or histogram. + */ +public final class ScalarDownsampler { + + private ScalarDownsampler() { + } + + /** + * Aggregates the given values into at most width bins. + * + * @param values the source values, adapting to Date and Number. + * @param width the maximum number of bins (pixel columns). + * @return the aggregated values, or the source array if no aggregation is needed. + */ + public static IAdaptable[] downsample(IAdaptable[] values, int width) { + if (values == null || values.length == 0) { + return new IAdaptable[0]; + } + if (width <= 0 || values.length <= width) { + return values; + } + + IAdaptable[] result = new IAdaptable[width]; + for (int column = 0; column < width; column++) { + int start = (int) ((long) column * values.length / width); + int end = (int) ((long) (column + 1) * values.length / width); + if (end <= start) { + end = start + 1; + } + if (end > values.length) { + end = values.length; + } + + double sum = 0.0; + int count = 0; + for (int i = start; i < end; i++) { + Number number = (Number) values[i].getAdapter(Number.class); + if (number != null) { + sum += number.doubleValue(); + count++; + } + } + double mean = count != 0 ? sum / count : 0.0; + + int representative = -1; + double bestDeviation = -1.0; + for (int i = start; i < end; i++) { + Number number = (Number) values[i].getAdapter(Number.class); + if (number == null) { + continue; + } + double deviation = Math.abs(number.doubleValue() - mean); + if (representative == -1 || deviation > bestDeviation) { + representative = i; + bestDeviation = deviation; + } + } + if (representative == -1) { + representative = end - 1; + } + + IAdaptable value = values[representative]; + Date date = (Date) value.getAdapter(Date.class); + Number number = (Number) value.getAdapter(Number.class); + result[column] = new NumberValue(date, number); + } + return result; + } +} diff --git a/org.eclipsetrader.core/src/org/eclipsetrader/core/internal/PreferenceInitializer.java b/org.eclipsetrader.core/src/org/eclipsetrader/core/internal/PreferenceInitializer.java new file mode 100644 index 000000000..6fd6e70db --- /dev/null +++ b/org.eclipsetrader.core/src/org/eclipsetrader/core/internal/PreferenceInitializer.java @@ -0,0 +1,17 @@ +package org.eclipsetrader.core.internal; + +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; +import org.eclipse.core.runtime.preferences.DefaultScope; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipsetrader.core.internal.CoreActivator; + +public class PreferenceInitializer extends AbstractPreferenceInitializer { + + private static final String JESSX_STREAMING_CONNECTOR = "org.eclipsetrader.jessx.connector"; + + @Override + public void initializeDefaultPreferences() { + IEclipsePreferences node = DefaultScope.INSTANCE.getNode(CoreActivator.PLUGIN_ID); + node.put(CoreActivator.DEFAULT_CONNECTOR_ID, JESSX_STREAMING_CONNECTOR); + } +} diff --git a/org.eclipsetrader.jessx/src/org/eclipsetrader/jessx/internal/JessxActivator.java b/org.eclipsetrader.jessx/src/org/eclipsetrader/jessx/internal/JessxActivator.java index 323812c3a..ab7c3e471 100644 --- a/org.eclipsetrader.jessx/src/org/eclipsetrader/jessx/internal/JessxActivator.java +++ b/org.eclipsetrader.jessx/src/org/eclipsetrader/jessx/internal/JessxActivator.java @@ -407,7 +407,7 @@ private void preRegisterJessxSecurities() { log("JESSX security pre-registration complete"); } } - } catch (Exception e) { + } catch (Throwable e) { IStatus status = new Status(IStatus.WARNING, PLUGIN_ID, "Could not pre-register JESSX securities. Charts may fail to initialize.", e); getLog().log(status); @@ -415,7 +415,14 @@ private void preRegisterJessxSecurities() { } private void populateTickersView() { - org.eclipse.swt.widgets.Display.getDefault().asyncExec(new Runnable() { + org.eclipse.swt.widgets.Display display; + try { + display = org.eclipse.swt.widgets.Display.getDefault(); + } catch (Throwable t) { + log("Headless environment detected; skipping Tickers view population."); + return; + } + display.asyncExec(new Runnable() { public void run() { try { // Access UIActivator safely diff --git a/org.eclipsetrader.market.sim-feature/feature.xml b/org.eclipsetrader.market.sim-feature/feature.xml new file mode 100644 index 000000000..4daef3508 --- /dev/null +++ b/org.eclipsetrader.market.sim-feature/feature.xml @@ -0,0 +1,26 @@ + + + + + Adaptive market simulation engine. + + + + [Enter Copyright Description here.] + + + + [Enter License Description here.] + + + + + diff --git a/org.eclipsetrader.market.sim-feature/pom.xml b/org.eclipsetrader.market.sim-feature/pom.xml new file mode 100644 index 000000000..22478257e --- /dev/null +++ b/org.eclipsetrader.market.sim-feature/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + org.eclipsetrader + eclipsetrader-parent + 1.0.0-SNAPSHOT + + org.eclipsetrader + org.eclipsetrader.market.sim-feature + 1.0.0-SNAPSHOT + eclipse-feature + diff --git a/org.eclipsetrader.market.sim.tests/META-INF/MANIFEST.MF b/org.eclipsetrader.market.sim.tests/META-INF/MANIFEST.MF new file mode 100644 index 000000000..3a4b64bc0 --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/META-INF/MANIFEST.MF @@ -0,0 +1,10 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Market Simulator Tests +Bundle-SymbolicName: org.eclipsetrader.market.sim.tests +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: EclipseTrader.org +Require-Bundle: org.eclipsetrader.market.sim, + org.junit +Bundle-ClassPath: ., lib/junit-4.13.2.jar, lib/junit-jupiter-api-5.10.2.jar, lib/junit-jupiter-engine-5.10.2.jar, lib/junit-platform-runner-1.10.2.jar, lib/junit-platform-suite-api-1.10.2.jar, lib/junit-platform-suite-commons-1.10.2.jar, lib/junit-platform-launcher-1.10.2.jar, lib/junit-platform-engine-1.10.2.jar, lib/junit-platform-commons-1.10.2.jar, lib/opentest4j-1.3.0.jar +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/org.eclipsetrader.market.sim.tests/build.properties b/org.eclipsetrader.market.sim.tests/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.eclipsetrader.market.sim.tests/pom.xml b/org.eclipsetrader.market.sim.tests/pom.xml new file mode 100644 index 000000000..60ce733ef --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/pom.xml @@ -0,0 +1,136 @@ + + 4.0.0 + + org.eclipsetrader + eclipsetrader-parent + 1.0.0-SNAPSHOT + + org.eclipsetrader.market.sim.tests + eclipse-test-plugin + org.eclipsetrader.market.sim.tests + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.0 + + + copy-market-sim-test-libs + generate-resources + + copy + + + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + ${project.basedir}/lib + junit-jupiter-api-5.10.2.jar + + + org.junit.jupiter + junit-jupiter-engine + 5.10.2 + ${project.basedir}/lib + junit-jupiter-engine-5.10.2.jar + + + junit + junit + 4.13.2 + ${project.basedir}/lib + junit-4.13.2.jar + + + org.junit.platform + junit-platform-runner + 1.10.2 + ${project.basedir}/lib + junit-platform-runner-1.10.2.jar + + + org.junit.platform + junit-platform-suite-api + 1.10.2 + ${project.basedir}/lib + junit-platform-suite-api-1.10.2.jar + + + org.junit.platform + junit-platform-suite-commons + 1.10.2 + ${project.basedir}/lib + junit-platform-suite-commons-1.10.2.jar + + + org.junit.platform + junit-platform-launcher + 1.10.2 + ${project.basedir}/lib + junit-platform-launcher-1.10.2.jar + + + org.junit.platform + junit-platform-engine + 1.10.2 + ${project.basedir}/lib + junit-platform-engine-1.10.2.jar + + + org.junit.platform + junit-platform-commons + 1.10.2 + ${project.basedir}/lib + junit-platform-commons-1.10.2.jar + + + org.opentest4j + opentest4j + 1.3.0 + ${project.basedir}/lib + opentest4j-1.3.0.jar + + + + + + + + org.eclipse.tycho + tycho-surefire-plugin + ${tycho.version} + + + default-test + test + + test + + + + + false + junit4 + testProbeFirst + + **/*Test.class + + + + org.junit + org.junit + 3.8.2 + eclipse-plugin + + + org.junit + + + + + diff --git a/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/AdaptationTest.java b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/AdaptationTest.java new file mode 100644 index 000000000..af6330865 --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/AdaptationTest.java @@ -0,0 +1,60 @@ +package org.eclipsetrader.market.sim.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipsetrader.market.sim.agent.FundamentalValue; +import org.eclipsetrader.market.sim.agent.MarketSimulator; +import org.eclipsetrader.market.sim.agent.NewsEvent; +import org.eclipsetrader.market.sim.agent.TrendEstimator; +import org.eclipsetrader.market.sim.engine.Exchange; +import org.junit.jupiter.api.Test; + +public class AdaptationTest { + + @Test + void newsMovesFundamentalValue() { + FundamentalValue fv = new FundamentalValue(); + fv.set("X", 100.0); + fv.applyNews(new NewsEvent("X", 1, 0.05, 0)); + assertEquals(105.0, fv.get("X"), 0.0001); + } + + @Test + void trendEstimatorDetectsDirection() { + TrendEstimator up = new TrendEstimator(10, 0.01, 0.0, 100.0); + for (double p = 100.0; p <= 110.0; p += 1.0) { + up.addPrice(p); + } + assertTrue(up.estimate() > 0); + + TrendEstimator down = new TrendEstimator(10, 0.01, 0.0, 100.0); + for (double p = 100.0; p >= 90.0; p -= 1.0) { + down.addPrice(p); + } + assertTrue(down.estimate() < 0); + } + + @Test + void tradesOccurWithoutPlayerAction() { + MarketSimulator sim = new MarketSimulator(42L, 1_800_000_000_000L, 5_000L, 0.02); + sim.addAsset("X", 100.0); + for (int i = 0; i < 200; i++) { + sim.step(); + } + Exchange ex = sim.getExchange(); + assertTrue(ex.getBook("X").getTrades().size() > 0); + } + + @Test + void newsMovesSimulatedPrice() { + MarketSimulator sim = new MarketSimulator(7L, 1_800_000_000_000L, 2_000L, 0.02); + sim.addAsset("X", 100.0); + // Drive enough steps to generate news and trades. + for (int i = 0; i < 500; i++) { + sim.step(); + } + double last = sim.getExchange().getBook("X").getLast(); + assertTrue(last > 0.0); + } +} diff --git a/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/CalendarTest.java b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/CalendarTest.java new file mode 100644 index 000000000..438dd6a80 --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/CalendarTest.java @@ -0,0 +1,75 @@ +package org.eclipsetrader.market.sim.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Calendar; + +import org.eclipsetrader.market.sim.clock.SimulatedClock; +import org.eclipsetrader.market.sim.clock.TradingCalendar; +import org.eclipsetrader.market.sim.engine.Exchange; +import org.eclipsetrader.market.sim.engine.OrderType; +import org.eclipsetrader.market.sim.engine.Side; +import org.eclipsetrader.market.sim.engine.Trade; +import org.junit.jupiter.api.Test; + +public class CalendarTest { + + private static final long AFTER_YEAR_2000 = 946684800000L; + + private long startOfDay(int day) { + Calendar c = Calendar.getInstance(); + c.set(2026, 0, day, 9, 30, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTimeInMillis(); + } + + @Test + void simulatedClockIsNotEpoch() { + SimulatedClock clock = new SimulatedClock(startOfDay(5)); + assertTrue(clock.now() > AFTER_YEAR_2000); + } + + @Test + void advanceToNextDayOpenMovesDay() { + SimulatedClock clock = new SimulatedClock(startOfDay(5)); + int before = clock.dayOfYear(); + clock.advanceToNextDayOpen(9, 30); + assertTrue(clock.dayOfYear() != before); + } + + @Test + void dayBoundaryClosesAndReopens() { + SimulatedClock clock = new SimulatedClock(startOfDay(5)); + TradingCalendar calendar = new TradingCalendar(clock, 9, 30, 16, 0); + calendar.open(); + assertTrue(calendar.isOpen()); + + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(clock.now()); + c.set(Calendar.HOUR_OF_DAY, 16); + c.set(Calendar.MINUTE, 0); + clock.setNow(c.getTimeInMillis()); + assertTrue(calendar.isEndOfDay()); + + calendar.close(); + assertFalse(calendar.isOpen()); + + calendar.advanceDay(); + assertTrue(calendar.isOpen()); + assertEquals(2, calendar.getDay()); + } + + @Test + void tradeTimestampIsSimulatedDate() { + Exchange ex = new Exchange(); + long start = startOfDay(5); + ex.setNow(start); + ex.enterOrder("X", Side.SELL, OrderType.LIMIT, 100, 10, "S"); + ex.enterOrder("X", Side.BUY, OrderType.LIMIT, 100, 10, "B"); + Trade trade = ex.getBook("X").getTrades().get(0); + assertEquals(start, trade.getTimestamp()); + assertTrue(trade.getTimestamp() > AFTER_YEAR_2000); + } +} diff --git a/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/JupiterSuiteTest.java b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/JupiterSuiteTest.java new file mode 100644 index 000000000..34960af08 --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/JupiterSuiteTest.java @@ -0,0 +1,10 @@ +package org.eclipsetrader.market.sim.tests; + +import org.junit.platform.runner.JUnitPlatform; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.runner.RunWith; + +@RunWith(JUnitPlatform.class) +@SelectClasses({ MatchingTest.class, PositionsTest.class, LeverageTest.class, AdaptationTest.class, CalendarTest.class }) +public class JupiterSuiteTest { +} diff --git a/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/LeverageTest.java b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/LeverageTest.java new file mode 100644 index 000000000..6c96dd570 --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/LeverageTest.java @@ -0,0 +1,90 @@ +package org.eclipsetrader.market.sim.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipsetrader.market.sim.engine.Exchange; +import org.eclipsetrader.market.sim.engine.OrderType; +import org.eclipsetrader.market.sim.engine.Side; +import org.eclipsetrader.market.sim.risk.Account; +import org.junit.jupiter.api.Test; + +public class LeverageTest { + + @Test + void buyingPowerIsCashTimesLeverage() { + Account account = new Account("P", 100); + account.setLeverage(4); + assertTrue(account.canBuy(400)); + assertFalse(account.canBuy(401)); + } + + @Test + void leveragedBuyMakesCashNegative() { + Account account = new Account("P", 100); + account.setLeverage(4); + account.applyTrade("X", 30, 10.0); + assertEquals(-200.0, account.getCash(), 0.0001); + } + + @Test + void marginRejectsOrderBeyondBuyingPower() { + Exchange ex = new Exchange(); + ex.setNow(1_000_000L); + Account account = new Account("P", 100); + account.setLeverage(4); + ex.addAccount(account); + assertNull(ex.enterOrder("X", Side.BUY, OrderType.LIMIT, 100, 10, "P")); + } + + @Test + void equityReflectsLongAndShort() { + Account account = new Account("P", 1000); + account.applyTrade("X", 10, 10.0); // long 10 @ 10 + account.applyTrade("Y", -5, 20.0); // short 5 @ 20 + Map prices = new HashMap(); + prices.put("X", 10.0); + prices.put("Y", 20.0); + assertEquals(1000.0 + 100.0 - 100.0, account.equity(prices), 0.0001); + } + + @Test + void loanAccruesInterest() { + Account account = new Account("P", 100); + account.setInterestRate(0.05); + account.applyTrade("X", 30, 10.0); // cash -200 + account.accrueInterest(); + assertEquals(-210.0, account.getCash(), 0.0001); + } + + @Test + void marginCallForceLiquidates() { + Exchange ex = new Exchange(); + ex.setNow(1_000_000L); + Account mm = new Account("MM", 1_000_000); + ex.addAccount(mm); + Account account = new Account("P", 100); + account.setLeverage(4); + account.setMaintenanceMargin(0.6); + ex.addAccount(account); + + // Liquidity provider quotes both sides (spread apart so they do not cross + // each other) so the leveraged buy and the later liquidation sell have + // a counterparty. + ex.enterOrder("X", Side.SELL, OrderType.LIMIT, 100, 11, "MM"); + ex.enterOrder("X", Side.BUY, OrderType.LIMIT, 100, 9, "MM"); + + // Buy 20 @ 11 = cost 220, cash becomes -120 (leveraged). + ex.enterOrder("X", Side.BUY, OrderType.LIMIT, 20, 11, "P"); + assertFalse(account.isFlat()); + + // Equity is 100, gross exposure 220, maintenance threshold 132. + ex.checkMargin(); + assertTrue(account.isFlat()); + } +} diff --git a/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/MatchingTest.java b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/MatchingTest.java new file mode 100644 index 000000000..7c6786921 --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/MatchingTest.java @@ -0,0 +1,217 @@ +package org.eclipsetrader.market.sim.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.eclipsetrader.market.sim.engine.Exchange; +import org.eclipsetrader.market.sim.engine.Order; +import org.eclipsetrader.market.sim.engine.OrderBook; +import org.eclipsetrader.market.sim.engine.OrderType; +import org.eclipsetrader.market.sim.engine.PegType; +import org.eclipsetrader.market.sim.engine.Side; +import org.eclipsetrader.market.sim.engine.TimeInForce; +import org.eclipsetrader.market.sim.engine.Trade; +import org.junit.jupiter.api.Test; + +public class MatchingTest { + + private long id; + + private Exchange exchange() { + Exchange ex = new Exchange(); + ex.setNow(1_000_000L); + return ex; + } + + private Order order(Exchange ex, String asset, Side side, OrderType type, long qty, double price, String who) { + return new Order(ex.nextOrderId(), asset, side, type, qty, price, who); + } + + @Test + void priceTimePriorityFullDepthSweep() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + Order a = order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S1"); + Order b = order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 11, "S2"); + book.enter(a, ex); + book.enter(b, ex); + + List trades = book.enter(order(ex, "X", Side.BUY, OrderType.LIMIT, 150, 11, "B"), ex); + + assertEquals(2, trades.size()); + assertEquals(100, a.getCumFilled()); + assertEquals(50, b.getCumFilled()); + assertEquals(10.0, trades.get(0).getPrice()); + assertEquals(11.0, trades.get(1).getPrice()); + } + + @Test + void samePriceTradesInArrivalOrder() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + Order first = order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S1"); + Order second = order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S2"); + book.enter(first, ex); + book.enter(second, ex); + + book.enter(order(ex, "X", Side.BUY, OrderType.LIMIT, 150, 10, "B"), ex); + + assertEquals(100, first.getCumFilled()); + assertEquals(50, second.getCumFilled()); + } + + @Test + void marketOrderSweepsAndCancelsRemainder() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + book.enter(order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S"), ex); + + Order buy = order(ex, "X", Side.BUY, OrderType.MARKET, 150, 0, "B"); + List trades = book.enter(buy, ex); + + assertEquals(1, trades.size()); + assertFalse(buy.isFilled()); + assertTrue(book.getBids().isEmpty()); + } + + @Test + void limitRemainderRests() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + Order buy = order(ex, "X", Side.BUY, OrderType.LIMIT, 50, 10, "B"); + book.enter(buy, ex); + assertTrue(book.getBids().contains(buy)); + } + + @Test + void cancelRemovesRestingOrder() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + Order buy = order(ex, "X", Side.BUY, OrderType.LIMIT, 50, 10, "B"); + book.enter(buy, ex); + assertTrue(book.cancel(buy.getId())); + assertTrue(book.getBids().isEmpty()); + } + + @Test + void amendPriceRequeues() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + Order buy = order(ex, "X", Side.BUY, OrderType.LIMIT, 50, 10, "B"); + book.enter(buy, ex); + assertTrue(book.amendPrice(buy.getId(), 11)); + assertEquals(11.0, book.getBestBid()); + } + + @Test + void iocPartialFillCancelsRemainder() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + book.enter(order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S"), ex); + + Order buy = order(ex, "X", Side.BUY, OrderType.LIMIT, 150, 11, "B"); + buy.setTimeInForce(TimeInForce.IOC); + List trades = book.enter(buy, ex); + + assertEquals(1, trades.size()); + assertTrue(book.getBids().isEmpty()); + assertEquals(50, buy.getRemaining()); + } + + @Test + void fokCancelsWhenNotFullyFillable() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + book.enter(order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S"), ex); + + Order buy = order(ex, "X", Side.BUY, OrderType.LIMIT, 150, 11, "B"); + buy.setTimeInForce(TimeInForce.FOK); + List trades = book.enter(buy, ex); + + assertTrue(trades.isEmpty()); + assertTrue(book.getBids().isEmpty()); + } + + @Test + void fokFillsWhenFullyFillable() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + book.enter(order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S"), ex); + + Order buy = order(ex, "X", Side.BUY, OrderType.LIMIT, 50, 11, "B"); + buy.setTimeInForce(TimeInForce.FOK); + List trades = book.enter(buy, ex); + + assertEquals(1, trades.size()); + assertTrue(buy.isFilled()); + } + + @Test + void icebergReplenishesDisplayQuantity() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + Order sell = order(ex, "X", Side.SELL, OrderType.LIMIT, 200, 10, "S"); + sell.setIceberg(50); + book.enter(sell, ex); + + List trades = book.enter(order(ex, "X", Side.BUY, OrderType.LIMIT, 120, 10, "B"), ex); + + assertEquals(3, trades.size()); + assertEquals(80, sell.getRemaining()); + } + + @Test + void peggedOrderRepricesWithBook() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + book.enter(order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 10, "S"), ex); + + Order peg = order(ex, "X", Side.BUY, OrderType.LIMIT, 10, 0, "B"); + peg.setPegType(PegType.BEST_ASK); + peg.setPegOffset(-0.10); + book.enter(peg, ex); + assertEquals(9.90, peg.getPrice(), 0.0001); + + book.enter(order(ex, "X", Side.SELL, OrderType.LIMIT, 100, 9.95, "S2"), ex); + assertEquals(9.85, peg.getPrice(), 0.0001); + } + + @Test + void stopOrderTriggersAndBecomesMarketOrder() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + book.enter(order(ex, "X", Side.BUY, OrderType.LIMIT, 100, 90, "B"), ex); + + Order stop = order(ex, "X", Side.SELL, OrderType.STOP, 100, 0, "S"); + stop.setStopPrice(90); + book.enter(stop, ex); + assertTrue(book.getStops().contains(stop)); + + book.checkStops(89, ex); + + assertEquals(1, book.getTrades().size()); + assertTrue(book.getStops().isEmpty()); + } + + @Test + void trailingStopFollowsMarketAndTriggersOnReversal() { + Exchange ex = exchange(); + OrderBook book = ex.getBook("X"); + book.enter(order(ex, "X", Side.BUY, OrderType.LIMIT, 100, 98, "B"), ex); + + Order stop = order(ex, "X", Side.SELL, OrderType.TRAILING_STOP, 100, 0, "S"); + stop.setStopPrice(100); + stop.setTrailingOffset(2); + book.enter(stop, ex); + + book.checkStops(102, ex); + assertTrue(book.getStops().contains(stop)); + + book.checkStops(100, ex); + assertTrue(book.getStops().isEmpty()); + assertEquals(1, book.getTrades().size()); + } +} diff --git a/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/PositionsTest.java b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/PositionsTest.java new file mode 100644 index 000000000..2139b7afc --- /dev/null +++ b/org.eclipsetrader.market.sim.tests/src/org/eclipsetrader/market/sim/tests/PositionsTest.java @@ -0,0 +1,79 @@ +package org.eclipsetrader.market.sim.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.eclipsetrader.market.sim.engine.Exchange; +import org.eclipsetrader.market.sim.engine.OrderType; +import org.eclipsetrader.market.sim.engine.Side; +import org.eclipsetrader.market.sim.risk.Account; +import org.junit.jupiter.api.Test; + +public class PositionsTest { + + @Test + void buyOpensLongPosition() { + Account account = new Account("P", 1000); + account.applyTrade("X", 100, 10.0); + assertEquals(100, account.getPosition("X")); + } + + @Test + void sellWithoutHoldingsOpensShort() { + Account account = new Account("P", 1000); + account.applyTrade("X", -100, 10.0); + assertEquals(-100, account.getPosition("X")); + } + + @Test + void buyCoversShortBeforeOpeningLong() { + Account account = new Account("P", 1000); + account.applyTrade("X", -100, 10.0); + account.applyTrade("X", 150, 10.0); + assertEquals(50, account.getPosition("X")); + } + + @Test + void sellFromLongReducesPosition() { + Account account = new Account("P", 1000); + account.applyTrade("X", 100, 10.0); + account.applyTrade("X", -60, 10.0); + assertEquals(40, account.getPosition("X")); + } + + @Test + void shortLimitRejectsExcessSelling() { + Account account = new Account("P", 1000); + account.setShortLimit(50); + account.applyTrade("X", -50, 10.0); + assertEquals(-50, account.getPosition("X")); + assertEquals(false, account.canSell("X", 1)); + } + + @Test + void exchangeRejectsOrderBeyondShortLimit() { + Exchange ex = new Exchange(); + ex.setNow(1_000_000L); + Account account = new Account("P", 1000); + account.setShortLimit(50); + ex.addAccount(account); + + assertNull(ex.enterOrder("X", Side.SELL, OrderType.LIMIT, 100, 10, "P")); + } + + @Test + void tradeUpdatesBothSides() { + Exchange ex = new Exchange(); + ex.setNow(1_000_000L); + Account buyer = new Account("B", 1000); + Account seller = new Account("S", 1000); + ex.addAccount(buyer); + ex.addAccount(seller); + + ex.enterOrder("X", Side.SELL, OrderType.LIMIT, 100, 10, "S"); + ex.enterOrder("X", Side.BUY, OrderType.LIMIT, 100, 10, "B"); + + assertEquals(100, buyer.getPosition("X")); + assertEquals(-100, seller.getPosition("X")); + } +} diff --git a/org.eclipsetrader.market.sim/META-INF/MANIFEST.MF b/org.eclipsetrader.market.sim/META-INF/MANIFEST.MF new file mode 100644 index 000000000..fbc969ab7 --- /dev/null +++ b/org.eclipsetrader.market.sim/META-INF/MANIFEST.MF @@ -0,0 +1,17 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Market Simulator +Bundle-SymbolicName: org.eclipsetrader.market.sim; singleton:=true +Bundle-Version: 1.0.0.qualifier +Bundle-Activator: org.eclipsetrader.market.sim.internal.MarketSimActivator +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Vendor: EclipseTrader.org +Require-Bundle: org.eclipsetrader.core;bundle-version="1.0.0", + org.eclipse.core.runtime, + org.eclipse.swt +Export-Package: org.eclipsetrader.market.sim.engine, + org.eclipsetrader.market.sim.risk, + org.eclipsetrader.market.sim.agent, + org.eclipsetrader.market.sim.clock, + org.eclipsetrader.market.sim.feed diff --git a/org.eclipsetrader.market.sim/build.properties b/org.eclipsetrader.market.sim/build.properties new file mode 100644 index 000000000..34d2e4d2d --- /dev/null +++ b/org.eclipsetrader.market.sim/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.eclipsetrader.market.sim/pom.xml b/org.eclipsetrader.market.sim/pom.xml new file mode 100644 index 000000000..120cd2641 --- /dev/null +++ b/org.eclipsetrader.market.sim/pom.xml @@ -0,0 +1,15 @@ + + 4.0.0 + + + org.eclipsetrader + eclipsetrader-parent + 1.0.0-SNAPSHOT + + + org.eclipsetrader.market.sim + eclipse-plugin + org.eclipsetrader.market.sim + diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/FundamentalValue.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/FundamentalValue.java new file mode 100644 index 000000000..893b93ee5 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/FundamentalValue.java @@ -0,0 +1,45 @@ +package org.eclipsetrader.market.sim.agent; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +/** + * The per-asset fundamental value, evolved by a drift (trend), a news shock, + * and noise: {@code F = F * (1 + drift + news + noise)}. + */ +public class FundamentalValue { + + private final Map values = new HashMap(); + + public void set(String asset, double value) { + values.put(asset, value); + } + + public double get(String asset) { + Double v = values.get(asset); + return v == null ? 0.0 : v.doubleValue(); + } + + /** + * Applies a news shock to the asset's fundamental value. + */ + public void applyNews(NewsEvent event) { + Double v = values.get(event.getAsset()); + if (v != null) { + values.put(event.getAsset(), v.doubleValue() * (1.0 + event.getSentiment() * event.getMagnitude())); + } + } + + /** + * Steps the fundamental value by the drift and a small noise term. + */ + public void step(String asset, double drift, double noise, Random random) { + Double v = values.get(asset); + if (v == null) { + return; + } + double shock = drift + noise * random.nextGaussian(); + values.put(asset, v.doubleValue() * (1.0 + shock)); + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/InformedAgent.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/InformedAgent.java new file mode 100644 index 000000000..58ce6a50a --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/InformedAgent.java @@ -0,0 +1,42 @@ +package org.eclipsetrader.market.sim.agent; + +import java.util.Random; + +import org.eclipsetrader.market.sim.engine.Exchange; +import org.eclipsetrader.market.sim.engine.OrderType; +import org.eclipsetrader.market.sim.engine.Side; + +/** + * An informed trader that tilts its orders toward the current trend and any + * recent news, hitting the quoted price to generate activity. + */ +public class InformedAgent { + + private final String participant; + private final long size; + private double newsBias; + + public InformedAgent(String participant, long size) { + this.participant = participant; + this.size = size; + } + + public void reactToNews(NewsEvent event) { + newsBias += event.getSentiment() * event.getMagnitude(); + } + + public void act(Exchange exchange, String asset, double fundamental, double trend, Random random) { + double noise = (random.nextDouble() - 0.5) * 0.002; + double bias = trend + newsBias + noise; + if (bias > 0) { + double ask = exchange.getBook(asset).getBestAsk(); + double price = ask != 0.0 ? ask : fundamental * 1.001; + exchange.enterOrder(asset, Side.BUY, OrderType.LIMIT, size, price, participant); + } else { + double bid = exchange.getBook(asset).getBestBid(); + double price = bid != 0.0 ? bid : fundamental * 0.999; + exchange.enterOrder(asset, Side.SELL, OrderType.LIMIT, size, price, participant); + } + newsBias *= 0.9; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/MarketMaker.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/MarketMaker.java new file mode 100644 index 000000000..2067c7697 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/MarketMaker.java @@ -0,0 +1,29 @@ +package org.eclipsetrader.market.sim.agent; + +import org.eclipsetrader.market.sim.engine.Exchange; +import org.eclipsetrader.market.sim.engine.OrderType; +import org.eclipsetrader.market.sim.engine.Side; + +/** + * The market maker quotes both sides around the fundamental value, providing + * continuous liquidity so trades happen without any player action. + */ +public class MarketMaker { + + private final String participant; + private final double spread; // total spread as a fraction of price + private final long size; + + public MarketMaker(String participant, double spread, long size) { + this.participant = participant; + this.spread = spread; + this.size = size; + } + + public void act(Exchange exchange, String asset, double fundamental) { + double bid = fundamental * (1.0 - spread / 2.0); + double ask = fundamental * (1.0 + spread / 2.0); + exchange.enterOrder(asset, Side.BUY, OrderType.LIMIT, size, bid, participant); + exchange.enterOrder(asset, Side.SELL, OrderType.LIMIT, size, ask, participant); + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/MarketSimulator.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/MarketSimulator.java new file mode 100644 index 000000000..057560d97 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/MarketSimulator.java @@ -0,0 +1,199 @@ +package org.eclipsetrader.market.sim.agent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import org.eclipsetrader.market.sim.clock.SimulatedClock; +import org.eclipsetrader.market.sim.clock.TradingCalendar; +import org.eclipsetrader.market.sim.engine.Exchange; +import org.eclipsetrader.market.sim.risk.Account; + +/** + * Orchestrates the adaptive market simulation: ties together the exchange, the + * simulated daily calendar, the fundamental value process, procedural news, and + * the trading agents. A seeded scenario entry point; all randomness comes from a + * single seed so runs are replayable. + */ +public class MarketSimulator { + + public static final String MARKET_MAKER = "MARKET_MAKER"; + public static final String INFORMED = "INFORMED"; + public static final String PLAYER = "PLAYER"; + + private final Exchange exchange = new Exchange(); + private final SimulatedClock clock; + private final TradingCalendar calendar; + private final Random random; + + private final Map fundamentals = new HashMap(); + private final Map trends = new HashMap(); + + private final NewsGenerator newsGenerator; + private final MarketMaker marketMaker; + private final InformedAgent informedAgent; + + private final double noise = 0.0005; + private final int trendWindow = 10; + private final double trendCap = 0.01; + private final double meanReversion = 0.0005; + private final double spread = 0.001; + private final long orderSize = 100; + + private long stepMillis = 1000; + private Thread thread; + private volatile boolean running; + + public MarketSimulator(long seed, long startEpochMillis, long newsIntervalMillis, double newsMagnitude) { + this.random = new Random(seed); + this.clock = new SimulatedClock(startEpochMillis); + this.calendar = new TradingCalendar(clock, 9, 30, 16, 0); + this.calendar.open(); + this.newsGenerator = new NewsGenerator(seed, newsIntervalMillis, newsMagnitude); + this.marketMaker = new MarketMaker(MARKET_MAKER, spread, orderSize); + this.informedAgent = new InformedAgent(INFORMED, orderSize); + + exchange.addAccount(new Account(MARKET_MAKER, 1_000_000_000.0)); + exchange.addAccount(new Account(INFORMED, 1_000_000_000.0)); + } + + public Exchange getExchange() { + return exchange; + } + + public SimulatedClock getClock() { + return clock; + } + + public TradingCalendar getCalendar() { + return calendar; + } + + public void setStepMillis(long stepMillis) { + this.stepMillis = stepMillis; + } + + public void addAsset(String asset, double initialPrice) { + fundamentals.put(asset, initialPrice); + trends.put(asset, new TrendEstimator(trendWindow, trendCap, meanReversion, initialPrice)); + newsGenerator.addAsset(asset); + } + + public Account addPlayer(double cash, double leverage, long shortLimit) { + Account account = new Account(PLAYER, cash); + account.setLeverage(leverage); + account.setShortLimit(shortLimit); + exchange.addAccount(account); + return account; + } + + /** + * Advances the simulation by one step: moves the clock, generates news, + * has agents trade, updates trend estimates, and checks margin. + */ + public void step() { + if (!calendar.isOpen()) { + return; + } + clock.advance(stepMillis); + if (calendar.isEndOfDay()) { + endOfDay(); + return; + } + exchange.setNow(clock.now()); + + NewsEvent news = newsGenerator.maybeGenerate(clock.now()); + if (news != null) { + applyNews(news); + } + + for (String asset : new ArrayList(fundamentals.keySet())) { + double f = fundamentals.get(asset); + TrendEstimator te = trends.get(asset); + double trend = te.estimate(); + f = f * (1.0 + trend + noise * random.nextGaussian()); + fundamentals.put(asset, f); + + marketMaker.act(exchange, asset, f); + informedAgent.act(exchange, asset, f, trend, random); + + double last = exchange.getBook(asset).getLast(); + if (last != 0.0) { + te.addPrice(last); + } + exchange.getBook(asset).checkStops(last != 0.0 ? last : f, exchange); + } + + exchange.checkMargin(); + } + + private void applyNews(NewsEvent news) { + double before = fundamentals.containsKey(news.getAsset()) ? fundamentals.get(news.getAsset()) : 0.0; + double after = before * (1.0 + news.getSentiment() * news.getMagnitude()); + fundamentals.put(news.getAsset(), after); + informedAgent.reactToNews(news); + } + + private void endOfDay() { + calendar.close(); + for (Account account : exchangeAccounts()) { + account.accrueInterest(); + } + calendar.advanceDay(); + exchange.setNow(clock.now()); + } + + private List exchangeAccounts() { + // Accounts are not exposed by Exchange; accrue interest on known participants. + List result = new ArrayList(); + Account mm = exchange.getAccount(MARKET_MAKER); + Account inf = exchange.getAccount(INFORMED); + Account player = exchange.getAccount(PLAYER); + if (mm != null) { + result.add(mm); + } + if (inf != null) { + result.add(inf); + } + if (player != null) { + result.add(player); + } + return result; + } + + public synchronized void start() { + if (running) { + return; + } + running = true; + thread = new Thread(new Runnable() { + @Override + public void run() { + while (running) { + step(); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + return; + } + } + } + }, "MarketSimulator"); + thread.setDaemon(true); + thread.start(); + } + + public synchronized void stop() { + running = false; + if (thread != null) { + thread.interrupt(); + thread = null; + } + } + + public boolean isRunning() { + return running; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/NewsEvent.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/NewsEvent.java new file mode 100644 index 000000000..b69355330 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/NewsEvent.java @@ -0,0 +1,36 @@ +package org.eclipsetrader.market.sim.agent; + +/** + * A procedurally generated news event carrying an asset, a sentiment direction, + * and a magnitude (as a fraction of price). + */ +public class NewsEvent { + + private final String asset; + private final int sentiment; // +1 good, -1 bad + private final double magnitude; + private final long timestamp; + + public NewsEvent(String asset, int sentiment, double magnitude, long timestamp) { + this.asset = asset; + this.sentiment = sentiment; + this.magnitude = magnitude; + this.timestamp = timestamp; + } + + public String getAsset() { + return asset; + } + + public int getSentiment() { + return sentiment; + } + + public double getMagnitude() { + return magnitude; + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/NewsGenerator.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/NewsGenerator.java new file mode 100644 index 000000000..6cb5fa861 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/NewsGenerator.java @@ -0,0 +1,46 @@ +package org.eclipsetrader.market.sim.agent; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Generates procedural news events on a configurable schedule, drawn from a + * seeded random source so scenarios are replayable. + */ +public class NewsGenerator { + + private final Random random; + private final List assets = new ArrayList(); + private final long intervalMillis; + private final double baseMagnitude; + private long nextNewsTime; + + public NewsGenerator(long seed, long intervalMillis, double baseMagnitude) { + this.random = new Random(seed); + this.intervalMillis = intervalMillis; + this.baseMagnitude = baseMagnitude; + this.nextNewsTime = intervalMillis; + } + + public void addAsset(String asset) { + assets.add(asset); + } + + /** + * Returns a news event if one is due at the given time, otherwise null. + */ + public NewsEvent maybeGenerate(long now) { + if (assets.isEmpty()) { + return null; + } + if (now >= nextNewsTime) { + nextNewsTime = now + intervalMillis; + String asset = assets.get(random.nextInt(assets.size())); + int sentiment = random.nextBoolean() ? 1 : -1; + double magnitude = baseMagnitude * (0.5 + random.nextDouble()); + return new NewsEvent(asset, sentiment, magnitude, now); + } + return null; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/TrendEstimator.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/TrendEstimator.java new file mode 100644 index 000000000..9b1e03f39 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/agent/TrendEstimator.java @@ -0,0 +1,43 @@ +package org.eclipsetrader.market.sim.agent; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Estimates the current drift (trend) from recent trade prices, capped in + * magnitude and pulled toward an anchor by mean reversion. + */ +public class TrendEstimator { + + private final Deque prices = new ArrayDeque(); + private final int window; + private final double cap; + private final double meanReversion; + private final double anchor; + + public TrendEstimator(int window, double cap, double meanReversion, double anchor) { + this.window = window; + this.cap = cap; + this.meanReversion = meanReversion; + this.anchor = anchor; + } + + public void addPrice(double price) { + prices.addLast(price); + if (prices.size() > window) { + prices.removeFirst(); + } + } + + public double estimate() { + if (prices.size() < 2) { + return 0.0; + } + Double first = prices.peekFirst(); + Double last = prices.peekLast(); + double raw = (last.doubleValue() - first.doubleValue()) / first.doubleValue() / prices.size(); + double capped = Math.max(-cap, Math.min(cap, raw)); + double reversion = meanReversion * (anchor - last.doubleValue()) / anchor; + return capped + reversion; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/clock/SimulatedClock.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/clock/SimulatedClock.java new file mode 100644 index 000000000..73b75ba3d --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/clock/SimulatedClock.java @@ -0,0 +1,64 @@ +package org.eclipsetrader.market.sim.clock; + +import java.util.Calendar; +import java.util.Date; + +/** + * A simulated clock whose "now" is an absolute timestamp on a simulated daily + * calendar (not wall-clock and not the 1970 epoch). All engine timestamps come + * from this clock. + */ +public class SimulatedClock { + + private long now; + + public SimulatedClock(long startEpochMillis) { + this.now = startEpochMillis; + } + + public long now() { + return now; + } + + public void setNow(long now) { + this.now = now; + } + + public Date time() { + return new Date(now); + } + + public void advance(long millis) { + now += millis; + } + + /** + * Advances the clock to the market open of the next trading day. + */ + public void advanceToNextDayOpen(int openHour, int openMinute) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(now); + c.add(Calendar.DAY_OF_YEAR, 1); + c.set(Calendar.HOUR_OF_DAY, openHour); + c.set(Calendar.MINUTE, openMinute); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + now = c.getTimeInMillis(); + } + + /** + * True if the current time is within the given intraday window (inclusive). + */ + public boolean isWithinWindow(int openHour, int openMinute, int closeHour, int closeMinute) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(now); + int minutes = c.get(Calendar.HOUR_OF_DAY) * 60 + c.get(Calendar.MINUTE); + return minutes >= openHour * 60 + openMinute && minutes < closeHour * 60 + closeMinute; + } + + public int dayOfYear() { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(now); + return c.get(Calendar.DAY_OF_YEAR); + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/clock/TradingCalendar.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/clock/TradingCalendar.java new file mode 100644 index 000000000..edee50303 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/clock/TradingCalendar.java @@ -0,0 +1,56 @@ +package org.eclipsetrader.market.sim.clock; + +/** + * A daily trading calendar over a {@link SimulatedClock}. Each period is one + * trading day with an open and a close; the calendar tracks the current open + * state and advances the day. + */ +public class TradingCalendar { + + private final SimulatedClock clock; + private final int openHour; + private final int openMinute; + private final int closeHour; + private final int closeMinute; + + private boolean open; + private int day; + + public TradingCalendar(SimulatedClock clock, int openHour, int openMinute, int closeHour, int closeMinute) { + this.clock = clock; + this.openHour = openHour; + this.openMinute = openMinute; + this.closeHour = closeHour; + this.closeMinute = closeMinute; + this.day = 1; + } + + public boolean isOpen() { + return open; + } + + public int getDay() { + return day; + } + + public void open() { + this.open = true; + } + + public void close() { + this.open = false; + } + + public boolean isEndOfDay() { + return !clock.isWithinWindow(openHour, openMinute, closeHour, closeMinute); + } + + /** + * Advances to the next trading day and opens the market. + */ + public void advanceDay() { + clock.advanceToNextDayOpen(openHour, openMinute); + day++; + open = true; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Exchange.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Exchange.java new file mode 100644 index 000000000..d658af3fa --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Exchange.java @@ -0,0 +1,215 @@ +package org.eclipsetrader.market.sim.engine; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipsetrader.market.sim.risk.Account; + +/** + * The central market. Holds the order books per asset, the participant accounts, + * and dispatches market activity to registered listeners. + */ +public class Exchange { + + private final Map books = new HashMap(); + private final Map accounts = new HashMap(); + private final List listeners = new ArrayList(); + + private long orderIdSeed = 1; + private long tradeIdSeed = 1; + private long now; + + public long now() { + return now; + } + + public void setNow(long now) { + this.now = now; + } + + public long nextOrderId() { + return orderIdSeed++; + } + + public long nextTradeId() { + return tradeIdSeed++; + } + + public OrderBook getBook(String asset) { + OrderBook book = books.get(asset); + if (book == null) { + book = new OrderBook(asset); + books.put(asset, book); + } + return book; + } + + public Map getBooks() { + return books; + } + + public void addAccount(Account account) { + accounts.put(account.getParticipant(), account); + } + + public Account getAccount(String participant) { + return accounts.get(participant); + } + + public void addListener(MarketListener listener) { + listeners.add(listener); + } + + public void removeListener(MarketListener listener) { + listeners.remove(listener); + } + + /** + * Enters an order, returning null if it is rejected by the risk checks + * (insufficient margin or short limit). Otherwise the order is matched + * against the book and any residual rests. + */ + public Order enterOrder(String asset, Side side, OrderType type, long quantity, double price, String participant) { + Account account = accounts.get(participant); + if (account != null) { + if (side == Side.BUY) { + if (type != OrderType.MARKET && !account.canBuy(price * quantity)) { + return null; + } + } else { + if (!account.canSell(asset, quantity)) { + return null; + } + } + } + Order order = new Order(nextOrderId(), asset, side, type, quantity, price, participant); + getBook(asset).enter(order, this); + return order; + } + + public boolean cancelOrder(long orderId, String asset) { + OrderBook book = books.get(asset); + if (book == null) { + return false; + } + boolean result = book.cancel(orderId); + if (result) { + notifyBook(book); + } + return result; + } + + public boolean amendOrder(long orderId, String asset, double newPrice, long newQty) { + OrderBook book = books.get(asset); + if (book == null) { + return false; + } + Order order = book.findOrder(orderId); + if (order == null) { + return false; + } + boolean changed = false; + if (newPrice != 0.0 && newPrice != order.getPrice()) { + changed = book.amendPrice(orderId, newPrice) || changed; + } + if (newQty > 0 && newQty < order.getRemaining()) { + changed = book.reduceQuantity(orderId, newQty) || changed; + } + if (changed) { + notifyBook(book); + } + return changed; + } + + /** + * Called by the order book after each match to update participant accounts. + */ + void applyTrade(Trade trade) { + Account buyer = accounts.get(trade.getBuyer()); + Account seller = accounts.get(trade.getSeller()); + if (buyer != null) { + buyer.applyTrade(trade.getAsset(), trade.getQuantity(), trade.getPrice()); + } + if (seller != null) { + seller.applyTrade(trade.getAsset(), -trade.getQuantity(), trade.getPrice()); + } + } + + /** + * Evaluates margin for every account and force-liquidates (flattens) any + * account whose equity is below the maintenance margin. + */ + public void checkMargin() { + for (Account account : new ArrayList(accounts.values())) { + Map prices = currentPrices(); + if (!account.isFlat() && account.equity(prices) < account.getMaintenanceMargin() * account.grossExposure(prices)) { + liquidate(account); + } + } + } + + private Map currentPrices() { + Map prices = new HashMap(); + for (OrderBook book : books.values()) { + double last = book.getLast(); + if (last != 0.0) { + prices.put(book.getAsset(), last); + } else if (book.getBestBid() != 0.0 || book.getBestAsk() != 0.0) { + double bid = book.getBestBid(); + double ask = book.getBestAsk(); + double mid = bid != 0.0 && ask != 0.0 ? (bid + ask) / 2.0 : (bid != 0.0 ? bid : ask); + prices.put(book.getAsset(), mid); + } + } + return prices; + } + + private void liquidate(Account account) { + for (Map.Entry en : account.getPositions().entrySet()) { + long qty = en.getValue().longValue(); + if (qty > 0) { + enterOrder(en.getKey(), Side.SELL, OrderType.MARKET, qty, 0, account.getParticipant()); + } else if (qty < 0) { + enterOrder(en.getKey(), Side.BUY, OrderType.MARKET, -qty, 0, account.getParticipant()); + } + } + } + + void notifyTrade(Trade trade) { + for (MarketListener listener : listeners) { + listener.onTrade(trade); + } + } + + void notifyBook(OrderBook book) { + List bids = new ArrayList(book.getBids()); + List asks = new ArrayList(book.getAsks()); + for (MarketListener listener : listeners) { + listener.onBook(book.getAsset(), bids, asks); + } + } + + void notifyQuote(String asset) { + OrderBook book = books.get(asset); + if (book == null) { + return; + } + double bid = book.getBestBid(); + double ask = book.getBestAsk(); + long bidSize = book.getBestBidVol(); + long askSize = book.getBestAskVol(); + for (MarketListener listener : listeners) { + listener.onQuote(asset, bid, ask, bidSize, askSize); + } + } + + void notifyOrderPlaced(Order order) { + notifyQuote(order.getAsset()); + } + + void notifyOrderCancelled(Order order) { + notifyQuote(order.getAsset()); + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/MarketListener.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/MarketListener.java new file mode 100644 index 000000000..c56c9fc7e --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/MarketListener.java @@ -0,0 +1,17 @@ +package org.eclipsetrader.market.sim.engine; + +import java.util.List; + +/** + * Callback interface notified of market activity, decoupled from any UI or + * feed layer. Implementations (such as the feed bridge) translate these into + * platform-specific events. + */ +public interface MarketListener { + + void onTrade(Trade trade); + + void onQuote(String asset, double bid, double ask, long bidSize, long askSize); + + void onBook(String asset, List bids, List asks); +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Order.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Order.java new file mode 100644 index 000000000..3b85b324c --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Order.java @@ -0,0 +1,236 @@ +package org.eclipsetrader.market.sim.engine; + +/** + * A single order in the simulated market. Supports limit, market, stop, + * stop-limit, and trailing-stop types, plus iceberg (display quantity), + * pegged (reference price) modifiers, and time-in-force (GTC/IOC/FOK). + */ +public class Order { + + private final long id; + private final String asset; + private final Side side; + private final OrderType type; + private TimeInForce timeInForce = TimeInForce.GTC; + private PegType pegType = PegType.NONE; + + private double price; // limit price (ignored for MARKET) + private double stopPrice; // trigger for STOP / STOP_LIMIT + private double trailingOffset; // offset for TRAILING_STOP + private double trailingBest; // best observed price for trailing stop + private double pegOffset; // offset for PEGGED orders + + private final long quantity; // original total quantity + private long remaining; // total unfilled (including hidden) + private long display; // currently visible quantity + private long displayQuantity; // iceberg chunk (0 = no iceberg) + + private long cumFilled; + private double avgFillPrice; + + private long timestamp; // simulated clock time + private final String participant; + + public Order(long id, String asset, Side side, OrderType type, long quantity, double price, String participant) { + this.id = id; + this.asset = asset; + this.side = side; + this.type = type; + this.quantity = quantity; + this.remaining = quantity; + this.price = price; + this.participant = participant; + this.displayQuantity = 0; + this.display = quantity; + } + + public long getId() { + return id; + } + + public String getAsset() { + return asset; + } + + public Side getSide() { + return side; + } + + public OrderType getType() { + return type; + } + + public TimeInForce getTimeInForce() { + return timeInForce; + } + + public void setTimeInForce(TimeInForce timeInForce) { + this.timeInForce = timeInForce; + } + + public PegType getPegType() { + return pegType; + } + + public void setPegType(PegType pegType) { + this.pegType = pegType; + } + + public double getPegOffset() { + return pegOffset; + } + + public void setPegOffset(double pegOffset) { + this.pegOffset = pegOffset; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public double getStopPrice() { + return stopPrice; + } + + public void setStopPrice(double stopPrice) { + this.stopPrice = stopPrice; + } + + public double getTrailingOffset() { + return trailingOffset; + } + + public void setTrailingOffset(double trailingOffset) { + this.trailingOffset = trailingOffset; + } + + public double getTrailingBest() { + return trailingBest; + } + + public void setTrailingBest(double trailingBest) { + this.trailingBest = trailingBest; + } + + public long getQuantity() { + return quantity; + } + + public long getRemaining() { + return remaining; + } + + public long getDisplayQuantity() { + return displayQuantity; + } + + public boolean isIceberg() { + return displayQuantity > 0; + } + + public long getCumFilled() { + return cumFilled; + } + + public double getAvgFillPrice() { + return avgFillPrice; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getParticipant() { + return participant; + } + + public boolean isMarket() { + return type == OrderType.MARKET; + } + + public boolean isContingent() { + return type == OrderType.STOP || type == OrderType.STOP_LIMIT || type == OrderType.TRAILING_STOP; + } + + /** + * The quantity currently available to be matched against (the visible portion). + */ + public long getAvailable() { + if (displayQuantity > 0) { + return Math.min(display, remaining); + } + return remaining; + } + + /** + * Sets the iceberg display chunk and initializes the visible quantity. + */ + public void setIceberg(long chunk) { + this.displayQuantity = chunk; + initializeDisplay(); + } + + public void initializeDisplay() { + if (displayQuantity > 0) { + display = Math.min(displayQuantity, remaining); + } else { + display = remaining; + } + } + + /** + * Fills the given quantity, replenishing the visible portion for iceberg orders. + */ + public void fill(long qty) { + if (qty <= 0) { + return; + } + if (qty > remaining) { + qty = remaining; + } + long filledBefore = cumFilled; + avgFillPrice = (avgFillPrice * filledBefore + getPrice() * qty) / (filledBefore + qty); + remaining -= qty; + cumFilled += qty; + if (displayQuantity > 0) { + display -= qty; + if (display <= 0 && remaining > 0) { + display = Math.min(displayQuantity, remaining); + } + } else { + display = remaining; + } + } + + /** + * Reduces the remaining quantity for an amendment (down) without recording a fill. + */ + public void reduce(long qty) { + if (qty > remaining) { + qty = remaining; + } + remaining -= qty; + if (displayQuantity <= 0) { + display = remaining; + } else { + display = Math.min(display, remaining); + } + } + + public boolean isFilled() { + return remaining <= 0; + } + + @Override + public String toString() { + return side + " " + type + " " + remaining + "@" + price; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/OrderBook.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/OrderBook.java new file mode 100644 index 000000000..f8e98e0bf --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/OrderBook.java @@ -0,0 +1,411 @@ +package org.eclipsetrader.market.sim.engine; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * The limit order book for a single asset. Implements price-time priority + * insertion and full-depth matching across price levels. Contingent (stop) + * orders rest off-book until triggered. + */ +public class OrderBook { + + private final String asset; + + private final List bids = new ArrayList(); + private final List asks = new ArrayList(); + private final List stops = new ArrayList(); + + private final List trades = new ArrayList(); + private double tradedVolume; + private double vwap; + private double lastPrice; + + public OrderBook(String asset) { + this.asset = asset; + } + + public String getAsset() { + return asset; + } + + public List getBids() { + return bids; + } + + public List getAsks() { + return asks; + } + + public List getStops() { + return stops; + } + + public List getTrades() { + return trades; + } + + public double getBestBid() { + return bids.isEmpty() ? 0.0 : bids.get(0).getPrice(); + } + + public double getBestAsk() { + return asks.isEmpty() ? 0.0 : asks.get(0).getPrice(); + } + + public long getBestBidVol() { + return levelVolume(bids); + } + + public long getBestAskVol() { + return levelVolume(asks); + } + + private long levelVolume(List orders) { + if (orders.isEmpty()) { + return 0; + } + double price = orders.get(0).getPrice(); + long total = 0; + for (Order o : orders) { + if (o.getPrice() == price) { + total += o.getAvailable(); + } else { + break; + } + } + return total; + } + + public double getLast() { + return lastPrice; + } + + public double getVwap() { + return vwap; + } + + public double getTradedVolume() { + return tradedVolume; + } + + /** + * Enters a non-contingent order, matching it against the opposite side and + * queuing any residual. Returns the trades generated. + */ + public List enter(Order order, Exchange exchange) { + order.setTimestamp(exchange.now()); + if (order.isContingent()) { + order.setTrailingBest(lastPrice != 0.0 ? lastPrice : order.getStopPrice()); + stops.add(order); + exchange.notifyOrderPlaced(order); + return new ArrayList(); + } + + if (order.getPegType() != PegType.NONE) { + applyPeg(order); + } + + List result = new ArrayList(); + + if (order.getTimeInForce() == TimeInForce.FOK) { + List opposite = order.getSide() == Side.BUY ? asks : bids; + if (!canFillFully(order, opposite)) { + exchange.notifyOrderCancelled(order); + return result; + } + } + + match(order, exchange, result); + + if (order.isMarket()) { + if (!order.isFilled()) { + exchange.notifyOrderCancelled(order); + } + } else if (order.getTimeInForce() == TimeInForce.IOC) { + if (!order.isFilled()) { + exchange.notifyOrderCancelled(order); + } + } else if (order.getTimeInForce() == TimeInForce.FOK) { + if (!order.isFilled()) { + exchange.notifyOrderCancelled(order); + } + } else if (!order.isFilled()) { + insertSorted(order); + exchange.notifyOrderPlaced(order); + } + + for (Trade t : result) { + addTrade(t); + exchange.notifyTrade(t); + } + + refreshPegged(); + exchange.notifyBook(this); + return result; + } + + private void match(Order order, Exchange exchange, List result) { + List opposite = order.getSide() == Side.BUY ? asks : bids; + int i = 0; + while (!order.isFilled() && i < opposite.size()) { + Order resting = opposite.get(i); + if (!crosses(resting, order)) { + break; + } + long tradeQty = Math.min(order.getRemaining(), resting.getAvailable()); + double tradePrice = resting.getPrice(); + resting.fill(tradeQty); + order.fill(tradeQty); + long buyOrderId = order.getSide() == Side.BUY ? order.getId() : resting.getId(); + long sellOrderId = order.getSide() == Side.BUY ? resting.getId() : order.getId(); + String buyer = order.getSide() == Side.BUY ? order.getParticipant() : resting.getParticipant(); + String seller = order.getSide() == Side.BUY ? resting.getParticipant() : order.getParticipant(); + Trade trade = new Trade(exchange.nextTradeId(), asset, tradePrice, tradeQty, buyOrderId, sellOrderId, order.getTimestamp(), buyer, seller); + result.add(trade); + exchange.applyTrade(trade); + if (resting.isFilled()) { + opposite.remove(i); + } + } + } + + private boolean crosses(Order resting, Order incoming) { + if (incoming.isMarket()) { + return true; + } + if (incoming.getSide() == Side.BUY) { + return incoming.getPrice() >= resting.getPrice(); + } + return incoming.getPrice() <= resting.getPrice(); + } + + private boolean canFillFully(Order order, List opposite) { + long needed = order.getRemaining(); + long avail = 0; + for (Order o : opposite) { + if (!crosses(o, order)) { + break; + } + avail += o.getAvailable(); + if (avail >= needed) { + return true; + } + } + return avail >= needed; + } + + private void insertSorted(Order order) { + List list = order.getSide() == Side.BUY ? bids : asks; + int i = 0; + if (order.getSide() == Side.BUY) { + while (i < list.size() && list.get(i).getPrice() >= order.getPrice()) { + i++; + } + } else { + while (i < list.size() && list.get(i).getPrice() <= order.getPrice()) { + i++; + } + } + list.add(i, order); + } + + private void applyPeg(Order order) { + double reference = pegReference(order); + order.setPrice(reference + order.getPegOffset()); + } + + private double pegReference(Order order) { + switch (order.getPegType()) { + case BEST_BID: + return getBestBid() != 0.0 ? getBestBid() : order.getPrice(); + case BEST_ASK: + return getBestAsk() != 0.0 ? getBestAsk() : order.getPrice(); + case MID: + return mid(); + default: + return order.getPrice(); + } + } + + private double mid() { + double bid = getBestBid(); + double ask = getBestAsk(); + if (bid != 0.0 && ask != 0.0) { + return (bid + ask) / 2.0; + } + if (bid != 0.0) { + return bid; + } + if (ask != 0.0) { + return ask; + } + return lastPrice; + } + + /** + * Re-prices all pegged orders against the current reference and re-sorts. + */ + public void refreshPegged() { + List pegged = new ArrayList(); + Iterator it = bids.iterator(); + while (it.hasNext()) { + Order o = it.next(); + if (o.getPegType() != PegType.NONE) { + pegged.add(o); + it.remove(); + } + } + it = asks.iterator(); + while (it.hasNext()) { + Order o = it.next(); + if (o.getPegType() != PegType.NONE) { + pegged.add(o); + it.remove(); + } + } + for (Order o : pegged) { + applyPeg(o); + insertSorted(o); + } + } + + /** + * Evaluates stop orders against the given reference price, triggering those + * whose trigger has been crossed by submitting the resulting order. + */ + public void checkStops(double marketPrice, Exchange exchange) { + List triggered = new ArrayList(); + Iterator it = stops.iterator(); + while (it.hasNext()) { + Order o = it.next(); + boolean fire = false; + if (o.getType() == OrderType.TRAILING_STOP) { + if (o.getSide() == Side.SELL) { + o.setTrailingBest(Math.max(o.getTrailingBest(), marketPrice)); + fire = marketPrice <= o.getTrailingBest() - o.getTrailingOffset(); + } else { + o.setTrailingBest(Math.min(o.getTrailingBest(), marketPrice)); + fire = marketPrice >= o.getTrailingBest() + o.getTrailingOffset(); + } + } else if (o.getSide() == Side.SELL) { + fire = marketPrice <= o.getStopPrice(); + } else { + fire = marketPrice >= o.getStopPrice(); + } + if (fire) { + it.remove(); + triggered.add(o); + } + } + for (Order o : triggered) { + OrderType resultType = o.getType() == OrderType.STOP_LIMIT ? OrderType.LIMIT : OrderType.MARKET; + Order replacement = new Order(exchange.nextOrderId(), asset, o.getSide(), resultType, o.getRemaining(), o.getPrice(), o.getParticipant()); + replacement.setTimeInForce(TimeInForce.GTC); + enter(replacement, exchange); + } + } + + public Order findOrder(long id) { + for (Order o : bids) { + if (o.getId() == id) { + return o; + } + } + for (Order o : asks) { + if (o.getId() == id) { + return o; + } + } + for (Order o : stops) { + if (o.getId() == id) { + return o; + } + } + return null; + } + + public boolean cancel(long orderId) { + Iterator it = bids.iterator(); + while (it.hasNext()) { + if (it.next().getId() == orderId) { + it.remove(); + return true; + } + } + it = asks.iterator(); + while (it.hasNext()) { + if (it.next().getId() == orderId) { + it.remove(); + return true; + } + } + it = stops.iterator(); + while (it.hasNext()) { + if (it.next().getId() == orderId) { + it.remove(); + return true; + } + } + return false; + } + + /** + * Amends the price of a resting order, re-queuing it at the new price. + */ + public boolean amendPrice(long orderId, double newPrice) { + Order order = removeFromBook(orderId); + if (order == null) { + return false; + } + order.setPrice(newPrice); + order.initializeDisplay(); + insertSorted(order); + return true; + } + + /** + * Reduces the quantity of a resting order (time priority retained). + */ + public boolean reduceQuantity(long orderId, long newQty) { + Order order = findOrder(orderId); + if (order == null) { + return false; + } + long filled = order.getQuantity() - order.getRemaining(); + long reduceTo = Math.max(filled, newQty); + long reduction = order.getRemaining() - (reduceTo - filled); + if (reduction > 0) { + order.reduce(reduction); + } + return true; + } + + private Order removeFromBook(long orderId) { + Iterator it = bids.iterator(); + while (it.hasNext()) { + Order o = it.next(); + if (o.getId() == orderId) { + it.remove(); + return o; + } + } + it = asks.iterator(); + while (it.hasNext()) { + Order o = it.next(); + if (o.getId() == orderId) { + it.remove(); + return o; + } + } + return null; + } + + private void addTrade(Trade trade) { + trades.add(trade); + lastPrice = trade.getPrice(); + vwap = (vwap * tradedVolume + trade.getPrice() * trade.getQuantity()) / (tradedVolume + trade.getQuantity()); + tradedVolume += trade.getQuantity(); + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/OrderType.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/OrderType.java new file mode 100644 index 000000000..996813a28 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/OrderType.java @@ -0,0 +1,8 @@ +package org.eclipsetrader.market.sim.engine; + +/** + * The type of an order. + */ +public enum OrderType { + LIMIT, MARKET, STOP, STOP_LIMIT, TRAILING_STOP +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/PegType.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/PegType.java new file mode 100644 index 000000000..ed57f5a68 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/PegType.java @@ -0,0 +1,8 @@ +package org.eclipsetrader.market.sim.engine; + +/** + * Peg reference for a pegged order. + */ +public enum PegType { + NONE, BEST_BID, BEST_ASK, MID +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Side.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Side.java new file mode 100644 index 000000000..b54b03e7c --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Side.java @@ -0,0 +1,8 @@ +package org.eclipsetrader.market.sim.engine; + +/** + * The side of an order: buy or sell. + */ +public enum Side { + BUY, SELL +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/TimeInForce.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/TimeInForce.java new file mode 100644 index 000000000..92edc6a10 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/TimeInForce.java @@ -0,0 +1,13 @@ +package org.eclipsetrader.market.sim.engine; + +/** + * Time-in-force modifier for an order. + */ +public enum TimeInForce { + /** Good till cancelled: rest in the book until filled or cancelled. */ + GTC, + /** Immediate or cancel: fill what is available now, cancel the rest. */ + IOC, + /** Fill or kill: fill the entire quantity now or cancel with no fill. */ + FOK +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Trade.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Trade.java new file mode 100644 index 000000000..a7e8c54e4 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/engine/Trade.java @@ -0,0 +1,70 @@ +package org.eclipsetrader.market.sim.engine; + +/** + * A single executed trade between a buyer and a seller. + */ +public class Trade { + + private final long id; + private final String asset; + private final double price; + private final long quantity; + private final long buyOrderId; + private final long sellOrderId; + private final long timestamp; + private final String buyer; + private final String seller; + + public Trade(long id, String asset, double price, long quantity, long buyOrderId, long sellOrderId, long timestamp, String buyer, String seller) { + this.id = id; + this.asset = asset; + this.price = price; + this.quantity = quantity; + this.buyOrderId = buyOrderId; + this.sellOrderId = sellOrderId; + this.timestamp = timestamp; + this.buyer = buyer; + this.seller = seller; + } + + public long getId() { + return id; + } + + public String getAsset() { + return asset; + } + + public double getPrice() { + return price; + } + + public long getQuantity() { + return quantity; + } + + public long getBuyOrderId() { + return buyOrderId; + } + + public long getSellOrderId() { + return sellOrderId; + } + + public long getTimestamp() { + return timestamp; + } + + public String getBuyer() { + return buyer; + } + + public String getSeller() { + return seller; + } + + @Override + public String toString() { + return "Trade " + quantity + " " + asset + "@" + price; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/feed/MarketFeedConnector.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/feed/MarketFeedConnector.java new file mode 100644 index 000000000..33364e119 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/feed/MarketFeedConnector.java @@ -0,0 +1,186 @@ +package org.eclipsetrader.market.sim.feed; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.ListenerList; +import org.eclipse.swt.widgets.Display; +import org.eclipsetrader.core.feed.Book; +import org.eclipsetrader.core.feed.BookEntry; +import org.eclipsetrader.core.feed.FeedIdentifier; +import org.eclipsetrader.core.feed.FeedProperties; +import org.eclipsetrader.core.feed.IConnectorListener; +import org.eclipsetrader.core.feed.IFeedConnector2; +import org.eclipsetrader.core.feed.IFeedIdentifier; +import org.eclipsetrader.core.feed.IFeedSubscription; +import org.eclipsetrader.core.feed.IFeedSubscription2; +import org.eclipsetrader.core.feed.IBookEntry; +import org.eclipsetrader.core.feed.Quote; +import org.eclipsetrader.core.feed.Trade; +import org.eclipsetrader.market.sim.engine.MarketListener; +import org.eclipsetrader.market.sim.engine.Order; + +/** + * Bridges the market simulator to the EclipseTrader feed seam. It is both an + * {@link IFeedConnector2} (so views can subscribe) and a {@link MarketListener} + * (so it receives engine activity and pushes it into the subscriptions on the + * display thread). + */ +public class MarketFeedConnector implements IFeedConnector2, MarketListener { + + private String id = "org.eclipsetrader.market.sim.feed"; + private String name = "Market Simulator"; + + private final Map subscriptions = new HashMap(); + private final ListenerList listeners = new ListenerList(ListenerList.IDENTITY); + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public IFeedSubscription subscribe(IFeedIdentifier identifier) { + return getOrCreate(identifier); + } + + @Override + public IFeedSubscription2 subscribeLevel2(IFeedIdentifier identifier) { + return getOrCreate(identifier); + } + + @Override + public IFeedSubscription2 subscribeLevel2(String symbol) { + return getOrCreate(symbol); + } + + private MarketFeedSubscription getOrCreate(IFeedIdentifier identifier) { + synchronized (subscriptions) { + MarketFeedSubscription sub = subscriptions.get(identifier.getSymbol()); + if (sub == null) { + sub = new MarketFeedSubscription(this, identifier); + subscriptions.put(identifier.getSymbol(), sub); + } + return sub; + } + } + + private MarketFeedSubscription getOrCreate(String symbol) { + synchronized (subscriptions) { + MarketFeedSubscription sub = subscriptions.get(symbol); + if (sub == null) { + sub = new MarketFeedSubscription(this, new FeedIdentifier(symbol, new FeedProperties())); + subscriptions.put(symbol, sub); + } + return sub; + } + } + + void disposeSubscription(MarketFeedSubscription subscription) { + synchronized (subscriptions) { + subscriptions.remove(subscription.getSymbol()); + } + } + + @Override + public void connect() { + } + + @Override + public void disconnect() { + } + + @Override + public void addConnectorListener(IConnectorListener listener) { + listeners.add(listener); + } + + @Override + public void removeConnectorListener(IConnectorListener listener) { + listeners.remove(listener); + } + + @Override + public void onTrade(final org.eclipsetrader.market.sim.engine.Trade trade) { + final MarketFeedSubscription sub = subscriptions.get(trade.getAsset()); + if (sub == null) { + return; + } + final Trade t = new Trade(new Date(trade.getTimestamp()), trade.getPrice(), trade.getQuantity(), trade.getQuantity()); + runOnDisplay(new Runnable() { + @Override + public void run() { + sub.setTrade(t); + sub.fireNotification(); + } + }); + } + + @Override + public void onQuote(final String asset, final double bid, final double ask, final long bidSize, final long askSize) { + final MarketFeedSubscription sub = subscriptions.get(asset); + if (sub == null) { + return; + } + runOnDisplay(new Runnable() { + @Override + public void run() { + sub.setQuote(new Quote(bid, ask, bidSize, askSize)); + sub.fireNotification(); + } + }); + } + + @Override + public void onBook(final String asset, final List bids, final List asks) { + final MarketFeedSubscription sub = subscriptions.get(asset); + if (sub == null) { + return; + } + final Book book = buildBook(bids, asks); + runOnDisplay(new Runnable() { + @Override + public void run() { + sub.setBook(book); + sub.fireNotification(); + } + }); + } + + private Book buildBook(List bids, List asks) { + IBookEntry[] bidEntries = new IBookEntry[bids.size()]; + for (int i = 0; i < bids.size(); i++) { + Order o = bids.get(i); + bidEntries[i] = new BookEntry(null, o.getPrice(), o.getAvailable(), 1L, null); + } + IBookEntry[] askEntries = new IBookEntry[asks.size()]; + for (int i = 0; i < asks.size(); i++) { + Order o = asks.get(i); + askEntries[i] = new BookEntry(null, o.getPrice(), o.getAvailable(), 1L, null); + } + return new Book(bidEntries, askEntries); + } + + private void runOnDisplay(Runnable runnable) { + try { + Display.getDefault().asyncExec(runnable); + } catch (Throwable t) { + runnable.run(); + } + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/feed/MarketFeedSubscription.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/feed/MarketFeedSubscription.java new file mode 100644 index 000000000..5109d0712 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/feed/MarketFeedSubscription.java @@ -0,0 +1,143 @@ +package org.eclipsetrader.market.sim.feed; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.core.runtime.ListenerList; +import org.eclipsetrader.core.feed.IBook; +import org.eclipsetrader.core.feed.IFeedIdentifier; +import org.eclipsetrader.core.feed.IFeedSubscription2; +import org.eclipsetrader.core.feed.ILastClose; +import org.eclipsetrader.core.feed.IQuote; +import org.eclipsetrader.core.feed.ISubscriptionListener; +import org.eclipsetrader.core.feed.ITodayOHL; +import org.eclipsetrader.core.feed.ITrade; +import org.eclipsetrader.core.feed.QuoteDelta; +import org.eclipsetrader.core.feed.QuoteEvent; + +/** + * A feed subscription populated by the market simulator. Values are pushed in + * via the public setters and delivered to listeners as quote updates. + */ +public class MarketFeedSubscription implements IFeedSubscription2 { + + private final MarketFeedConnector connector; + private final IFeedIdentifier identifier; + + private ITrade trade; + private IQuote quote; + private ITodayOHL todayOHL; + private IBook book; + + private final ListenerList listeners = new ListenerList(ListenerList.IDENTITY); + private final List deltaList = new ArrayList(); + + public MarketFeedSubscription(MarketFeedConnector connector, IFeedIdentifier identifier) { + this.connector = connector; + this.identifier = identifier; + } + + @Override + public IFeedIdentifier getIdentifier() { + return identifier; + } + + @Override + public String getSymbol() { + return identifier.getSymbol(); + } + + @Override + public void dispose() { + connector.disposeSubscription(this); + } + + @Override + public ITrade getTrade() { + return trade; + } + + @Override + public IQuote getQuote() { + return quote; + } + + @Override + public ITodayOHL getTodayOHL() { + return todayOHL; + } + + @Override + public ILastClose getLastClose() { + return null; + } + + @Override + public IBook getBook() { + return book; + } + + @Override + public void addSubscriptionListener(ISubscriptionListener listener) { + listeners.add(listener); + } + + @Override + public void removeSubscriptionListener(ISubscriptionListener listener) { + listeners.remove(listener); + } + + public void setTrade(ITrade trade) { + if (this.trade == null || !trade.equals(this.trade)) { + addDelta(new QuoteDelta(identifier, this.trade, trade)); + this.trade = trade; + } + } + + public void setQuote(IQuote quote) { + if (this.quote == null || !quote.equals(this.quote)) { + addDelta(new QuoteDelta(identifier, this.quote, quote)); + this.quote = quote; + } + } + + public void setTodayOHL(ITodayOHL todayOHL) { + if (this.todayOHL == null || !todayOHL.equals(this.todayOHL)) { + addDelta(new QuoteDelta(identifier, this.todayOHL, todayOHL)); + this.todayOHL = todayOHL; + } + } + + public void setBook(IBook book) { + addDelta(new QuoteDelta(identifier, this.book, book)); + this.book = book; + } + + private void addDelta(QuoteDelta delta) { + synchronized (deltaList) { + deltaList.add(delta); + } + } + + public boolean hasPendingChanges() { + synchronized (deltaList) { + return !deltaList.isEmpty(); + } + } + + public void fireNotification() { + QuoteDelta[] deltas; + synchronized (deltaList) { + if (deltaList.isEmpty()) { + return; + } + deltas = deltaList.toArray(new QuoteDelta[deltaList.size()]); + deltaList.clear(); + } + QuoteEvent event = new QuoteEvent(connector, identifier, deltas); + Object[] l = listeners.getListeners(); + for (Object o : l) { + ((ISubscriptionListener) o).quoteUpdate(event); + } + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/internal/MarketSimActivator.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/internal/MarketSimActivator.java new file mode 100644 index 000000000..611ff800e --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/internal/MarketSimActivator.java @@ -0,0 +1,28 @@ +package org.eclipsetrader.market.sim.internal; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Bundle activator for the market simulator. Headless-safe: performs no + * platform or display work on startup, so the bundle can be loaded in test and + * CI environments without a display. + */ +public class MarketSimActivator implements BundleActivator { + + private static MarketSimActivator instance; + + public static MarketSimActivator getDefault() { + return instance; + } + + @Override + public void start(BundleContext context) throws Exception { + instance = this; + } + + @Override + public void stop(BundleContext context) throws Exception { + instance = null; + } +} diff --git a/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/risk/Account.java b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/risk/Account.java new file mode 100644 index 000000000..c26915cc8 --- /dev/null +++ b/org.eclipsetrader.market.sim/src/org/eclipsetrader/market/sim/risk/Account.java @@ -0,0 +1,152 @@ +package org.eclipsetrader.market.sim.risk; + +import java.util.HashMap; +import java.util.Map; + +/** + * A participant account: signed cash (negative = borrowed) and signed positions + * (negative = short). Provides leverage buying power, margin, short-limit, and + * equity computations. Pure Java, no platform dependencies. + */ +public class Account { + + private final String participant; + private double cash; + private double leverage = 1.0; + private long shortLimit = Long.MAX_VALUE; + private double maintenanceMargin = 0.0; + private double interestRate = 0.0; + + private final Map positions = new HashMap(); + + public Account(String participant, double cash) { + this.participant = participant; + this.cash = cash; + } + + public String getParticipant() { + return participant; + } + + public double getCash() { + return cash; + } + + public void setCash(double cash) { + this.cash = cash; + } + + public double getLeverage() { + return leverage; + } + + public void setLeverage(double leverage) { + this.leverage = leverage; + } + + public long getShortLimit() { + return shortLimit; + } + + public void setShortLimit(long shortLimit) { + this.shortLimit = shortLimit; + } + + public double getMaintenanceMargin() { + return maintenanceMargin; + } + + public void setMaintenanceMargin(double maintenanceMargin) { + this.maintenanceMargin = maintenanceMargin; + } + + public double getInterestRate() { + return interestRate; + } + + public void setInterestRate(double interestRate) { + this.interestRate = interestRate; + } + + public long getPosition(String asset) { + Long pos = positions.get(asset); + return pos == null ? 0L : pos.longValue(); + } + + public Map getPositions() { + return new HashMap(positions); + } + + /** + * Applies a signed position delta and the corresponding cash movement. + * A positive delta (buy) reduces cash; a negative delta (sell) increases cash. + */ + public void applyTrade(String asset, long signedDelta, double price) { + long newPos = getPosition(asset) + signedDelta; + positions.put(asset, newPos); + cash -= signedDelta * price; + } + + /** + * True if the account has enough buying power (cash times leverage) to + * afford the given purchase cost. + */ + public boolean canBuy(double cost) { + return cost <= cash * leverage; + } + + /** + * True if selling the given quantity does not breach the short limit. + */ + public boolean canSell(String asset, long qty) { + long after = getPosition(asset) - qty; + return after >= -shortLimit; + } + + /** + * Cash plus mark-to-market value of long positions minus the value of short + * positions. + */ + public double equity(Map prices) { + double e = cash; + for (Map.Entry en : positions.entrySet()) { + Double px = prices.get(en.getKey()); + if (px != null) { + e += en.getValue().longValue() * px.doubleValue(); + } + } + return e; + } + + /** + * Sum of the absolute mark-to-market value of all positions. + */ + public double grossExposure(Map prices) { + double g = 0.0; + for (Map.Entry en : positions.entrySet()) { + Double px = prices.get(en.getKey()); + if (px != null) { + g += Math.abs(en.getValue().longValue()) * px.doubleValue(); + } + } + return g; + } + + public boolean isFlat() { + for (long v : positions.values()) { + if (v != 0) { + return false; + } + } + return true; + } + + /** + * Accrues one period of interest on any borrowed cash (negative balance). + */ + public void accrueInterest() { + if (cash < 0) { + cash += cash * interestRate; + } + } +} diff --git a/org.eclipsetrader.platform/plugin.xml b/org.eclipsetrader.platform/plugin.xml index a05dfc0d9..1747e71a9 100644 --- a/org.eclipsetrader.platform/plugin.xml +++ b/org.eclipsetrader.platform/plugin.xml @@ -21,6 +21,9 @@ + diff --git a/org.eclipsetrader.platform/plugin_customization.ini b/org.eclipsetrader.platform/plugin_customization.ini index 0966cf0b6..08b5b5398 100644 --- a/org.eclipsetrader.platform/plugin_customization.ini +++ b/org.eclipsetrader.platform/plugin_customization.ini @@ -29,8 +29,8 @@ org.eclipse.update.core/org.eclipse.update.core.updateVersions=compatible org.eclipsetrader.ui/EXIT_PROMPT_ON_CLOSE_LAST_WINDOW=true # default connectors -org.eclipsetrader.core/DEFAULT_CONNECTOR=org.eclipsetrader.yahoo -org.eclipsetrader.core/DEFAULT_BACKFILL_CONNECTOR=org.eclipsetrader.yahoo +org.eclipsetrader.core/DEFAULT_CONNECTOR=org.eclipsetrader.jessx.connector +org.eclipsetrader.core/DEFAULT_BACKFILL_CONNECTOR=org.eclipsetrader.jessx.connector # chart defaults org.eclipsetrader.ui/INITIAL_BACKFILL_METHOD=0 diff --git a/org.eclipsetrader.releng/eclipsetrader.product b/org.eclipsetrader.releng/eclipsetrader.product index 2c4b80495..65d992db0 100644 --- a/org.eclipsetrader.releng/eclipsetrader.product +++ b/org.eclipsetrader.releng/eclipsetrader.product @@ -53,6 +53,7 @@ + diff --git a/org.eclipsetrader.ui/data/basic-template.xml b/org.eclipsetrader.ui/data/basic-template.xml index b6fa15c94..711b23cb4 100644 --- a/org.eclipsetrader.ui/data/basic-template.xml +++ b/org.eclipsetrader.ui/data/basic-template.xml @@ -3,7 +3,7 @@ Basic
- + diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/BarChart.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/BarChart.java index 9af4fbd28..2f5039194 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/BarChart.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/BarChart.java @@ -23,6 +23,7 @@ import org.eclipse.swt.widgets.Composite; import org.eclipsetrader.core.charts.IDataSeries; import org.eclipsetrader.core.charts.OHLCDataSeries; +import org.eclipsetrader.core.charts.OHLCDownsampler; import org.eclipsetrader.core.feed.IOHLC; import org.eclipsetrader.core.feed.TimeSpan; @@ -31,10 +32,13 @@ public class BarChart implements IChartObject, ISummaryBarDecorator, IAdaptable private IDataSeries dataSeries; private int width = 5; - private RGB positiveColor = new RGB(0, 254, 0); - private RGB negativeColor = new RGB(254, 0, 0); + private RGB positiveColor = ChartThemes.getDefault().getPositive(); + private RGB negativeColor = ChartThemes.getDefault().getNegative(); private IAdaptable[] values; + private Date firstDate; + private Date lastDate; + private int pixelWidth; private List pointArray; private boolean valid; private boolean hasFocus; @@ -89,6 +93,11 @@ public BarChart(IDataSeries dataSeries, RGB positiveColor, RGB negativeColor) { */ @Override public void setDataBounds(DataBounds dataBounds) { + this.width = dataBounds.horizontalSpacing; + if (isSameRange(dataBounds)) { + return; + } + List l = new ArrayList(2048); for (IAdaptable value : dataSeries.getValues()) { Date date = (Date) value.getAdapter(Date.class); @@ -96,17 +105,36 @@ public void setDataBounds(DataBounds dataBounds) { l.add(value); } } - this.values = l.toArray(new IAdaptable[l.size()]); - this.width = dataBounds.horizontalSpacing; + IAdaptable[] visible = l.toArray(new IAdaptable[l.size()]); + this.values = OHLCDownsampler.downsample(visible, dataBounds.width); + this.firstDate = dataBounds.first; + this.lastDate = dataBounds.last; + this.pixelWidth = dataBounds.width; this.valid = false; } + private boolean isSameRange(DataBounds dataBounds) { + if (values == null || pixelWidth != dataBounds.width) { + return false; + } + if (firstDate != dataBounds.first && (firstDate == null || !firstDate.equals(dataBounds.first))) { + return false; + } + if (lastDate != dataBounds.last && (lastDate == null || !lastDate.equals(dataBounds.last))) { + return false; + } + return true; + } + /* (non-Javadoc) * @see org.eclipsetrader.ui.charts.IChartObject#invalidate() */ @Override public void invalidate() { this.valid = false; + this.values = null; + this.firstDate = null; + this.lastDate = null; } /* (non-Javadoc) @@ -333,10 +361,11 @@ public void setColor(RGB color) { } public void paint(IGraphics graphics) { + int barWidth = Math.max(width, 3); graphics.setForegroundColor(color); graphics.drawLine(x, yHigh, x, yLow); - graphics.drawLine(x - width / 2, yOpen, x, yOpen); - graphics.drawLine(x, yClose, x + width / 2, yClose); + graphics.drawLine(x - barWidth / 2, yOpen, x, yOpen); + graphics.drawLine(x, yClose, x + barWidth / 2, yClose); } public boolean containsPoint(int x, int y) { diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/CandleStickChart.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/CandleStickChart.java index e820cb7ad..732f2058f 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/CandleStickChart.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/CandleStickChart.java @@ -22,6 +22,7 @@ import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.eclipsetrader.core.charts.IDataSeries; +import org.eclipsetrader.core.charts.OHLCDownsampler; import org.eclipsetrader.core.feed.IOHLC; /** @@ -34,11 +35,14 @@ public class CandleStickChart implements IChartObject, ISummaryBarDecorator, IAd private IDataSeries dataSeries; private int width = 5; - private RGB outlineColor = new RGB(0, 0, 0); - private RGB positiveColor = new RGB(254, 254, 254); - private RGB negativeColor = new RGB(0, 0, 0); + private RGB outlineColor = ChartThemes.getDefault().getOutline(); + private RGB positiveColor = ChartThemes.getDefault().getPositive(); + private RGB negativeColor = ChartThemes.getDefault().getNegative(); private IAdaptable[] values; + private Date firstDate; + private Date lastDate; + private int pixelWidth; private List pointArray; private boolean valid; private boolean hasFocus; @@ -81,6 +85,11 @@ public CandleStickChart(IDataSeries dataSeries, RGB outlineColor, RGB positiveCo */ @Override public void setDataBounds(DataBounds dataBounds) { + this.width = dataBounds.horizontalSpacing; + if (isSameRange(dataBounds)) { + return; + } + List l = new ArrayList(2048); for (IAdaptable value : dataSeries.getValues()) { Date date = (Date) value.getAdapter(Date.class); @@ -88,17 +97,36 @@ public void setDataBounds(DataBounds dataBounds) { l.add(value); } } - this.values = l.toArray(new IAdaptable[l.size()]); - this.width = dataBounds.horizontalSpacing; + IAdaptable[] visible = l.toArray(new IAdaptable[l.size()]); + this.values = OHLCDownsampler.downsample(visible, dataBounds.width); + this.firstDate = dataBounds.first; + this.lastDate = dataBounds.last; + this.pixelWidth = dataBounds.width; this.valid = false; } + private boolean isSameRange(DataBounds dataBounds) { + if (values == null || pixelWidth != dataBounds.width) { + return false; + } + if (firstDate != dataBounds.first && (firstDate == null || !firstDate.equals(dataBounds.first))) { + return false; + } + if (lastDate != dataBounds.last && (lastDate == null || !lastDate.equals(dataBounds.last))) { + return false; + } + return true; + } + /* (non-Javadoc) * @see org.eclipsetrader.ui.charts.IChartObject#invalidate() */ @Override public void invalidate() { this.valid = false; + this.values = null; + this.firstDate = null; + this.lastDate = null; } /* (non-Javadoc) @@ -326,17 +354,20 @@ public void setOhlc(IOHLC ohlc) { } public void paint(IGraphics graphics) { + int bodyWidth = Math.max(width, 2); graphics.setForegroundColor(outlineColor); graphics.drawLine(x, yHigh, x, yLow); if (yOpen < yClose) { + int bodyHeight = Math.max(yClose - yOpen, 1); graphics.setBackgroundColor(fillColor); - graphics.fillRectangle(x - width / 2, yOpen, width, yClose - yOpen); - graphics.drawRectangle(x - width / 2, yOpen, width - 1, yClose - yOpen - 1); + graphics.fillRectangle(x - bodyWidth / 2, yOpen, bodyWidth, bodyHeight); + graphics.drawRectangle(x - bodyWidth / 2, yOpen, bodyWidth - 1, bodyHeight - 1); } else { + int bodyHeight = Math.max(yOpen - yClose, 1); graphics.setBackgroundColor(fillColor); - graphics.fillRectangle(x - width / 2, yClose, width, yOpen - yClose); - graphics.drawRectangle(x - width / 2, yClose, width - 1, yOpen - yClose - 1); + graphics.fillRectangle(x - bodyWidth / 2, yClose, bodyWidth, bodyHeight); + graphics.drawRectangle(x - bodyWidth / 2, yClose, bodyWidth - 1, bodyHeight - 1); } } diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartCanvas.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartCanvas.java index 54bff9aa6..ccefc26d3 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartCanvas.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartCanvas.java @@ -53,6 +53,8 @@ public class ChartCanvas { private Image image; private Image verticalScaleImage; + private int imageZoom; + private int verticalScaleImageZoom; private Label label; @@ -250,8 +252,13 @@ private void onPaint(PaintEvent event) { image.dispose(); } } - if (image == null || image.isDisposed()) { - image = new Image(canvas.getDisplay(), clientArea.width, clientArea.height); + int zoom = ChartUtils.getZoom(canvas); + if (image == null || image.isDisposed() || imageZoom != zoom) { + if (image != null && !image.isDisposed()) { + image.dispose(); + } + image = ChartUtils.createBackingImage(canvas, clientArea); + imageZoom = zoom; needsRedraw = true; } @@ -360,8 +367,13 @@ private void onPaintVerticalScale(PaintEvent event) { verticalScaleImage.dispose(); } } - if (verticalScaleImage == null || verticalScaleImage.isDisposed()) { - verticalScaleImage = new Image(verticalScaleCanvas.getDisplay(), clientArea.width, clientArea.height); + int zoom = ChartUtils.getZoom(verticalScaleCanvas); + if (verticalScaleImage == null || verticalScaleImage.isDisposed() || verticalScaleImageZoom != zoom) { + if (verticalScaleImage != null && !verticalScaleImage.isDisposed()) { + verticalScaleImage.dispose(); + } + verticalScaleImage = ChartUtils.createBackingImage(verticalScaleCanvas, clientArea); + verticalScaleImageZoom = zoom; needsRedraw = true; } diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartTheme.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartTheme.java new file mode 100644 index 000000000..8cae1c51a --- /dev/null +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartTheme.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2004-2011 Marco Maccaferri and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Marco Maccaferri - initial API and implementation + */ + +package org.eclipsetrader.ui.charts; + +import org.eclipse.swt.graphics.RGB; + +/** + * Immutable chart color theme. + * + * @since 1.0 + */ +public class ChartTheme { + + private final RGB line; + private final RGB positive; + private final RGB negative; + private final RGB outline; + private final RGB grid; + private final RGB background; + + public ChartTheme(RGB line, RGB positive, RGB negative, RGB outline, RGB grid, RGB background) { + this.line = line; + this.positive = positive; + this.negative = negative; + this.outline = outline; + this.grid = grid; + this.background = background; + } + + public RGB getLine() { + return line; + } + + public RGB getPositive() { + return positive; + } + + public RGB getNegative() { + return negative; + } + + public RGB getOutline() { + return outline; + } + + public RGB getGrid() { + return grid; + } + + public RGB getBackground() { + return background; + } +} diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartThemes.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartThemes.java new file mode 100644 index 000000000..c228a7e98 --- /dev/null +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartThemes.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2004-2011 Marco Maccaferri and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Marco Maccaferri - initial API and implementation + */ + +package org.eclipsetrader.ui.charts; + +import org.eclipse.swt.graphics.RGB; + +/** + * Holds the available chart themes. + * + * @since 1.0 + */ +public final class ChartThemes { + + private static final ChartTheme DEFAULT = new ChartTheme( + new RGB(33, 150, 243), new RGB(38, 166, 154), new RGB(239, 83, 80), new RGB(64, 64, 64), new RGB(224, 224, 224), new RGB(255, 255, 255)); + + private ChartThemes() { + } + + public static ChartTheme getDefault() { + return DEFAULT; + } +} diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartUtils.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartUtils.java new file mode 100644 index 000000000..fc2e166a2 --- /dev/null +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/ChartUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2004-2011 Marco Maccaferri and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Marco Maccaferri - initial API and implementation + */ + +package org.eclipsetrader.ui.charts; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Canvas; + +/** + * Chart rendering utilities. + * + * @since 1.0 + */ +public final class ChartUtils { + + private ChartUtils() { + } + + /** + * Creates the offscreen image backing a chart canvas. The image is sized in + * logical (device-independent) pixels; SWT 4.31 auto-scales it to the + * device's native resolution at creation time. + * + * @param canvas the canvas the image backs + * @param bounds the logical size of the image + * @return the offscreen image + */ + public static Image createBackingImage(Canvas canvas, Rectangle bounds) { + return new Image(canvas.getDisplay(), bounds.width, bounds.height); + } + + /** + * Returns the display zoom (percent) of the canvas's monitor, falling back + * to the DPI-derived zoom when the monitor zoom is unavailable. + * + * @param canvas the canvas + * @return the zoom factor in percent (100 = no scaling) + */ + public static int getZoom(Canvas canvas) { + int zoom = canvas.getMonitor().getZoom(); + if (zoom <= 0) { + zoom = canvas.getDisplay().getDPI().x * 100 / 72; + } + return zoom; + } +} diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/DateScaleCanvas.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/DateScaleCanvas.java index 3cd98277a..3883138cf 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/DateScaleCanvas.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/DateScaleCanvas.java @@ -45,6 +45,7 @@ public class DateScaleCanvas { private Canvas horizontalScaleCanvas; private Image horizontalScaleImage; + private int horizontalScaleImageZoom; private Label label; private TimeSpan resolutionTimeSpan; @@ -136,8 +137,13 @@ private void onPaint(PaintEvent event) { horizontalScaleImage.dispose(); } } - if (horizontalScaleImage == null || horizontalScaleImage.isDisposed()) { - horizontalScaleImage = new Image(horizontalScaleCanvas.getDisplay(), clientArea.width, clientArea.height); + int zoom = ChartUtils.getZoom(horizontalScaleCanvas); + if (horizontalScaleImage == null || horizontalScaleImage.isDisposed() || horizontalScaleImageZoom != zoom) { + if (horizontalScaleImage != null && !horizontalScaleImage.isDisposed()) { + horizontalScaleImage.dispose(); + } + horizontalScaleImage = ChartUtils.createBackingImage(horizontalScaleCanvas, clientArea); + horizontalScaleImageZoom = zoom; needsRedraw = true; } diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/Graphics.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/Graphics.java index 3f2a1f193..f971a2acb 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/Graphics.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/Graphics.java @@ -43,6 +43,8 @@ public class Graphics implements IGraphics { public Graphics(Drawable drawable, Point location, IAxis horizontalAxis, IAxis verticalAxis) { this.gc = new GC(drawable); + this.gc.setAntialias(SWT.ON); + this.gc.setTextAntialias(SWT.ON); this.horizontalAxis = horizontalAxis; this.verticalAxis = verticalAxis; diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramAreaChart.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramAreaChart.java index 249605683..af7f19f7d 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramAreaChart.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramAreaChart.java @@ -23,6 +23,7 @@ import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.eclipsetrader.core.charts.IDataSeries; +import org.eclipsetrader.core.charts.ScalarDownsampler; import org.eclipsetrader.core.feed.IOHLC; /** @@ -36,11 +37,14 @@ public class HistogramAreaChart implements IChartObject, ISummaryBarDecorator, I private OHLCField field; private IAdaptable[] values; + private Date firstDate; + private Date lastDate; + private int pixelWidth; private List pointArray = new ArrayList(2048); private boolean valid; private boolean focus; - private RGB color = new RGB(0, 0, 0); + private RGB color = ChartThemes.getDefault().getLine(); private RGB fillColor; private SummaryDateItem dateItem; @@ -81,6 +85,10 @@ public void setColor(RGB color) { */ @Override public void setDataBounds(DataBounds dataBounds) { + if (isSameRange(dataBounds)) { + return; + } + List l = new ArrayList(2048); for (IAdaptable value : dataSeries.getValues()) { Date date = (Date) value.getAdapter(Date.class); @@ -88,16 +96,36 @@ public void setDataBounds(DataBounds dataBounds) { l.add(value); } } - this.values = l.toArray(new IAdaptable[l.size()]); + IAdaptable[] visible = l.toArray(new IAdaptable[l.size()]); + this.values = ScalarDownsampler.downsample(visible, dataBounds.width); + this.firstDate = dataBounds.first; + this.lastDate = dataBounds.last; + this.pixelWidth = dataBounds.width; this.valid = false; } + private boolean isSameRange(DataBounds dataBounds) { + if (values == null || pixelWidth != dataBounds.width) { + return false; + } + if (firstDate != dataBounds.first && (firstDate == null || !firstDate.equals(dataBounds.first))) { + return false; + } + if (lastDate != dataBounds.last && (lastDate == null || !lastDate.equals(dataBounds.last))) { + return false; + } + return true; + } + /* (non-Javadoc) * @see org.eclipsetrader.ui.charts.IChartObject#invalidate() */ @Override public void invalidate() { this.valid = false; + this.values = null; + this.firstDate = null; + this.lastDate = null; } /* (non-Javadoc) diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramBarChart.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramBarChart.java index 70dbe046c..531c27303 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramBarChart.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/HistogramBarChart.java @@ -21,6 +21,7 @@ import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.eclipsetrader.core.charts.IDataSeries; +import org.eclipsetrader.core.charts.ScalarDownsampler; /** * Draw an historgram bar chart. @@ -32,10 +33,13 @@ public class HistogramBarChart implements IChartObject, ISummaryBarDecorator, IA private IDataSeries dataSeries; private int width = 5; - private RGB positiveColor = new RGB(0, 254, 0); - private RGB negativeColor = new RGB(254, 0, 0); + private RGB positiveColor = ChartThemes.getDefault().getPositive(); + private RGB negativeColor = ChartThemes.getDefault().getNegative(); private IAdaptable[] values; + private Date firstDate; + private Date lastDate; + private int pixelWidth; private List pointArray = new ArrayList(2048); private boolean valid; private boolean hasFocus; @@ -58,6 +62,11 @@ public HistogramBarChart(IDataSeries dataSeries) { */ @Override public void setDataBounds(DataBounds dataBounds) { + this.width = dataBounds.horizontalSpacing - 1; + if (isSameRange(dataBounds)) { + return; + } + List l = new ArrayList(2048); for (IAdaptable value : dataSeries.getValues()) { Date date = (Date) value.getAdapter(Date.class); @@ -65,17 +74,36 @@ public void setDataBounds(DataBounds dataBounds) { l.add(value); } } - this.values = l.toArray(new IAdaptable[l.size()]); - this.width = dataBounds.horizontalSpacing - 1; + IAdaptable[] visible = l.toArray(new IAdaptable[l.size()]); + this.values = ScalarDownsampler.downsample(visible, dataBounds.width); + this.firstDate = dataBounds.first; + this.lastDate = dataBounds.last; + this.pixelWidth = dataBounds.width; this.valid = false; } + private boolean isSameRange(DataBounds dataBounds) { + if (values == null || pixelWidth != dataBounds.width) { + return false; + } + if (firstDate != dataBounds.first && (firstDate == null || !firstDate.equals(dataBounds.first))) { + return false; + } + if (lastDate != dataBounds.last && (lastDate == null || !lastDate.equals(dataBounds.last))) { + return false; + } + return true; + } + /* (non-Javadoc) * @see org.eclipsetrader.ui.charts.IChartObject#invalidate() */ @Override public void invalidate() { this.valid = false; + this.values = null; + this.firstDate = null; + this.lastDate = null; } /* (non-Javadoc) diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/LineChart.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/LineChart.java index 6826a3657..97968ac3b 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/LineChart.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/LineChart.java @@ -22,6 +22,7 @@ import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.eclipsetrader.core.charts.IDataSeries; +import org.eclipsetrader.core.charts.ScalarDownsampler; /** * Draws a line chart. @@ -33,10 +34,13 @@ public class LineChart implements IChartObject, ISummaryBarDecorator, IAdaptable private IDataSeries dataSeries; private LineStyle style; - private RGB color; + private RGB color = ChartThemes.getDefault().getLine(); private int width = 5; private IAdaptable[] values; + private Date firstDate; + private Date lastDate; + private int pixelWidth; private Point[] pointArray; private boolean valid; private boolean hasFocus; @@ -52,7 +56,9 @@ public static enum LineStyle { public LineChart(IDataSeries dataSeries, LineStyle style, RGB color) { this.dataSeries = dataSeries; this.style = style; - this.color = color; + if (color != null) { + this.color = color; + } numberFormat.setGroupingUsed(true); numberFormat.setMinimumIntegerDigits(1); @@ -73,6 +79,11 @@ public void setColor(RGB color) { */ @Override public void setDataBounds(DataBounds dataBounds) { + this.width = dataBounds.horizontalSpacing; + if (isSameRange(dataBounds)) { + return; + } + List l = new ArrayList(2048); for (IAdaptable value : dataSeries.getValues()) { Date date = (Date) value.getAdapter(Date.class); @@ -80,11 +91,27 @@ public void setDataBounds(DataBounds dataBounds) { l.add(value); } } - this.values = l.toArray(new IAdaptable[l.size()]); - this.width = dataBounds.horizontalSpacing; + IAdaptable[] visible = l.toArray(new IAdaptable[l.size()]); + this.values = ScalarDownsampler.downsample(visible, dataBounds.width); + this.firstDate = dataBounds.first; + this.lastDate = dataBounds.last; + this.pixelWidth = dataBounds.width; this.valid = false; } + private boolean isSameRange(DataBounds dataBounds) { + if (values == null || pixelWidth != dataBounds.width) { + return false; + } + if (firstDate != dataBounds.first && (firstDate == null || !firstDate.equals(dataBounds.first))) { + return false; + } + if (lastDate != dataBounds.last && (lastDate == null || !lastDate.equals(dataBounds.last))) { + return false; + } + return true; + } + /* (non-Javadoc) * @see org.eclipsetrader.ui.charts.IChartObject#handleFocusGained(org.eclipsetrader.ui.charts.ChartObjectFocusEvent) */ @@ -111,6 +138,9 @@ protected boolean hasFocus() { @Override public void invalidate() { this.valid = false; + this.values = null; + this.firstDate = null; + this.lastDate = null; } /* (non-Javadoc) diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/OHLCLineChart.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/OHLCLineChart.java index b0ddac3b1..d94c157fe 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/OHLCLineChart.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/charts/OHLCLineChart.java @@ -23,6 +23,7 @@ import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Composite; import org.eclipsetrader.core.charts.IDataSeries; +import org.eclipsetrader.core.charts.OHLCDownsampler; import org.eclipsetrader.core.feed.IOHLC; /** @@ -35,10 +36,13 @@ public class OHLCLineChart implements IChartObject, ISummaryBarDecorator, IAdapt private IDataSeries dataSeries; private LineStyle style; - private RGB color; + private RGB color = ChartThemes.getDefault().getLine(); private int width = 5; private IAdaptable[] values; + private Date firstDate; + private Date lastDate; + private int pixelWidth; private Point[] pointArray; private boolean valid; private boolean hasFocus; @@ -56,7 +60,9 @@ public static enum LineStyle { public OHLCLineChart(IDataSeries dataSeries, LineStyle style, RGB color) { this.dataSeries = dataSeries; this.style = style; - this.color = color; + if (color != null) { + this.color = color; + } numberFormat.setGroupingUsed(true); numberFormat.setMinimumIntegerDigits(1); @@ -77,6 +83,11 @@ public void setColor(RGB color) { */ @Override public void setDataBounds(DataBounds dataBounds) { + this.width = dataBounds.horizontalSpacing; + if (isSameRange(dataBounds)) { + return; + } + List l = new ArrayList(2048); for (IAdaptable value : dataSeries.getValues()) { Date date = (Date) value.getAdapter(Date.class); @@ -84,11 +95,38 @@ public void setDataBounds(DataBounds dataBounds) { l.add(value); } } - this.values = l.toArray(new IAdaptable[l.size()]); - this.width = dataBounds.horizontalSpacing; + IAdaptable[] visible = l.toArray(new IAdaptable[l.size()]); + this.values = OHLCDownsampler.downsample(visible, dataBounds.width); + this.firstDate = dataBounds.first; + this.lastDate = dataBounds.last; + this.pixelWidth = dataBounds.width; this.valid = false; } + private boolean isSameRange(DataBounds dataBounds) { + if (values == null || pixelWidth != dataBounds.width) { + return false; + } + if (firstDate != dataBounds.first && (firstDate == null || !firstDate.equals(dataBounds.first))) { + return false; + } + if (lastDate != dataBounds.last && (lastDate == null || !lastDate.equals(dataBounds.last))) { + return false; + } + return true; + } + + /* (non-Javadoc) + * @see org.eclipsetrader.ui.charts.IChartObject#invalidate() + */ + @Override + public void invalidate() { + this.valid = false; + this.values = null; + this.firstDate = null; + this.lastDate = null; + } + /* (non-Javadoc) * @see org.eclipsetrader.ui.charts.IChartObject#handleFocusGained(org.eclipsetrader.ui.charts.ChartObjectFocusEvent) */ @@ -109,14 +147,6 @@ protected boolean hasFocus() { return hasFocus; } - /* (non-Javadoc) - * @see org.eclipsetrader.ui.charts.IChartObject#invalidate() - */ - @Override - public void invalidate() { - this.valid = false; - } - /* (non-Javadoc) * @see org.eclipsetrader.ui.charts.IChartObject#paint(org.eclipsetrader.ui.charts.IGraphics) */ diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/TraderPerspective.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/TraderPerspective.java index 4d48a3ee5..0441aa6b0 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/TraderPerspective.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/TraderPerspective.java @@ -49,6 +49,8 @@ public void createInitialLayout(IPageLayout layout) { // Right. IPlaceholderFolderLayout right = layout.createPlaceholderFolder("right", IPageLayout.RIGHT, (float) 0.75, UIConstants.EDITOR_AREA); //$NON-NLS-1$ right.addPlaceholder("org.eclipsetrader.ui.views.level2:*"); //$NON-NLS-1$ + right.addPlaceholder("org.eclipsetrader.ui.views.orders:*"); //$NON-NLS-1$ + right.addPlaceholder("org.eclipsetrader.ui.views.tickers:*"); //$NON-NLS-1$ // Add "new wizards". layout.addNewWizardShortcut("org.eclipsetrader.ui.wizards.new.stock");//$NON-NLS-1$ @@ -59,8 +61,17 @@ public void createInitialLayout(IPageLayout layout) { layout.addShowViewShortcut("org.eclipsetrader.ui.views.navigator"); //$NON-NLS-1$ layout.addShowViewShortcut("org.eclipsetrader.ui.views.markets"); //$NON-NLS-1$ layout.addShowViewShortcut("org.eclipsetrader.ui.views.repositories"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.watchlist"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.level2"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.orders"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.tickers"); //$NON-NLS-1$ // Add default action sets layout.addActionSet("org.eclipsetrader.ui.launcher"); + layout.addActionSet("org.eclipsetrader.ui.charts.tools"); + layout.addActionSet("org.eclipsetrader.ui.charts.zoom"); + + // Add "perspectives". + layout.addPerspectiveShortcut("org.eclipsetrader.ui.charts"); //$NON-NLS-1$ } } diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/ChartsPerspective.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/ChartsPerspective.java index 4501929fc..4ef8ee3ea 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/ChartsPerspective.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/ChartsPerspective.java @@ -49,8 +49,16 @@ public void createInitialLayout(IPageLayout layout) { layout.addView("org.eclipsetrader.ui.charts.palette", IPageLayout.BOTTOM, (float) 0.50, "left"); //$NON-NLS-1$ //$NON-NLS-2$ // Bottom - IPlaceholderFolderLayout bottom = layout.createPlaceholderFolder("bottom", IPageLayout.BOTTOM, (float) 0.75, UIConstants.EDITOR_AREA); //$NON-NLS-1$ + IFolderLayout bottom = layout.createFolder("bottom", IPageLayout.BOTTOM, (float) 0.75, UIConstants.EDITOR_AREA); //$NON-NLS-1$ + bottom.addView("org.eclipsetrader.ui.views.markets"); //$NON-NLS-1$ bottom.addPlaceholder("org.eclipse.ui.views.ProgressView"); //$NON-NLS-1$ + bottom.addPlaceholder("org.eclipsetrader.ui.views.orders:*"); //$NON-NLS-1$ + + // Right. + IPlaceholderFolderLayout right = layout.createPlaceholderFolder("right", IPageLayout.RIGHT, (float) 0.75, UIConstants.EDITOR_AREA); //$NON-NLS-1$ + right.addPlaceholder("org.eclipsetrader.ui.views.level2:*"); //$NON-NLS-1$ + right.addPlaceholder("org.eclipsetrader.ui.views.tickers:*"); //$NON-NLS-1$ + right.addPlaceholder("org.eclipsetrader.ui.views.watchlist:*"); //$NON-NLS-1$ // Add "new wizards". layout.addNewWizardShortcut("org.eclipsetrader.ui.wizards.new.security");//$NON-NLS-1$ @@ -64,6 +72,10 @@ public void createInitialLayout(IPageLayout layout) { layout.addShowViewShortcut("org.eclipsetrader.ui.views.navigator"); //$NON-NLS-1$ layout.addShowViewShortcut("org.eclipsetrader.ui.views.markets"); //$NON-NLS-1$ layout.addShowViewShortcut("org.eclipsetrader.ui.views.repositories"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.watchlist"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.level2"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.orders"); //$NON-NLS-1$ + layout.addShowViewShortcut("org.eclipsetrader.ui.views.tickers"); //$NON-NLS-1$ // Add "perspectives". layout.addPerspectiveShortcut("org.eclipsetrader.ui.traderPerspective"); //$NON-NLS-1$ diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/ChartViewPart.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/ChartViewPart.java index fb59946b3..ea456ad98 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/ChartViewPart.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/ChartViewPart.java @@ -96,6 +96,7 @@ import org.eclipsetrader.ui.charts.ChartViewItem; import org.eclipsetrader.ui.charts.IChartEditorListener; import org.eclipsetrader.ui.charts.IChartObject; +import org.eclipsetrader.ui.charts.IChartObjectFactory; import org.eclipsetrader.ui.internal.UIActivator; import org.eclipsetrader.ui.internal.charts.DataImportJob; import org.eclipsetrader.ui.internal.charts.ImportDataPage; @@ -149,6 +150,11 @@ public class ChartViewPart extends ViewPart implements ISaveablePart { private CurrentBookFactory currentBookFactory; private TradeFactory tradeFactory; + private Action candleAction; + private Action barAction; + private Action lineAction; + private Action histogramAction; + IMemento memento; IPreferenceStore preferenceStore; @@ -335,7 +341,18 @@ public void init(IViewSite site, IMemento memento) throws PartInitException { IToolBarManager toolBarManager = actionBars.getToolBarManager(); toolBarManager.add(new Separator("additions")); //$NON-NLS-1$ toolBarManager.add(updateAction); - + toolBarManager.add(new Separator()); + toolBarManager.add(candleAction); + toolBarManager.add(barAction); + toolBarManager.add(lineAction); + toolBarManager.add(histogramAction); + toolBarManager.add(new Separator()); + if (periodActions != null) { + for (int i = 0; i < periodActions.length; i++) { + toolBarManager.add(periodActions[i]); + } + } + if (dialogSettings != null) { TimeSpan periodTimeSpan = TimeSpan.fromString(dialogSettings.get(K_PERIOD)); TimeSpan resolutionTimeSpan = TimeSpan.fromString(dialogSettings.get(K_RESOLUTION)); @@ -471,7 +488,7 @@ public void run() { public void run() { } }; - pasteAction.setId("copy"); //$NON-NLS-1$ + pasteAction.setId("paste"); //$NON-NLS-1$ pasteAction.setActionDefinitionId("org.eclipse.ui.edit.paste"); //$NON-NLS-1$ pasteAction.setImageDescriptor(sharedImages.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE)); pasteAction.setDisabledImageDescriptor(sharedImages.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE_DISABLED)); @@ -556,6 +573,61 @@ public void run() { } } }; + + candleAction = new Action("Candlestick", IAction.AS_RADIO_BUTTON) { + + @Override + public void run() { + switchChartType(MainRenderStyle.Candles); + } + }; + candleAction.setToolTipText(Messages.ChartViewPart_CandlestickAction); + + barAction = new Action("OHLC Bars", IAction.AS_RADIO_BUTTON) { + + @Override + public void run() { + switchChartType(MainRenderStyle.Bars); + } + }; + barAction.setToolTipText(Messages.ChartViewPart_BarAction); + + lineAction = new Action("Line", IAction.AS_RADIO_BUTTON) { + + @Override + public void run() { + switchChartType(MainRenderStyle.Line); + } + }; + lineAction.setToolTipText(Messages.ChartViewPart_LineChartAction); + + histogramAction = new Action("Area", IAction.AS_RADIO_BUTTON) { + + @Override + public void run() { + switchChartType(MainRenderStyle.Histogram); + } + }; + histogramAction.setToolTipText(Messages.ChartViewPart_HistogramAction); + } + + void switchChartType(MainRenderStyle style) { + IViewItem[] rows = view.getItems(); + for (int i = 0; i < rows.length; i++) { + ChartRowViewItem rowItem = (ChartRowViewItem) rows[i]; + IViewItem[] children = rowItem.getItems(); + if (children != null) { + for (int j = 0; j < children.length; j++) { + ChartViewItem viewItem = (ChartViewItem) children[j]; + IChartObjectFactory factory = viewItem.getFactory(); + if (factory instanceof MainChartFactory) { + ((MainChartFactory) factory).setStyle(style); + rowItem.refresh(); + } + } + } + } + refreshChart(); } /* (non-Javadoc) @@ -658,6 +730,23 @@ void createContextMenu() { @Override public void menuAboutToShow(IMenuManager menuManager) { menuManager.add(new Separator("top")); //$NON-NLS-1$ + + MenuManager chartTypeMenu = new MenuManager(Messages.ChartViewPart_ChartTypeMenu); + chartTypeMenu.add(candleAction); + chartTypeMenu.add(barAction); + chartTypeMenu.add(lineAction); + chartTypeMenu.add(histogramAction); + menuManager.add(chartTypeMenu); + + MenuManager periodMenu = new MenuManager(Messages.ChartViewPart_PeriodMenu); + if (periodActions != null) { + for (int i = 0; i < periodActions.length; i++) { + periodMenu.add(periodActions[i]); + } + } + menuManager.add(periodMenu); + + menuManager.add(new Separator()); menuManager.add(cutAction); menuManager.add(copyAction); menuManager.add(pasteAction); @@ -1024,8 +1113,28 @@ public int compare(Period o1, Period o2) { periodActions[i] = new ContributionItem(list.get(i)); } } catch (Exception e) { + periodActions = createDefaultPeriodActions(); e.printStackTrace(); } + if (periodActions == null) { + periodActions = createDefaultPeriodActions(); + } + } + + private ContributionItem[] createDefaultPeriodActions() { + PeriodList list = new PeriodList(); + list.add(new Period("2 Years", TimeSpan.years(2), TimeSpan.days(1))); + list.add(new Period("1 Year", TimeSpan.years(1), TimeSpan.days(1))); + list.add(new Period("6 Months", TimeSpan.months(6), TimeSpan.days(1))); + list.add(new Period("3 Months", TimeSpan.months(3), TimeSpan.days(1))); + list.add(new Period("1 Month", TimeSpan.months(1), TimeSpan.days(1))); + list.add(new Period("5 Days", TimeSpan.days(5), TimeSpan.minutes(5))); + list.add(new Period("1 Day", TimeSpan.days(1), TimeSpan.minutes(1))); + ContributionItem[] actions = new ContributionItem[list.size()]; + for (int i = 0; i < actions.length; i++) { + actions[i] = new ContributionItem(list.get(i)); + } + return actions; } public void setPeriodActionSelection(TimeSpan period, TimeSpan resolution) { diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainChartFactory.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainChartFactory.java index f57d19f9b..1f4d425a8 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainChartFactory.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainChartFactory.java @@ -184,7 +184,7 @@ public IChartParameters getParameters() { */ @Override public void setParameters(IChartParameters parameters) { - style = parameters.hasParameter("style") ? MainRenderStyle.getStyleFromName(parameters.getString("style")) : MainRenderStyle.Bars; + style = parameters.hasParameter("style") ? MainRenderStyle.getStyleFromName(parameters.getString("style")) : MainRenderStyle.Candles; lineColor = parameters.getColor("line-color"); diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainPropertiesPage.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainPropertiesPage.java index 6692541a4..b2f8949d8 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainPropertiesPage.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/MainPropertiesPage.java @@ -15,7 +15,6 @@ import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; -import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; @@ -23,6 +22,7 @@ import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.ui.dialogs.PropertyPage; +import org.eclipsetrader.ui.charts.ChartThemes; public class MainPropertiesPage extends PropertyPage { @@ -77,7 +77,7 @@ public void widgetSelected(SelectionEvent e) { label = new Label(content, SWT.NONE); label.setText("Line"); lineColor = new ColorSelector(content); - lineColor.setColorValue(new RGB(0, 0, 255)); + lineColor.setColorValue(ChartThemes.getDefault().getLine()); lineColor.getButton().setData("label", label); label = new Label(content, SWT.NONE); label.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false)); @@ -85,7 +85,7 @@ public void widgetSelected(SelectionEvent e) { label = new Label(content, SWT.NONE); label.setText("Bars"); barPositiveColor = new ColorSelector(content); - barPositiveColor.setColorValue(new RGB(0, 0, 255)); + barPositiveColor.setColorValue(ChartThemes.getDefault().getPositive()); barPositiveColor.getButton().setData("label", label); label = new Label(content, SWT.NONE); label.setText("Positive"); @@ -94,7 +94,7 @@ public void widgetSelected(SelectionEvent e) { label = new Label(content, SWT.NONE); barNegativeColor = new ColorSelector(content); - barNegativeColor.setColorValue(new RGB(0, 0, 255)); + barNegativeColor.setColorValue(ChartThemes.getDefault().getNegative()); barNegativeColor.getButton().setData("label", label); label = new Label(content, SWT.NONE); label.setText("Negative"); @@ -104,7 +104,7 @@ public void widgetSelected(SelectionEvent e) { label = new Label(content, SWT.NONE); label.setText("Candles"); candlePositiveColor = new ColorSelector(content); - candlePositiveColor.setColorValue(new RGB(0, 0, 255)); + candlePositiveColor.setColorValue(ChartThemes.getDefault().getPositive()); candlePositiveColor.getButton().setData("label", label); label = new Label(content, SWT.NONE); label.setText("Positive"); @@ -113,7 +113,7 @@ public void widgetSelected(SelectionEvent e) { label = new Label(content, SWT.NONE); candleNegativeColor = new ColorSelector(content); - candleNegativeColor.setColorValue(new RGB(0, 0, 255)); + candleNegativeColor.setColorValue(ChartThemes.getDefault().getNegative()); candleNegativeColor.getButton().setData("label", label); label = new Label(content, SWT.NONE); label.setText("Negative"); @@ -122,7 +122,7 @@ public void widgetSelected(SelectionEvent e) { label = new Label(content, SWT.NONE); candleOutlineColor = new ColorSelector(content); - candleOutlineColor.setColorValue(new RGB(0, 0, 255)); + candleOutlineColor.setColorValue(ChartThemes.getDefault().getOutline()); candleOutlineColor.getButton().setData("label", label); label = new Label(content, SWT.NONE); label.setText("Outline"); diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/Messages.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/Messages.java index d11dcb6de..0b3235077 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/Messages.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/Messages.java @@ -26,6 +26,12 @@ public class Messages extends NLS { public static String ChartViewPart_UpdateAction; public static String ChartViewPart_ZoomInAction; public static String ChartViewPart_ZoomOutAction; + public static String ChartViewPart_CandlestickAction; + public static String ChartViewPart_BarAction; + public static String ChartViewPart_LineChartAction; + public static String ChartViewPart_HistogramAction; + public static String ChartViewPart_ChartTypeMenu; + public static String ChartViewPart_PeriodMenu; public static String CurrentBookFactory_Name; public static String CurrentPriceLineFactory_Name; public static String CustomPeriodDialog_BeginDateLabel; diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/messages.properties b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/messages.properties index 43766ac1d..220417068 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/messages.properties +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/charts/views/messages.properties @@ -19,6 +19,12 @@ ChartViewPart_ShowCurrentPriceAction=Show current price ChartViewPart_UpdateAction=Update ChartViewPart_ZoomInAction=Zoom-In ChartViewPart_ZoomOutAction=Zoom-Out +ChartViewPart_CandlestickAction=Candlestick +ChartViewPart_BarAction=OHLC Bars +ChartViewPart_LineChartAction=Line +ChartViewPart_HistogramAction=Area +ChartViewPart_ChartTypeMenu=Chart Type +ChartViewPart_PeriodMenu=Period CurrentBookFactory_Name=Current Price CurrentPriceLineFactory_Name=Current Price CustomPeriodDialog_BeginDateLabel=Begin Date diff --git a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/views/Level2View.java b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/views/Level2View.java index 41dcf18fd..6d9857c29 100644 --- a/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/views/Level2View.java +++ b/org.eclipsetrader.ui/src/org/eclipsetrader/ui/internal/views/Level2View.java @@ -346,6 +346,15 @@ public void linkExited(HyperlinkEvent e) { activeConnector.setText(this.connector.getName()); } } + if (this.connector == null) { + IFeedConnector connector = CoreActivator.getDefault().getDefaultConnector(); + if (connector instanceof IFeedConnector2) { + this.connector = (IFeedConnector2) connector; + if (this.connector != null) { + activeConnector.setText(this.connector.getName()); + } + } + } if (s != null && connector != null) { subscription = this.connector.subscribeLevel2(s); diff --git a/org.jdom/.tycho-consumer-pom.xml b/org.jdom/.tycho-consumer-pom.xml index d681ccd87..125ee927a 100644 --- a/org.jdom/.tycho-consumer-pom.xml +++ b/org.jdom/.tycho-consumer-pom.xml @@ -6,14 +6,6 @@ org.jdom 1.0.0-SNAPSHOT JDOM (External) - Delegates to Maven Central version of JDOM 2.x. - - - net.sf.cglib - net.sf.cglib - 3.3.0 - compile - false - - + Delegates to embedded jdom-1.1.3.jar. + diff --git a/org.jdom/build.properties b/org.jdom/build.properties index 9ac5b3415..5dcd575dc 100644 --- a/org.jdom/build.properties +++ b/org.jdom/build.properties @@ -1,2 +1,3 @@ +output.. = bin/ bin.includes = META-INF/,\ libs/jdom-1.1.3.jar diff --git a/pom.xml b/pom.xml index 6eca6a7c3..a76f9c018 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,9 @@ org.eclipsetrader.directaworld-feature org.eclipsetrader.jessx org.eclipsetrader.jessx.test + org.eclipsetrader.market.sim + org.eclipsetrader.market.sim.tests + org.eclipsetrader.market.sim-feature org.eclipsetrader.jessx-feature org.eclipsetrader.news org.eclipsetrader.platform