15 Commits

Author SHA1 Message Date
mo
2be9c843e5 Archive stale docs and mark historical references 2026-04-05 17:38:35 -04:00
mo
a6ececaf73 Merge branch 'cleanup/docs-governance' into cleanup/archive-stale-docs 2026-04-05 17:30:16 -04:00
mo
ce74f68e54 Establish governance docs as canonical repo context 2026-04-05 17:24:48 -04:00
mo
3ccd3a8bfd Create canonical governance documentation files 2026-04-05 16:54:22 -04:00
mo
e2ea45b58f Normalize line endings (CRLF/LF) 2026-04-05 16:52:09 -04:00
mo
9a28a49292 Pre-cleanup baseline snapshot
Some checks failed
Build and Test / build (push) Has been cancelled
2026-04-05 16:50:18 -04:00
mo
d856f3949d fix: restore EntriesPerDirection=2 and align risk defaults
Some checks failed
Build and Test / build (push) Has been cancelled
- Restore EntriesPerDirection=2 so runner leg can enter alongside scaler.
  Replay burst protection is now handled by the State.Realtime guard in
  ProcessStrategyIntent rather than by limiting entries per direction.

- Set MaxOpenPositions=2 in SimpleORBNT8 to match scaler+runner structure.
  Previous value of 1 caused PortfolioRiskManager to block the runner.

- Confirm RiskPerTrade=100 and MaxContracts=3 as live defaults.
  The /9-contract configuration was a one-off backtest experiment and
  must not be the deployed default.

- _realtimeBarSeen field and OnBarUpdate guard confirmed present and correct.
  ProcessStrategyIntent guard: if (State == State.Realtime && !_realtimeBarSeen)
  allows backtest (State.Historical) to execute normally while blocking
  replay bars in live/SIM mode.

Backtest validation: Jan 2026 - Mar 2026, NQ, trail=20ticks
  PF=7.00, win=75%, avg winner=, avg loser=-, max DD=-
2026-03-29 19:18:29 -04:00
mo
c9e8b35f15 Fix stop/target ordering, P&L wiring, dynamic targets, confluence wiring
Some checks failed
Build and Test / build (push) Has been cancelled
- Move SetStopLoss/SetProfitTarget before EnterLong/EnterShort in
  SubmitOrderToNT8(). NT8 requires stop/target pre-registered to the
  signal name before entry fires — calling after caused silent fallback
  to strategy defaults in backtest, nullifying all dynamic target scaling.
  Add [dynamic] tag to SUBMIT log line for observability.

- Wire OnPositionUpdate() in NT8StrategyBase to call
  _riskManager.OnPnLUpdate() and PortfolioRiskManager.ReportPnL() on
  every position change. Daily loss limit checks were evaluating against
   PnL permanently — safety net now live.

- Fix BasicRiskManager.CheckEmergencyConditions() hardcoded 1000 limit.
  Add dailyLossLimit constructor overload; emergency halt now fires at
  90% of the configured DailyLossLimit, not a hardcoded .

- Add AVWAP slope hard veto in SimpleORBStrategy.OnBar(). Trend=0.00
  (AVWAP working against trade direction) can no longer pass grade
  filter. Pre-scoring veto prevents high gap/volume scores from masking
  a directionally broken setup.

- Raise default MinTradeGrade from 4 (B) to 5 (A) in both
  SimpleORBStrategy and SimpleORBNT8 defaults.

- Scale TargetTicks dynamically in CreateIntent() based on ORB/ATR
  ratio (0.75x-1.75x of base target). Tight ORBs get extended targets;
  wide ORBs get tightened. Hard floor at stopTicks+4.

- Add NarrowRange, OrbRangeVsAtr, GapDirectionAlignment,
  BreakoutVolumeStrength, PriorDayCloseStrength default weights in
  ConfluenceScorer constructor. All 10 factors now registered.

- Fix session_open_price tracking for GapDirectionAlignment factor.
  TodayOpen in DailyBarContext now reflects first RTH bar open, not
  the breakout bar open.

- Log all 10 factor scores at signal acceptance for observability.
2026-03-23 22:43:21 -04:00
mo
ae8ac05017 Baseline v1: fix multi-trade bug, working confluence factors, PF=1.12 over 15 months
Some checks failed
Build and Test / build (push) Has been cancelled
- Fix BreakoutVolumeStrength: was always 0.50 due to self-referential volume comparison
- Fix VolatilityRegime: was always ~0.50 due to single-bar value area baseline
- Fix multi-trade-per-day: session reset now compares trading date not session timestamp
- Add NT8.Strategies.dll to deploy script (was missing from every deployment)
- Fix grade filter minimum from C to B (MinTradeGrade=4)
- Add MinTradeGrade as configurable NinjaScript property
- Fix confluence_score metadata: now stores full ConfluenceScore object not double
- Baseline: B-grade long-only 30min ORB on ES, Jan 2025-Mar 2026
  120 trades, 35.8% win rate, PF=1.12, Sharpe=0.18, MaxDD=
2026-03-22 19:26:17 -04:00
mo
229f4e413e Fix BreakoutVolumeStrength and Volatility factor calculators, session reset gate
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-22 19:12:08 -04:00
mo
c094e65b10 Grade filter B minimum, MinTradeGrade property, session reset fix
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-22 18:56:06 -04:00
mo
a2af272d73 Fix deploy script: add NT8.Strategies.dll to deployment pipeline
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-22 17:28:03 -04:00
mo
2f623dc2f8 S2-B3/S3-05: PortfolioRiskManager, connection loss recovery, long-only lock
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-19 16:17:02 -04:00
mo
3282254572 S2-B1/B2: live account balance, Optimizable attrs, BarsRequired=50, archive NT8.Core.Orders
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-19 14:48:22 -04:00
mo
498f298975 Add ORB confluence factors (NR4/NR7, gap alignment, breakout volume, prior close) + session file logger
Some checks failed
Build and Test / build (push) Has been cancelled
2026-03-19 12:16:39 -04:00
45 changed files with 3693 additions and 160 deletions

BIN
.gitattributes vendored Normal file

Binary file not shown.

View File

@@ -1,9 +1,66 @@
# Coding Patterns — NT8 SDK Required Patterns
**Last Updated:** 2026-03-27
All code in the NT8 SDK MUST follow these patterns without exception.
---
## 0. C# 5.0 Hard Constraints (NinjaScript Compiler)
```csharp
// ❌ PROHIBITED — compiler will fail silently or error
$"Hello {name}" // no string interpolation
obj?.Method() // no null-conditional
public int Prop => _value; // no expression body
nameof(SomeClass) // no nameof
await SomeAsync() // no async/await
// ✅ REQUIRED
string.Format("Hello {0}", name)
obj != null ? obj.Method() : null
public int Prop { get { return _value; } }
"SomeClass" // string literal
// synchronous only
```
---
## 0b. NT8-Specific Critical Rules
```csharp
// SetStopLoss/SetProfitTarget MUST come BEFORE EnterLong/EnterShort
// Calling them after is silently ignored in backtest
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false); // FIRST
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks); // SECOND
EnterShort(qty, signalName); // THIRD
// OnBarUpdate must guard secondary series
protected override void OnBarUpdate()
{
if (BarsInProgress != 0) return; // CRITICAL: ignore daily bar series updates
// ...
}
// State guard in ProcessStrategyIntent
// Allows Historical (backtest), blocks Realtime replay burst:
if (State == State.Realtime && !_realtimeBarSeen)
return;
```
---
## Sizing Formula
```
contracts = floor(RiskPerTrade / (StopTicks × TickValue))
NQ tick value = $5.00
$100 / (8 × $5) = 2 contracts
$200 / (8 × $5) = 5 contracts (capped at MaxContracts)
Always verify: RiskPerTrade ≤ MaxTradeRisk
```
---
## 1. Thread Safety — Lock Everything Shared
Every class with shared state must have a lock object:

View File

@@ -124,3 +124,8 @@ Math.Floor(x); // (via standard using System;)
- Search for `=>` — if on a property or method, rewrite as full block
- Search for `nameof` — replace with string literal
- Search for `out var` — split into declaration + assignment
## NinjaScript attributes
`[Optimizable]` does not exist in NinjaScript. Use `[NinjaScriptProperty]`
and `[Range(min, max)]` instead. See nt8compilespec.md for full NT8 API rules.

View File

@@ -1,7 +1,108 @@
# NT8 Institutional SDK - Development Workflow
# NT8-SDK — Kilocode Development Workflow
**Last Updated:** 2026-03-27
## Overview
This document outlines the development workflow for the NT8 Institutional SDK, following the Archon workflow principles even in the absence of the Archon MCP server.
This is the authoritative workflow for all development work on NT8-SDK using Kilocode.
---
## Division of Labor
| Role | Responsibility |
|---|---|
| **Claude** | Architecture, diagnosis, Kilocode prompt authoring, sequencing |
| **Kilocode** | ALL code implementation — zero exceptions |
| **Mo** | Strategy direction, backtest execution, log collection, go/no-go |
---
## Per-Task Workflow
1. **Claude writes Kilocode prompt** — exact Find/Replace, both file paths, build command, validation checklist
2. **Mo runs Kilocode** — pastes prompt, Kilocode executes
3. **Kilocode reports** — build output + files changed
4. **Mo brings results to Claude** — Kilocode report + session log + CSV
5. **Claude diagnoses** — confirms or issues follow-up prompt
6. **Mo commits:** `git add``git commit``git push`
7. **Update SPRINT_BOARD** — task to Done or Blocked
---
## Kilocode Prompt Template
```
TASK: [one-line description]
CONTEXT:
[1-3 sentences explaining why]
FILES TO MODIFY:
1. C:\dev\nt8-sdk\src\... [repo path]
2. C:\Users\billy\...\Strategies\... [NT8 path — same change]
CHANGE 1 — [description]:
File: [path]
Find:
[exact existing code]
Replace with:
[new code]
BUILD & DEPLOY:
1. dotnet build NT8-SDK.sln --configuration Release
2. deployment\deploy-to-nt8.bat
3. NT8: Tools → Edit NinjaScript → open NT8StrategyBase.cs → save
VALIDATION:
- Run Strategy Analyzer: NQ JUN26, Jan 1 2026 → Mar 27 2026
- Look for in session log: [specific confirmation lines]
```
---
## Guardrails — Kilocode MUST NEVER
1. Modify files outside the task spec
2. Use C# 6+ syntax
3. Remove existing comments or XML documentation
4. Change interface signatures
5. Deploy without building first
6. Edit NT8 path without also updating repo path
7. Guess NT8 API signatures
8. Introduce async/await
---
## Log Analysis Quick Reference
**Healthy dual-leg trade in session log:**
```
SIGNAL Sell | Grade=A | Score=0.820
SUBMIT Scaler=1 Runner=1 Stop=8 Target=20
FILL Short 1 @ XXXXX <- scaler fill
PNL_UPDATE Position=Short Qty=1
FILL Short 1 @ XXXXX <- runner fill
PNL_UPDATE Position=Short Qty=2 <- CRITICAL: Qty=2 = runner entered
```
**Warning signs:**
- SUBMIT then only 1 FILL → runner blocked (check EntriesPerDirection + MaxOpenPositions)
- Multiple SIGNALs in milliseconds → replay burst (_realtimeBarSeen not working)
- SIGNAL then nothing → ProcessStrategyIntent guard blocking backtest
---
## Commit Message Format
```
feat: description <- new feature
fix: description <- bug fix
refactor: description <- no behavior change
test: description <- tests only
docs: description <- documentation only
```
---
## Original Archon Workflow (2025, archived below)
## Archon Workflow Principles

View File

@@ -137,3 +137,126 @@ Compile Checklist (Preflight)
""").format(date=datetime.date.today().isoformat())
files["NT8_Templates.md"] = textwrap.dedent("""
---
# NinjaScript Compiler Constraints
## CRITICAL: Two separate compilers exist in this project
This project uses TWO compilers with DIFFERENT rules:
1. **dotnet build** — compiles src/NT8.Core/ and src/NT8.Adapters/ as
standard .NET Framework 4.8 assemblies. Errors here show up in the
terminal.
2. **NinjaTrader NinjaScript compiler** — compiles the .cs files deployed
to `C:\Users\billy\Documents\NinjaTrader 8\bin\Custom\Strategies\`.
Errors here only show up inside the NT8 NinjaScript Editor, NOT in
dotnet build output.
**dotnet build passing does NOT mean NT8 will compile successfully.**
Always treat NT8 compilation as a required separate verification step.
---
## NinjaScript-specific API rules
### OnConnectionStatusUpdate — correct signature
```csharp
// CORRECT — single ConnectionStatusEventArgs parameter
protected override void OnConnectionStatusUpdate(
ConnectionStatusEventArgs connectionStatusUpdate)
{
connectionStatusUpdate.Status // ConnectionStatus enum
connectionStatusUpdate.PriceStatus // ConnectionStatus enum
}
// WRONG — this signature does not exist in NT8
protected override void OnConnectionStatusUpdate(
Connection connection,
ConnectionStatus status,
DateTime time) // CS0115 — no suitable method found to override
```
### [Optimizable] attribute — does not exist
```csharp
// WRONG — OptimizableAttribute does not exist in NinjaScript
[Optimizable]
public int StopTicks { get; set; } // CS0246
// CORRECT — all [NinjaScriptProperty] params are optimizer-eligible
// Use [Range] to set optimizer bounds
[NinjaScriptProperty]
[Range(1, 50)]
public int StopTicks { get; set; }
```
### Attributes that DO exist in NinjaScript
```csharp
[NinjaScriptProperty] // exposes to UI and optimizer
[Display(...)] // controls UI label, group, order
[Range(min, max)] // sets optimizer/validation bounds
[Browsable(false)] // hides from UI
[XmlIgnore] // excludes from serialization
```
### Attributes that do NOT exist in NinjaScript
```
[Optimizable] — use [NinjaScriptProperty] + [Range] instead
[JsonProperty] — not available
[Required] — not available
```
### Method overrides — always verify against NT8 documentation
Before adding any `protected override` method to NT8StrategyBase.cs or
any NinjaScript file, verify the exact signature at:
https://developer.ninjatrader.com/docs/desktop
Common signatures that differ from intuition:
| Method | Correct NT8 Signature |
|---|---|
| OnConnectionStatusUpdate | `(ConnectionStatusEventArgs e)` |
| OnMarketData | `(MarketDataEventArgs e)` |
| OnMarketDepth | `(MarketDepthEventArgs e)` |
| OnOrderUpdate | `(Order order, double limitPrice, double stopPrice, int quantity, int filled, double avgFillPrice, OrderState orderState, DateTime time, ErrorCode error, string nativeError)` |
| OnExecutionUpdate | `(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time)` |
---
## Deployment reminder
The full deployment sequence is always:
```
1. dotnet build NT8-SDK.sln --configuration Release
2. deployment\deploy-to-nt8.bat
3. NT8: Tools → NinjaScript Editor → Compile All
4. Reload any open Strategy Analyzer or chart instances
```
Steps 1-2 passing does NOT mean step 3 will pass. NT8 compilation is
the only authoritative check for NinjaScript-specific code.
---
## Files compiled by NT8 (not dotnet build)
These files are deployed as source and compiled by NT8's embedded
compiler. Any NT8-specific API usage in these files is invisible to
dotnet build:
- `src/NT8.Adapters/Strategies/NT8StrategyBase.cs`
- `src/NT8.Adapters/Strategies/SimpleORBNT8.cs`
- `src/NT8.Adapters/Wrappers/BaseNT8StrategyWrapper.cs`
- `src/NT8.Adapters/Wrappers/SimpleORBNT8Wrapper.cs`
Any new strategy or wrapper file added to these locations inherits the
same constraint. When modifying these files, the NT8 compiler is the
only valid test.

View File

@@ -1,7 +1,87 @@
# Project Context — NT8 SDK (Production Hardening Phase)
# Project Context — NT8 SDK (Sprint 2: SIM Validation)
**Last Updated:** 2026-03-27
You are working on the **NT8 SDK** — an institutional-grade algorithmic trading framework for NinjaTrader 8.
This is production trading software. Bugs cause real financial losses.
You are working on the **NT8 SDK** — an institutional-grade algorithmic futures trading system for NinjaTrader 8.
This is **production trading software**. Bugs cause real financial losses. Never take shortcuts.
---
## Onboarding / First Read (Canonical Order)
For every new Kilo session, start with `docs/00-governance/` as the primary source of truth:
1. `docs/00-governance/executive_summary.md`
2. `docs/00-governance/current_status.md`
3. `docs/00-governance/active_work.md`
4. `docs/00-governance/architecture.md`
5. `docs/00-governance/roadmap.md`
`PROJECT_HANDOVER.md` and `DESIGNED_VS_IMPLEMENTED_GAP_ANALYSIS.md` are historical/contextual references only. Use them for background, not for authoritative current direction.
---
## Critical Rules for Kilocode
1. **Only modify files listed in the task spec.** Never touch adjacent code.
2. **C# 5.0 only.** No `$""`, no `?.`, no `=>` bodies, no `nameof()`, no async/await.
3. **Never remove XML documentation or comments.**
4. **Never change interface signatures** — IStrategy, IRiskManager, IPositionSizer, INT8ExecutionBridge are frozen.
5. **Always build before deploying:** `dotnet build NT8-SDK.sln --configuration Release`
6. **Always deploy to BOTH paths** after every code change:
- Repo: `C:\dev\nt8-sdk\src\NT8.Adapters\Strategies\`
- NT8: `C:\Users\billy\Documents\NinjaTrader 8\bin\Custom\Strategies\`
7. **Never guess NT8 API signatures.** Verify at `https://developer.ninjatrader.com/docs/desktop`.
---
## Current State (2026-03-27)
**What works end-to-end:**
- SimpleORBStrategy with 10-factor confluence scoring
- NT8StrategyBase with session management, kill switch, connection recovery
- Dual-leg scaler + runner architecture (EntriesPerDirection=2 restored)
- PortfolioRiskManager singleton (kill switch, daily loss, contract cap)
- File logging (session log + settings export)
- Historical replay guard (_realtimeBarSeen)
- Execution confirmed in SIM on 2026-03-27
**Pending validation:**
- Runner leg dual-fill (Qty=2) — run backtest to confirm
- Breakeven + trail in live multi-bar scenario
---
## Architecture
```
SimpleORBNT8.cs NT8 entry point
NT8StrategyBase.cs Abstract base: bar routing, kill switch, breakeven, runner trail
SimpleORBStrategy.cs Signal: ORB detection, 10-factor confluence, _tradeTaken lock
NT8OrderAdapter.cs INT8ExecutionBridge: EnterLong/EnterShort/SetStopLoss
PortfolioRiskManager.cs Singleton: cross-strategy risk
NinjaTrader 8
```
## Key Files
| File | Path |
|---|---|
| NT8StrategyBase.cs | `src\NT8.Adapters\Strategies\` |
| SimpleORBNT8.cs | `src\NT8.Adapters\Strategies\` |
| SimpleORBStrategy.cs | `src\NT8.Strategies\Examples\` |
| NT8OrderAdapter.cs | `src\NT8.Adapters\NinjaTrader\` |
| PortfolioRiskManager.cs | `src\NT8.Core\Risk\` |
| deploy-to-nt8.bat | `deployment\` |
## Active Sprint Tasks
See `SPRINT_BOARD.md` (at `docs\architecture\phase1_sprint_plan.md`) for full task list.
Immediate next action: Run Strategy Analyzer backtest (NQ JUN26, Jan 1 2026 → Mar 27 2026) and confirm `PNL_UPDATE Position=Short Qty=2` in session log to validate runner leg.
---

View File

@@ -1,4 +1,75 @@
# Designed vs. Implemented Features - Gap Analysis
> ⚠️ HISTORICAL — see docs/00-governance/ for current state
This file may contain outdated or mixed historical information.
Canonical current-state documentation lives in docs/00-governance/.
This file is retained for history/reference only.
# NT8-SDK — Gap Analysis & Roadmap
**Version:** 3.0 | **Date:** 2026-03-27 | Supersedes all previous gap analysis documents.
---
## Open Gaps
| ID | Description | Priority | Sprint |
|---|---|---|---|
| GAP-001 | Runner leg backtest validation (Qty=2 check). EntriesPerDirection=2 restored but not backtested. | CRITICAL | S2-05 |
| GAP-002 | orbRangeTicks not wired in DailyBarContext (hardcoded 0.0 in SimpleORBNT8.OnBarUpdate) | LOW | Sprint 3 |
| GAP-003 | Risk parameter consistency: RiskPerTrade can exceed MaxTradeRisk silently. No assertion. | HIGH | S2-06 |
| GAP-004 | GetRiskStatus() returns hardcoded limit rather than registered strategy config value | LOW | Sprint 3 |
| GAP-005 | No Gitea CI pipeline. Build and test are manual. | MEDIUM | Sprint 3 |
| GAP-006 | No n8n webhook alerts for fills, risk events, connection loss | MEDIUM | Sprint 3 |
| GAP-007 | No walk-forward / out-of-sample validation. All backtests are in-sample. | HIGH | S2-08 |
| GAP-008 | Short-side profitable only in crash regimes. No regime filter. | MEDIUM | Sprint 3 |
| GAP-009 | No tick replay backtest. OnBarClose simulation compresses trade duration. | LOW | Sprint 3 |
---
## Sprint Roadmap
### Sprint 2 (ACTIVE) — SIM Validation
Goal: 2+ weeks unattended SIM with dual-leg execution confirmed.
Key pending: GAP-001 (runner validation), GAP-003 (risk consistency), GAP-007 (walk-forward).
### Sprint 3 — Production Hardening
Goal: 30-day SIM clean. CI and alerts wired.
Key work: GAP-004 through GAP-009, VWAPMeanReversion skeleton.
### Sprint 4 — Live Capital
Gate: 30-day SIM PF > 2.0, max DD < $500.
Key work: Go live 1 NQ contract, OvernightGap strategies, ops runbook.
### Sprint 5 — ML Inference
Prerequisite: 60 days live data.
Key work: FastAPI /predict, MLSignalFactorCalculator as 11th factor.
---
## Strategy Backlog
| ID | Strategy | Priority | Notes |
|---|---|---|---|
| STRAT_079 | Liquidity Sweep Reversal | Medium | Potential short-trade improvement |
| STRAT_154 | Overnight Gap Continuation | High | Leverages existing SessionManager |
| STRAT_214 | Overnight Gap Reversion | High | Counter to STRAT_154 |
| LondonORB | London ORB (3:00 AM ET) | Medium | Separate LondonORBNT8 strategy file |
| VWAP-MR | VWAP Mean Reversion | High | Sprint 3 build target |
---
## Backtest Performance Reference
| Date | Period | Trades | Win% | PF | Net | Config |
|---|---|---|---|---|---|---|
| 2026-03-27 | JanMar 2026 | 20 | 75% | **7.00** | $1,200 | trail=20 ✅ Production config |
| 2026-03-27 | JanMar 2026 | 40 | 75% | 3.69 | $1,075 | trail=12 |
| 2026-03-27 | Mar 2025Mar 2026 | 148 | 51% | 3.15 | $71,303 | 9 cts experimental |
Note: 148-trade run used RiskPerTrade=$500 + EntriesPerDirection=1 (runner blocked). Not a production reference.
---
# ARCHIVED BELOW — Original Gap Analysis (2026-02-17, superseded)
**Date:** February 17, 2026
**Status:** Post Phase A-B-C NT8 Integration

View File

@@ -1,9 +1,140 @@
# NT8 SDK Project - Comprehensive Recap & Handover
> ⚠️ HISTORICAL — see docs/00-governance/ for current state
**Document Version:** 2.0
**Date:** February 16, 2026
**Current Phase:** Phase 5 Complete
**Project Completion:** ~85%
This file may contain outdated or mixed historical information.
Canonical current-state documentation lives in docs/00-governance/.
This file is retained for history/reference only.
# NT8-SDK — Project Context & Current State
**Version:** 3.0 | **Date:** 2026-03-27 | **Status:** Sprint 2 Active — SIM Validation
> This file supersedes the previous PROJECT_HANDOVER.md and is the live source of truth.
> See also: `SPRINT_BOARD.md`, `GAP_ANALYSIS_AND_ROADMAP.md`, `CODING_PATTERNS.md`, `KILOCODE_WORKFLOW.md`
> Full formatted handover: `NT8_SDK_Handover_Package.docx`
---
## 1. What This Is
NT8-SDK is an institutional-grade algorithmic futures trading system built on NinjaTrader 8. It is not a research prototype — it is production trading software where bugs equal real financial losses.
The system trades NQ (Nasdaq 100 E-mini futures) using a 30-minute Opening Range Breakout strategy (SimpleORB) with a 10-factor confluence scoring engine that grades each signal A+ through F before allowing execution. A scaler/runner dual-leg architecture captures quick targets on the scaler while the runner trails for extended moves.
**Division of labor:** Claude handles architecture, diagnosis, and Kilocode prompt design. Kilocode executes ALL code changes. Mo owns strategy direction and go/no-go decisions.
---
## 2. Technology Stack
| Layer | Technology | Constraint |
|---|---|---|
| Language | C# 5.0 | Hard — NinjaScript compiler limit |
| Framework | .NET Framework 4.8 | Hard — NT8 requirement |
| Platform | NinjaTrader 8 | Hard — execution environment |
| Local repo | `C:\dev\nt8-sdk` | Windows path |
| NT8 deploy | `C:\Users\billy\Documents\NinjaTrader 8\bin\Custom\Strategies\` | Must match source |
| Deploy script | `deployment\deploy-to-nt8.bat` | Creates timestamped backups |
| VCS | Gitea (self-hosted) | `https://git.thehussains.org/mo/nt8-sdk` |
| AI coding | Kilocode | Executes ALL code changes |
| Automation | n8n (self-hosted) | Deferred to Sprint 4 |
| ML inference | Ollama (local) | Deferred to Sprint 5 |
**Critical C# constraint:** No `$""`, no `?.`, no `=>`, no async/await. Use `string.Format()`, explicit null checks, full method bodies.
---
## 3. Architecture (Top to Bottom)
```
SimpleORBNT8.cs NT8 entry point — sets defaults, adds daily bar series, builds DailyBarContext
NT8StrategyBase.cs Abstract base — bar routing, session management, kill switch, breakeven, runner
SimpleORBStrategy.cs Core signal — ORB detection, 10-factor confluence, _tradeTaken session lock
NT8OrderAdapter.cs INT8ExecutionBridge — calls EnterLong/EnterShort/SetStopLoss
PortfolioRiskManager.cs Singleton — cross-strategy daily loss + contract cap
NinjaTrader 8 Execution, fills, order management
```
---
## 4. Current Production Parameters (SIM: SimSimple ORB)
| Parameter | Value | Notes |
|---|---|---|
| Instrument | NQ JUN26 | Primary instrument |
| Bar type | 13-Range bars | |
| BarsRequiredToTrade | 50 | Warm-up guard |
| DailyLossLimit | $1,000 | |
| MaxTradeRisk | $200 | |
| RiskPerTrade | $100 | 2 contracts at current NQ prices |
| MinContracts | 1 | |
| MaxContracts | 3 | |
| MaxOpenPositions | 2 | Scaler + runner |
| EntriesPerDirection | 2 | Scaler slot 1, runner slot 2 |
| MinTradeGrade | 5 (A) | 4 (B) for broad SIM testing |
| EnableShortTrades | False | Long-only until short backtest done |
| BreakevenTriggerTicks | 20 | Tuned from 12 |
| RunnerTrailTicks | 20 | Tuned from 12 |
| OpeningRangeMinutes | 30 | 9:3010:00 ET |
| StopTicks | 8 | $40 per contract NQ |
| TargetTicks | 16 (dynamic) | Scales with ORB/ATR ratio |
---
## 5. Two-Path Deployment Rule
Every code change MUST be applied to both:
1. `C:\dev\nt8-sdk\src\NT8.Adapters\Strategies\` (repo source)
2. `C:\Users\billy\Documents\NinjaTrader 8\bin\Custom\Strategies\` (NT8 runtime)
After deployment, NT8 must recompile: Tools → Edit NinjaScript → open `NT8StrategyBase.cs` → save.
---
## 6. Key Learnings (Hard-Won)
1. **`State.Historical` guard** — `if (State == State.Realtime && !_realtimeBarSeen) return;` in `ProcessStrategyIntent` allows backtest (Historical), blocks replay burst in live.
2. **`_realtimeBarSeen` flag** — reset to `false` in `State.Realtime`, set `true` on first bar. Skips catch-up bar on live load to prevent replay burst.
3. **`EntriesPerDirection = 2`** — required for scaler + runner. Setting to 1 silently blocks the runner with no error.
4. **`SetStopLoss`/`SetProfitTarget` before `EnterLong`/`EnterShort`** — calling after entry is silently ignored in backtest.
5. **`Calculate.OnBarClose` backtest** — trades appear to close in under 1 second in logs. NT8 simulation artifact, not a bug. Live trades hold 220 minutes.
6. **NR7 warm-up** — requires 7 daily bars. Warm-up messages (0/7 bars) are correct behavior.
7. **NT8 never auto-recompiles** — always force recompile after file changes via NinjaScript Editor.
8. **Dual-path deployment mandatory** — stale deployed files cause phantom bugs where code looks right but behaves wrong.
9. **`PortfolioRiskManager` is a singleton** — fully implemented, no changes required. Kill switch, daily loss, contract cap all working.
10. **Sizing formula**`floor(RiskPerTrade / (StopTicks × $5.00))`. NQ: `$100 / (8 × $5) = 2 contracts`. Always verify `RiskPerTrade <= MaxTradeRisk`.
---
## 7. Validated Backtest Results
| Date | Period | Trades | Win% | PF | Net | Config |
|---|---|---|---|---|---|---|
| 2026-03-27 | JanMar 2026 | 20 | 75% | **7.00** | $1,200 | 1 ct, trail=20 ✅ Best |
| 2026-03-27 | JanMar 2026 | 40 | 75% | 3.69 | $1,075 | 1 ct, trail=12 |
| 2026-03-27 | Mar 2025Mar 2026 | 148 | 51% | 3.15 | $71,303 | 9 cts (experimental) |
The 9-contract run used `RiskPerTrade=$500` — not a production configuration. Runner leg was also blocked (`EntriesPerDirection=1`) for that run. Re-run required after Sprint 2 fixes.
---
## 8. Immediate Next Actions Before Market Open
1. Run Strategy Analyzer (NQ JUN26, Jan 1 2026 → Mar 27 2026) and confirm `PNL_UPDATE Position=Short Qty=2` in session log — validates runner leg
2. Verify SIM account settings: `RiskPerTrade=$100`, `BreakevenTriggerTicks=20`, `RunnerTrailTicks=20`
3. Only re-enable BX68915-15 after runner validation passes — long-only, `MaxContracts=2`, `DailyLossLimit=$500`
---

View File

@@ -1,3 +1,18 @@
> 📋 NOTE — This README is under revision. See docs/00-governance/ for current architecture and status.
# NT8-SDK — Institutional Algorithmic Futures Trading System
**See `NT8_SDK_Handover_Package.docx` for the complete milestone handover document.**
Quick reference docs in repo root:
- `PROJECT_CONTEXT.md` — current state, parameters, key learnings
- `SPRINT_BOARD.md` — active sprint tasks
- `GAP_ANALYSIS_AND_ROADMAP.md` — open gaps and sprint plan
- `CODING_PATTERNS.md` — C# 5.0 rules, NT8 quirks
- `KILOCODE_WORKFLOW.md` — Kilocode prompt template and guardrails
---
# NT8 Institutional Trading SDK
**Version:** 0.2.0

130
cleanup-repo.ps1 Normal file
View File

@@ -0,0 +1,130 @@
# cleanup-repo.ps1
# Removes stale, superseded, and AI-process artifacts from the repo root
# Run from: C:\dev\nt8-sdk
Set-Location "C:\dev\nt8-sdk"
$filesToDelete = @(
# Archon planning docs (tool was never used)
"archon_task_mapping.md",
"archon_update_plan.md",
# AI team/agent process docs (internal scaffolding, no ongoing value)
"ai_agent_tasks.md",
"ai_success_metrics.md",
"AI_DEVELOPMENT_GUIDELINES.md",
"AI_TEAM_SETUP_DOCUMENTATION.md",
"ai_workflow_templates.md",
# Phase A/B/C planning docs (all phases complete, superseded by PROJECT_HANDOVER)
"PHASE_A_READY_FOR_KILOCODE.md",
"PHASE_A_SPECIFICATION.md",
"PHASE_B_SPECIFICATION.md",
"PHASE_C_SPECIFICATION.md",
"PHASES_ABC_COMPLETION_REPORT.md",
# Old TASK- files superseded by TASK_ files (better versions exist)
"TASK-01-kill-switch.md",
"TASK-02-circuit-breaker.md",
"TASK-03-trailing-stop.md",
"TASK-04-log-level.md",
"TASK-05-session-holidays.md",
# Fix specs already applied to codebase
"COMPILE_FIX_SPECIFICATION.md",
"DROPDOWN_FIX_SPECIFICATION.md",
"STRATEGY_DROPDOWN_COMPLETE_FIX.md",
# One-time historical docs
"NET_FRAMEWORK_CONVERSION.md",
"FIX_GIT_AUTH.md",
"GIT_COMMIT_INSTRUCTIONS.md",
# Superseded implementation docs
"implementation_guide.md",
"implementation_guide_summary.md",
"implementation_attention_points.md",
"OMS_IMPLEMENTATION_START.md",
"nt8_sdk_phase0_completion.md",
"NT8_INTEGRATION_COMPLETE_SPECS.md",
"nt8_integration_guidelines.md",
"POST_INTEGRATION_ROADMAP.md",
# Superseded project planning (PROJECT_HANDOVER.md is canonical now)
"project_plan.md",
"project_summary.md",
"architecture_summary.md",
"development_workflow.md",
# Kilocode setup (already done, no ongoing value)
"KILOCODE_SETUP_COMPLETE.md",
"setup-kilocode-files.ps1",
# Utility scripts (one-time use)
"commit-now.ps1",
"cleanup-repo.ps1" # self-delete at end
)
$dirsToDelete = @(
"plans", # single stale analysis report
"Specs" # original spec packages, all implemented
)
Write-Host "`n=== NT8-SDK Repository Cleanup ===" -ForegroundColor Cyan
Write-Host "Removing stale and superseded files...`n"
$deleted = 0
$notFound = 0
foreach ($file in $filesToDelete) {
$path = Join-Path (Get-Location) $file
if (Test-Path $path) {
Remove-Item $path -Force
Write-Host " DELETED: $file" -ForegroundColor Green
$deleted++
} else {
Write-Host " SKIP (not found): $file" -ForegroundColor DarkGray
$notFound++
}
}
foreach ($dir in $dirsToDelete) {
$path = Join-Path (Get-Location) $dir
if (Test-Path $path) {
Remove-Item $path -Recurse -Force
Write-Host " DELETED DIR: $dir\" -ForegroundColor Green
$deleted++
} else {
Write-Host " SKIP DIR (not found): $dir\" -ForegroundColor DarkGray
$notFound++
}
}
Write-Host "`n=== Staging changes ===" -ForegroundColor Cyan
git add -A
Write-Host "`n=== Committing ===" -ForegroundColor Cyan
git commit -m "chore: repo housekeeping - remove stale and superseded files
Removed categories:
- Archon planning docs (tool never used)
- AI team/agent scaffolding docs
- Phase A/B/C specs (complete, superseded by PROJECT_HANDOVER)
- Old TASK-0x files (superseded by TASK_0x versions)
- Applied fix specs (COMPILE, DROPDOWN, STRATEGY_DROPDOWN)
- One-time historical docs (NET_FRAMEWORK_CONVERSION, FIX_GIT_AUTH)
- Superseded implementation guides and planning docs
- plans/ and Specs/ directories (all implemented)
Kept:
- All active TASK_0x work items (TASK_01/02/03 execution wiring)
- PROJECT_HANDOVER, NEXT_STEPS_RECOMMENDED, GAP_ANALYSIS
- Phase3/4/5 Implementation Guides
- KILOCODE_RUNBOOK, OPTIMIZATION_GUIDE
- All spec files for pending work (RTH, CONFIG_EXPORT, DIAGNOSTIC_LOGGING)
- src/, tests/, docs/, deployment/, rules/, .kilocode/ unchanged"
Write-Host "`nDeleted: $deleted items" -ForegroundColor Green
Write-Host "Skipped: $notFound items (already gone)" -ForegroundColor DarkGray
Write-Host "`n=== Done! Current root files: ===" -ForegroundColor Cyan
Get-ChildItem -File | Where-Object { $_.Name -notlike ".*" } | Select-Object Name | Format-Table -HideTableHeaders

View File

@@ -173,7 +173,7 @@ else {
}
if (-not $SkipVerification) {
Write-Step "6/6" "Verifying deployment"
Write-Step "6/7" "Verifying deployment"
$ok = $true
if (-not (Test-Path "$nt8Custom\NT8.Core.dll")) {
@@ -195,13 +195,43 @@ if (-not $SkipVerification) {
Write-Success "Deployment verification passed"
}
else {
Write-Step "6/6" "Skipping verification"
Write-Step "6/7" "Skipping verification"
}
# Step 7: Delete the compiled NinjaScript assembly so NT8 is forced to do a
# full recompile from source on next startup. Without this, NT8 may load a
# stale cached assembly and silently ignore updated .cs files.
Write-Step "7/7" "Invalidating NinjaScript compiled assembly"
$compiledDll = Join-Path $nt8Custom "NinjaTrader.Custom.dll"
$compiledPdb = Join-Path $nt8Custom "NinjaTrader.Custom.pdb"
$compiledXml = Join-Path $nt8Custom "NinjaTrader.Custom.xml"
$invalidated = 0
foreach ($artifact in @($compiledDll, $compiledPdb)) {
if (Test-Path $artifact) {
Remove-Item $artifact -Force
Write-Success ("Deleted {0}" -f (Split-Path $artifact -Leaf))
$invalidated++
}
}
if ($invalidated -gt 0) {
Write-Host " NT8 will perform a full recompile on next startup." -ForegroundColor Green
Write-Host " The .xml file is documentation only and was left in place." -ForegroundColor Gray
} else {
Write-Warn "No compiled assembly found to delete (NT8 may not have been run yet)"
}
$duration = (Get-Date) - $startTime
Write-Header "Deployment Complete"
Write-Host ("Duration: {0:F1} seconds" -f $duration.TotalSeconds)
Write-Host "Next: Open NinjaTrader 8 -> NinjaScript Editor -> Compile All"
Write-Host ""
Write-Host "NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Start NinjaTrader 8 (full recompile will happen automatically)" -ForegroundColor White
Write-Host " 2. Wait for compilation to complete in the Output window" -ForegroundColor White
Write-Host " 3. Remove and re-add the strategy to the chart" -ForegroundColor White
Write-Host " 4. Verify defaults: BreakevenTriggerTicks=20 RunnerTrailTicks=20 MaxContracts=3" -ForegroundColor White
Write-Host " 5. Confirm NT8 Output shows: StartBehavior=AdoptAccountPosition EntriesPerDirection=2" -ForegroundColor White
exit 0

View File

@@ -10,6 +10,7 @@ set "NT8_CUSTOM=%USERPROFILE%\Documents\NinjaTrader 8\bin\Custom"
set "NT8_STRATEGIES=%NT8_CUSTOM%\Strategies"
set "CORE_BIN=%PROJECT_ROOT%\src\NT8.Core\bin\Release\net48"
set "ADAPTERS_BIN=%PROJECT_ROOT%\src\NT8.Adapters\bin\Release\net48"
set "STRATEGIES_BIN=%PROJECT_ROOT%\src\NT8.Strategies\bin\Release\net48"
set "WRAPPERS_SRC=%PROJECT_ROOT%\src\NT8.Adapters\Wrappers"
set "BACKUP_ROOT=%SCRIPT_DIR%backups"
@@ -40,6 +41,13 @@ if not exist "%ADAPTERS_BIN%\NT8.Adapters.dll" (
exit /b 1
)
if not exist "%STRATEGIES_BIN%\NT8.Strategies.dll" (
echo ERROR: Strategies DLL not found: %STRATEGIES_BIN%\NT8.Strategies.dll
echo Build release artifacts first:
echo dotnet build NT8-SDK.sln --configuration Release
exit /b 1
)
if not exist "%NT8_STRATEGIES%" (
mkdir "%NT8_STRATEGIES%"
)
@@ -52,6 +60,7 @@ mkdir "%BACKUP_ROOT%\%STAMP%" >nul 2>&1
echo Backing up existing NT8 SDK files...
if exist "%NT8_CUSTOM%\NT8.Core.dll" copy /Y "%NT8_CUSTOM%\NT8.Core.dll" "%BACKUP_DIR%\NT8.Core.dll" >nul
if exist "%NT8_CUSTOM%\NT8.Adapters.dll" copy /Y "%NT8_CUSTOM%\NT8.Adapters.dll" "%BACKUP_DIR%\NT8.Adapters.dll" >nul
if exist "%NT8_CUSTOM%\NT8.Strategies.dll" copy /Y "%NT8_CUSTOM%\NT8.Strategies.dll" "%BACKUP_DIR%\NT8.Strategies.dll" >nul
if exist "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" copy /Y "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" "%BACKUP_DIR%\BaseNT8StrategyWrapper.cs" >nul
if exist "%NT8_STRATEGIES%\SimpleORBNT8Wrapper.cs" copy /Y "%NT8_STRATEGIES%\SimpleORBNT8Wrapper.cs" "%BACKUP_DIR%\SimpleORBNT8Wrapper.cs" >nul
@@ -59,6 +68,7 @@ echo Deployment manifest > "%MANIFEST_FILE%"
echo Timestamp: %STAMP%>> "%MANIFEST_FILE%"
echo Source Core DLL: %CORE_BIN%\NT8.Core.dll>> "%MANIFEST_FILE%"
echo Source Adapters DLL: %ADAPTERS_BIN%\NT8.Adapters.dll>> "%MANIFEST_FILE%"
echo Source Strategies DLL: %STRATEGIES_BIN%\NT8.Strategies.dll>> "%MANIFEST_FILE%"
echo Destination Custom Folder: %NT8_CUSTOM%>> "%MANIFEST_FILE%"
echo Destination Strategies Folder: %NT8_STRATEGIES%>> "%MANIFEST_FILE%"
@@ -75,6 +85,12 @@ if errorlevel 1 (
exit /b 1
)
copy /Y "%STRATEGIES_BIN%\NT8.Strategies.dll" "%NT8_CUSTOM%\NT8.Strategies.dll" >nul
if errorlevel 1 (
echo ERROR: Failed to copy NT8.Strategies.dll
exit /b 1
)
echo Deploying wrapper sources...
copy /Y "%WRAPPERS_SRC%\BaseNT8StrategyWrapper.cs" "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" >nul
if errorlevel 1 (
@@ -112,6 +128,11 @@ if not exist "%NT8_CUSTOM%\NT8.Adapters.dll" (
exit /b 1
)
if not exist "%NT8_CUSTOM%\NT8.Strategies.dll" (
echo ERROR: Verification failed for NT8.Strategies.dll
exit /b 1
)
if not exist "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" (
echo ERROR: Verification failed for BaseNT8StrategyWrapper.cs
exit /b 1
@@ -126,11 +147,32 @@ echo.
echo Deployment complete.
echo Backup location: %BACKUP_DIR%
echo Manifest file : %MANIFEST_FILE%
echo.
echo Next steps:
echo 1. Open NinjaTrader 8.
echo 2. Open NinjaScript Editor and press F5 (Compile).
echo 3. Verify strategies appear in the Strategies list.
echo Invalidating NinjaScript compiled assembly...
set "COMPILED_DLL=%NT8_CUSTOM%\NinjaTrader.Custom.dll"
set "COMPILED_PDB=%NT8_CUSTOM%\NinjaTrader.Custom.pdb"
if exist "%COMPILED_DLL%" (
del /F /Q "%COMPILED_DLL%"
echo [OK] Deleted NinjaTrader.Custom.dll
) else (
echo [WARN] NinjaTrader.Custom.dll not found - NT8 may not have been run yet
)
if exist "%COMPILED_PDB%" (
del /F /Q "%COMPILED_PDB%"
echo [OK] Deleted NinjaTrader.Custom.pdb
)
echo.
echo ============================================================
echo NEXT STEPS:
echo 1. Start NinjaTrader 8 (full recompile happens automatically)
echo 2. Wait for compilation to finish in the Output window
echo 3. Remove and re-add the strategy to the chart
echo 4. Verify defaults: BreakevenTriggerTicks=20 RunnerTrailTicks=20 MaxContracts=3
echo 5. NT8 Output must show: StartBehavior=AdoptAccountPosition EntriesPerDirection=2
echo ============================================================
exit /b 0

View File

@@ -0,0 +1,22 @@
# Governance Cleanup Log
## 2026-04-05
- Established `docs/00-governance/` as canonical governance entry point.
- Added concise governance baseline documents:
- `executive_summary.md`
- `architecture.md`
- `current_status.md`
- `roadmap.md`
- `active_work.md`
- Updated onboarding guidance in `.kilocode/rules/project_context.md` to point new sessions to governance docs first.
- Reclassified `PROJECT_HANDOVER.md` and `DESIGNED_VS_IMPLEMENTED_GAP_ANALYSIS.md` as historical/contextual references, not primary truth.
- No code files modified and no file moves performed.
- Moved `docs/PHASE2_COMPLETION_REPORT.md` to `docs/archive/phase-history/PHASE2_COMPLETION_REPORT.md`.
- Added historical header + archival note block to:
- `docs/README.md`
- `PROJECT_HANDOVER.md`
- `DESIGNED_VS_IMPLEMENTED_GAP_ANALYSIS.md`
- `docs/INDEX.md`
- `docs/archive/phase-history/PHASE2_COMPLETION_REPORT.md`
- Added a softer revision note to `README.md` instead of a historical warning.
- Marked `docs/INDEX.md` historical in place because it currently misdirects navigation.

View File

@@ -0,0 +1,17 @@
# Active Work
## Current Execution Focus
- Validate runner-leg dual-fill behavior in Strategy Analyzer/session logs.
- Close remaining Sprint 2 hardening items already identified in task specs.
- Preserve strict compile and deployment verification sequence for every change.
## In-Scope Hardening Themes
- Strategy safety controls and execution circuit-breaker wiring.
- Trailing stop calculation correctness.
- Logging verbosity controls for production noise reduction.
- Session/holiday awareness to avoid invalid trading windows.
## Working Rules
- Apply changes only to explicitly scoped files per task.
- Keep NT8 API signatures and managed-order sequencing exact.
- Maintain C# 5.0 compatibility and existing interface boundaries.

View File

@@ -0,0 +1,28 @@
# Architecture Governance
## Runtime Flow (Authoritative)
```
SimpleORBNT8.cs
-> NT8StrategyBase.cs
-> SimpleORBStrategy.cs
-> NT8OrderAdapter.cs
-> PortfolioRiskManager.cs
-> NinjaTrader 8
```
## Responsibilities
- `SimpleORBNT8.cs`: NT8 entry point and platform lifecycle bridge.
- `NT8StrategyBase.cs`: orchestration, risk gate sequencing, execution handoff, platform callbacks.
- `SimpleORBStrategy.cs`: signal generation and confluence grading only.
- `NT8OrderAdapter.cs`: execution bridge to NT8 managed order APIs.
- `PortfolioRiskManager.cs`: cross-strategy risk controls and account-level enforcement.
## Architectural Constraints
- Risk-first flow is mandatory; no strategy-level bypass of risk validation.
- Managed-order sequence remains required (`SetStopLoss` / `SetProfitTarget` before entry).
- C# 5.0 syntax only, .NET Framework 4.8 only.
- NT8 signatures must be verified against official NinjaTrader docs before API-touching edits.
## Governance Notes
- Core Risk/Sizing/OMS/Intelligence/Analytics layers are treated as complete and stable unless explicitly re-opened by approved work.
- Hardening changes are concentrated in targeted adapter/utility components per active-work scope.

View File

View File

View File

@@ -0,0 +1,20 @@
# Current Status
## Snapshot (2026-04-05)
- Program phase: Sprint 2 SIM validation with production-hardening follow-through.
- Core implementation: complete across major layers with 240+ passing tests.
- Live focus: execution safety, validation depth, and operational reliability.
## Confirmed Working Areas
- End-to-end strategy pipeline from signal -> risk -> sizing -> NT8 managed execution.
- Session handling, portfolio risk controls, and base strategy orchestration.
- Existing analytics/risk/sizing/OMS foundations remain stable.
## Open Findings Driving Work
- Runner leg behavior requires explicit validation evidence (`Qty=2` path confirmation).
- Risk/config consistency checks need tightening in runtime safeguards.
- Operational controls (CI automation, alerting, and out-of-sample validation) remain pending.
## Operational Reality
- `dotnet build` success is necessary but not sufficient; NT8 NinjaScript compile remains a separate required validation step.
- Deployment integrity requires keeping repo and NT8 runtime strategy copies synchronized.

View File

View File

@@ -0,0 +1,20 @@
# Executive Summary
## Scope
- This folder (`docs/00-governance/`) is the canonical governance source for project state, priorities, architecture intent, and active execution guidance.
- Governance is aligned to Sprint 2 (SIM validation) with production hardening gaps tracked and prioritized.
## Current Position (2026-04-05)
- Core engine is implemented with 240+ passing tests across core components.
- NT8 execution path is wired and validated in SIM for baseline operation.
- Remaining work is focused on closing operational hardening gaps and validating runner behavior under production-like conditions.
## Approved Direction
- Keep execution in managed-order NT8 patterns and C# 5.0 / .NET Framework 4.8 constraints.
- Complete critical and high-priority hardening tasks before expanding strategy scope.
- Treat historical handover artifacts as context only; governance decisions flow from this folder.
## Immediate Priorities
- Confirm runner-leg dual-fill behavior in analyzer/session logs.
- Close safety and observability items already identified in Sprint 2/3 gap tracking.
- Maintain strict file-boundary and compile-guardrail discipline for all changes.

View File

@@ -0,0 +1,21 @@
# Roadmap
## Sprint 2 (Active): SIM Validation
- Validate dual-leg execution behavior with log evidence.
- Complete remaining safety/consistency fixes tied to active gap list.
- Exit criteria: stable SIM behavior with no unresolved critical gaps.
## Sprint 3: Production Hardening
- Implement CI build/test automation and operational alerting.
- Complete lower-priority runtime consistency and observability improvements.
- Add walk-forward and broader validation coverage beyond in-sample checks.
## Sprint 4: Live Capital Readiness
- Gate on sustained SIM metrics and drawdown controls.
- Introduce go-live runbook and operational controls for controlled capital exposure.
## Sprint 5: ML Extension (Deferred)
- Add inference integration only after sufficient live/sim data and stable production operations.
## Sequencing Rule
- No feature expansion ahead of unresolved safety and validation gates.

View File

@@ -1,3 +1,10 @@
> ⚠️ HISTORICAL — see docs/00-governance/ for current state
This file may contain outdated or mixed historical information.
Canonical current-state documentation lives in docs/00-governance/.
This file is retained for history/reference only.
Navigation links in this file may be stale or broken.
# NT8 SDK - Documentation Index
**Complete documentation for the NT8 Institutional Trading SDK**

View File

@@ -1,3 +1,9 @@
> ⚠️ HISTORICAL — see docs/00-governance/ for current state
This file may contain outdated or mixed historical information.
Canonical current-state documentation lives in docs/00-governance/.
This file is retained for history/reference only.
# NT8 Institutional Trading SDK
**Version:** 0.2.0

View File

@@ -1,7 +1,99 @@
# NT8 Institutional SDK - Phase 1 Sprint Plan
# NT8-SDK — Sprint Board
**Last Updated:** 2026-03-27 | **Active Sprint:** Sprint 2
## Overview
This document outlines the sprint plan for Phase 1 development of the NT8 Institutional SDK. Phase 1 builds upon the completed Phase 0 foundation to deliver a more complete trading system with Order Management System (OMS), NinjaTrader 8 integration, enhanced risk controls, and market data handling.
---
## Sprint 2 — SIM Validation (ACTIVE)
**Goal:** Strategy runs unattended 2+ weeks in SIM with correct dual-leg execution.
**Gate:** Zero unhandled exceptions, both scaler and runner filling (Qty=2 in log), daily loss limit never accidentally triggered.
| # | Task | Status | Notes |
|---|---|---|---|
| S2-01 | Restore `EntriesPerDirection=2` | ✅ Done | Deployed 2026-03-27 |
| S2-02 | `MaxOpenPositions=2` in SimpleORBNT8 | ✅ Done | Deployed 2026-03-27 |
| S2-03 | `_realtimeBarSeen` replay guard | ✅ Done | Blocks live replay burst, allows backtest |
| S2-04 | `State.Historical` guard in ProcessStrategyIntent | ✅ Done | Restores backtest execution |
| S2-05 | Validate runner leg — Qty=2 in session log | ⬜ Pending | Run backtest after S2-01 |
| S2-06 | Risk parameter consistency validation | ⬜ Pending | Assert RiskPerTrade ≤ MaxTradeRisk |
| S2-07 | SIM unattended run 2+ weeks | ▶ In progress | Started 2026-03-27 |
| S2-08 | Walk-forward backtest split | ⬜ Pending | MarSep 2025 train / OctMar 2026 test |
| S2-09 | BX68915-15 prop firm re-enable | 🔴 Blocked | Gate: S2-05 + S2-07 pass |
---
## Sprint 3 — Production Hardening
**Goal:** 30-day SIM run clean. CI and alerts live.
| # | Task | Status | Notes |
|---|---|---|---|
| S3-01 | Gitea CI — build + test on push | ⬜ Pending | `.gitea/workflows/build.yml` |
| S3-02 | n8n webhook — fills + risk events | ⬜ Pending | HTTP POST from NT8StrategyBase |
| S3-03 | Fix `GetRiskStatus()` hardcoded limit | ⬜ Pending | Read from registered strategy config |
| S3-04 | `orbRangeTicks` wiring in DailyBarContext | ⬜ Pending | Low priority |
| S3-05 | Short-side regime filter | ⬜ Pending | 20-day MA or VIX-based gate |
| S3-06 | Tick replay validation | ⬜ Pending | 30-day window |
| S3-07 | VWAPMeanReversion strategy skeleton | ⬜ Pending | New IStrategy implementation |
---
## Sprint 4 — Live Capital
**Gate:** 30-day SIM pass rate > 70%, PF > 2.0, max drawdown < $500
| # | Task | Status |
|---|---|---|
| S4-01 | Go live 1 NQ contract | ⬜ Pending gate |
| S4-02 | OvernightGapContinuation strategy | ⬜ Pending |
| S4-03 | OvernightGapReversion strategy | ⬜ Pending |
| S4-04 | Ops runbook and emergency procedures | ⬜ Pending |
| S4-05 | Connection loss recovery testing | ⬜ Pending |
| S4-06 | CME holiday filter | ⬜ Pending |
---
## Sprint 5 — ML Inference
**Prerequisite:** 60 days of live trading data.
| # | Task |
|---|---|
| S5-01 | FastAPI /predict endpoint on Ollama workstation |
| S5-02 | HTTP client in SimpleORBStrategy |
| S5-03 | MLSignalFactorCalculator as IFactorCalculator |
| S5-04 | Feature engineering from live trade history |
| S5-05 | A/B test: with vs without ML factor |
---
## Completed (Historical)
| Task | Sprint | Completed |
|---|---|---|
| Execute trades in NT8 SIM (execution bridge wired) | Sprint 1 | 2026-03-27 |
| Historical replay burst fix | Sprint 1 | 2026-03-27 |
| NR7 warm-up guard | Phase 4 | 2026-02 |
| 10-factor confluence scoring engine | Phase 4 | 2026-02 |
| PortfolioRiskManager singleton | Phase 4 | 2026-02 |
| Analytics layer (240+ tests) | Phase 5 | 2026-02-16 |
| Breakeven + runner trail logic | Sprint 1 | 2026-03 |
| Connection loss detection | Sprint 1 | 2026-03 |
| File logging + settings export | Sprint 1 | 2026-03 |
---
## Backtest Results History
| Date | Period | Trades | Win% | PF | Net | Config |
|---|---|---|---|---|---|---|
| 2026-03-27 | JanMar 2026 | 20 | 75% | 7.00 | $1,200 | trail=20, grade=B ✅ Use this |
| 2026-03-27 | JanMar 2026 | 40 | 75% | 3.69 | $1,075 | trail=12, grade=B |
| 2026-03-27 | Mar 2025Mar 2026 | 148 | 51% | 3.15 | $71,303 | 9 cts experimental |
**Note:** The 148-trade run used `RiskPerTrade=$500` producing 9 contracts AND had `EntriesPerDirection=1` blocking the runner. Not a production reference. Re-run required post Sprint 2.
---
# ARCHIVED BELOW — Phase 1 Sprint Plan (2025-09-15, superseded)
## Overview (ARCHIVED)
This document originally outlined Phase 1 sprint planning from September 2025.
## Sprint Goals
1. Implement Order Management System (OMS) with smart order routing

View File

@@ -1,3 +1,9 @@
> ⚠️ HISTORICAL — see docs/00-governance/ for current state
This file may contain outdated or mixed historical information.
Canonical current-state documentation lives in docs/00-governance/.
This file is retained for history/reference only.
# Phase 2 Completion Report
**Project:** NT8 Institutional Trading SDK

View File

@@ -0,0 +1,26 @@
using System;
namespace NT8.Adapters.NinjaTrader
{
/// <summary>
/// Provides NT8OrderAdapter access to NinjaScript execution methods.
/// Implemented by NT8StrategyBase.
/// </summary>
public interface INT8ExecutionBridge
{
/// <summary>Submit a long entry with stop and target.</summary>
void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize);
/// <summary>Submit a short entry with stop and target.</summary>
void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize);
/// <summary>Exit all long positions.</summary>
void ExitLongManaged(string signalName);
/// <summary>Exit all short positions.</summary>
void ExitShortManaged(string signalName);
/// <summary>Flatten the full position immediately.</summary>
void FlattenAll();
}
}

View File

@@ -27,11 +27,34 @@ namespace NT8.Adapters.NinjaTrader
public NT8Adapter()
{
_dataAdapter = new NT8DataAdapter();
_orderAdapter = new NT8OrderAdapter();
_orderAdapter = new NT8OrderAdapter(new NullExecutionBridge());
_loggingAdapter = new NT8LoggingAdapter();
_executionHistory = new List<NT8OrderExecutionRecord>();
}
private class NullExecutionBridge : INT8ExecutionBridge
{
public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
{
}
public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
{
}
public void ExitLongManaged(string signalName)
{
}
public void ExitShortManaged(string signalName)
{
}
public void FlattenAll()
{
}
}
/// <summary>
/// Initialize the adapter with required components
/// </summary>

View File

@@ -12,6 +12,7 @@ namespace NT8.Adapters.NinjaTrader
public class NT8OrderAdapter
{
private readonly object _lock = new object();
private readonly INT8ExecutionBridge _bridge;
private IRiskManager _riskManager;
private IPositionSizer _positionSizer;
private readonly List<NT8OrderExecutionRecord> _executionHistory;
@@ -19,8 +20,11 @@ namespace NT8.Adapters.NinjaTrader
/// <summary>
/// Constructor for NT8OrderAdapter.
/// </summary>
public NT8OrderAdapter()
public NT8OrderAdapter(INT8ExecutionBridge bridge)
{
if (bridge == null)
throw new ArgumentNullException("bridge");
_bridge = bridge;
_executionHistory = new List<NT8OrderExecutionRecord>();
}
@@ -127,18 +131,30 @@ namespace NT8.Adapters.NinjaTrader
private void ExecuteInNT8(StrategyIntent intent, SizingResult sizing)
{
if (intent == null)
{
throw new ArgumentNullException("intent");
}
if (sizing == null)
{
throw new ArgumentNullException("sizing");
}
// This is where the actual NT8 order execution would happen
// In a real implementation, this would call NT8's EnterLong/EnterShort methods
// along with SetStopLoss, SetProfitTarget, etc.
var signalName = string.Format("SDK_{0}_{1}", intent.Symbol, intent.Side);
if (intent.Side == OrderSide.Buy)
{
_bridge.EnterLongManaged(
sizing.Contracts,
signalName,
intent.StopTicks,
intent.TargetTicks.HasValue ? intent.TargetTicks.Value : 0,
0.25);
}
else if (intent.Side == OrderSide.Sell)
{
_bridge.EnterShortManaged(
sizing.Contracts,
signalName,
intent.StopTicks,
intent.TargetTicks.HasValue ? intent.TargetTicks.Value : 0,
0.25);
}
lock (_lock)
{
@@ -151,28 +167,6 @@ namespace NT8.Adapters.NinjaTrader
intent.TargetTicks,
DateTime.UtcNow));
}
// Example of what this might look like in NT8:
/*
if (intent.Side == OrderSide.Buy)
{
EnterLong(sizing.Contracts, "SDK_Entry");
SetStopLoss("SDK_Entry", CalculationMode.Ticks, intent.StopTicks);
if (intent.TargetTicks.HasValue)
{
SetProfitTarget("SDK_Entry", CalculationMode.Ticks, intent.TargetTicks.Value);
}
}
else if (intent.Side == OrderSide.Sell)
{
EnterShort(sizing.Contracts, "SDK_Entry");
SetStopLoss("SDK_Entry", CalculationMode.Ticks, intent.StopTicks);
if (intent.TargetTicks.HasValue)
{
SetProfitTarget("SDK_Entry", CalculationMode.Ticks, intent.TargetTicks.Value);
}
}
*/
}
/// <summary>

View File

@@ -32,7 +32,7 @@ namespace NinjaTrader.NinjaScript.Strategies
/// <summary>
/// Base class for strategies that integrate NT8 SDK components.
/// </summary>
public abstract class NT8StrategyBase : Strategy
public abstract class NT8StrategyBase : Strategy, INT8ExecutionBridge
{
private readonly object _lock = new object();
@@ -53,7 +53,15 @@ namespace NinjaTrader.NinjaScript.Strategies
private int _ordersSubmittedToday;
private DateTime _lastBarTime;
private bool _killSwitchTriggered;
private bool _connectionLost;
private bool _realtimeBarSeen;
private bool _breakevenMoved;
private string _scalerSignalName;
private string _runnerSignalName;
private bool _runnerActive;
private ExecutionCircuitBreaker _circuitBreaker;
private System.IO.StreamWriter _fileLog;
private readonly object _fileLock = new object();
#region User-Configurable Properties
@@ -93,8 +101,97 @@ namespace NinjaTrader.NinjaScript.Strategies
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
public bool EnableVerboseLogging { get; set; }
[NinjaScriptProperty]
[Display(Name = "Min Trade Grade (1=F,2=D,3=C,4=B,5=A,6=A+)", GroupName = "Confluence", Order = 1)]
[Range(0, 6)]
public int MinTradeGrade { get; set; }
[NinjaScriptProperty]
[Display(Name = "Enable File Logging", GroupName = "Diagnostics", Order = 10)]
public bool EnableFileLogging { get; set; }
[NinjaScriptProperty]
[Display(Name = "Log Directory", GroupName = "Diagnostics", Order = 11)]
public string LogDirectory { get; set; }
[NinjaScriptProperty]
[Display(Name = "Enable Long Trades", GroupName = "Trade Direction", Order = 1)]
public bool EnableLongTrades { get; set; }
[NinjaScriptProperty]
[Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)]
public bool EnableShortTrades { get; set; }
[NinjaScriptProperty]
[Display(Name = "Enable Auto Breakeven", GroupName = "Exit Management", Order = 1)]
public bool EnableAutoBreakeven { get; set; }
[NinjaScriptProperty]
[Display(Name = "Breakeven Trigger Ticks", GroupName = "Exit Management", Order = 2)]
[Range(1, 100)]
public int BreakevenTriggerTicks { get; set; }
[NinjaScriptProperty]
[Display(Name = "Breakeven Offset Ticks", GroupName = "Exit Management", Order = 3)]
[Range(0, 20)]
public int BreakevenOffsetTicks { get; set; }
[NinjaScriptProperty]
[Display(Name = "Enable Runner", GroupName = "Exit Management", Order = 4)]
public bool EnableRunner { get; set; }
[NinjaScriptProperty]
[Display(Name = "Runner Trail Ticks", GroupName = "Exit Management", Order = 5)]
[Range(4, 100)]
public int RunnerTrailTicks { get; set; }
#endregion
// INT8ExecutionBridge implementation
public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
{
if (stopTicks > 0)
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
if (targetTicks > 0)
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
EnterLong(quantity, signalName);
}
public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize)
{
if (stopTicks > 0)
SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false);
if (targetTicks > 0)
SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks);
EnterShort(quantity, signalName);
}
public void ExitLongManaged(string signalName)
{
ExitLong(signalName);
}
public void ExitShortManaged(string signalName)
{
ExitShort(signalName);
}
public void FlattenAll()
{
ExitLong("EmergencyFlatten");
ExitShort("EmergencyFlatten");
}
/// <summary>
/// Returns true if the concrete strategy has ForceSessionReset enabled.
/// Override in subclass to expose the NinjaScript parameter value.
/// Default returns false so base class never forces a reset unless overridden.
/// </summary>
protected virtual bool GetForceSessionReset()
{
return false;
}
/// <summary>
/// Create the SDK strategy instance.
/// </summary>
@@ -112,7 +209,7 @@ namespace NinjaTrader.NinjaScript.Strategies
Description = "SDK-integrated strategy base";
// Name intentionally not set - this is an abstract base class
Calculate = Calculate.OnBarClose;
EntriesPerDirection = 1;
EntriesPerDirection = 2;
EntryHandling = EntryHandling.AllEntries;
IsExitOnSessionCloseStrategy = true;
ExitOnSessionCloseSeconds = 30;
@@ -120,12 +217,12 @@ namespace NinjaTrader.NinjaScript.Strategies
MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;
OrderFillResolution = OrderFillResolution.Standard;
Slippage = 0;
StartBehavior = StartBehavior.WaitUntilFlat;
StartBehavior = StartBehavior.AdoptAccountPosition;
TimeInForce = TimeInForce.Gtc;
TraceOrders = false;
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
StopTargetHandling = StopTargetHandling.PerEntryExecution;
BarsRequiredToTrade = 20;
BarsRequiredToTrade = 1;
EnableSDK = true;
DailyLossLimit = 1000.0;
@@ -133,10 +230,21 @@ namespace NinjaTrader.NinjaScript.Strategies
MaxOpenPositions = 3;
RiskPerTrade = 100.0;
MinContracts = 1;
MaxContracts = 10;
MaxContracts = 3;
EnableKillSwitch = false;
EnableVerboseLogging = false;
MinTradeGrade = 4;
EnableFileLogging = true;
LogDirectory = string.Empty;
EnableLongTrades = true;
EnableShortTrades = true;
EnableAutoBreakeven = true;
BreakevenTriggerTicks = 20;
BreakevenOffsetTicks = 1;
EnableRunner = true;
RunnerTrailTicks = 20;
_killSwitchTriggered = false;
_connectionLost = false;
}
else if (State == State.DataLoaded)
{
@@ -144,9 +252,20 @@ namespace NinjaTrader.NinjaScript.Strategies
{
try
{
// DIAGNOSTIC: Print actual runtime property values to confirm
// what NT8 loaded vs what SetDefaults specified.
Print(string.Format("[SDK-DIAG] SetDefaults check: BE={0} Trail={1} MaxC={2} SB={3} EPD={4}",
BreakevenTriggerTicks,
RunnerTrailTicks,
MaxContracts,
StartBehavior,
EntriesPerDirection));
InitFileLog();
InitializeSdkComponents();
_sdkInitialized = true;
Print(string.Format("[SDK] {0} initialized successfully", Name));
WriteSettingsFile();
WriteSessionHeader();
}
catch (Exception ex)
{
@@ -156,11 +275,90 @@ namespace NinjaTrader.NinjaScript.Strategies
}
}
}
else if (State == State.Realtime)
{
_realtimeBarSeen = false;
// If ForceSessionReset is enabled, push a reset signal into the SDK strategy
// so _tradeTaken is cleared before any live bar is processed.
// This recovers from replay-burst scenarios where historical bars set _tradeTaken.
if (_sdkStrategy != null && GetForceSessionReset())
{
var resetParams = new Dictionary<string, object>();
resetParams.Add("force_session_reset", true);
_sdkStrategy.SetParameters(resetParams);
Print(string.Format("[SDK] ForceSessionReset: _tradeTaken cleared on live start at {0}", DateTime.Now.ToString("HH:mm:ss")));
}
WriteSettingsFile();
}
else if (State == State.Terminated)
{
PortfolioRiskManager.Instance.UnregisterStrategy(Name);
WriteSessionFooter();
}
}
protected override void OnBarUpdate()
{
// Kill switch check — must be first
// Only process primary bar series — ignore secondary data series updates.
// Secondary series (e.g. daily bars for confluence) trigger OnBarUpdate separately
// and must never generate strategy signals.
if (BarsInProgress != 0)
return;
// Require 7 completed daily bars before allowing any signal.
// NarrowRangeFactorCalculator needs 7 daily bars for NR7 scoring.
// Without this guard, NR7 silently returns its floor score (0.3)
// which may suppress trades via the MinTradeGrade confluence gate.
if (BarsArray != null && BarsArray.Length > 1
&& CurrentBars != null && CurrentBars.Length > 1
&& CurrentBars[1] < 7)
{
// Always print warm-up status — visible in Strategy Analyzer output
// to confirm how many daily bars are available on a given backtest range.
if (CurrentBar % 20 == 0)
Print(String.Format("[SDK] Daily warm-up: {0}/7 bars — waiting for NR7 history. Extend backtest start date or add pre-load days.",
CurrentBars[1] + 1));
return;
}
if (!_sdkInitialized || _sdkStrategy == null)
{
return;
}
if (CurrentBar < BarsRequiredToTrade)
{
return;
}
if (Time[0] == _lastBarTime)
return;
if (Time[0].Date != _lastBarTime.Date && _lastBarTime != DateTime.MinValue)
{
_runnerActive = false;
_breakevenMoved = false;
_scalerSignalName = null;
_runnerSignalName = null;
}
_lastBarTime = Time[0];
// Mark first bar seen after going realtime. Until this fires, we're
// processing catch-up replay bars and must not submit orders.
if (State == State.Realtime && !_realtimeBarSeen)
{
_realtimeBarSeen = true;
Print(string.Format("[SDK] First realtime bar seen: {0}", Time[0]));
return;
}
// Sync actual open position to portfolio manager on every bar
PortfolioRiskManager.Instance.UpdateOpenContracts(Name, Math.Abs(Position.Quantity));
// Kill switch — checked AFTER bar guards so ExitLong/ExitShort are valid
if (EnableKillSwitch)
{
if (!_killSwitchTriggered)
@@ -177,28 +375,44 @@ namespace NinjaTrader.NinjaScript.Strategies
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
}
}
return;
}
if (!_sdkInitialized || _sdkStrategy == null)
// Connection loss guard — do not submit new orders if broker is disconnected
if (_connectionLost)
{
if (CurrentBar == 0)
Print(string.Format("[SDK] Not initialized: sdkInit={0}, strategy={1}", _sdkInitialized, _sdkStrategy != null));
if (EnableVerboseLogging)
Print(string.Format("[NT8-SDK] Bar skipped — connection lost: {0}", Time[0]));
return;
}
if (CurrentBar < BarsRequiredToTrade)
// Hard RTH guard using NT8 bar time converted from CT to ET.
// Belt-and-suspenders against SDK session timezone issues.
DateTime ntBarTimeEt;
try
{
if (CurrentBar == 0)
Print(string.Format("[SDK] Waiting for bars: current={0}, required={1}", CurrentBar, BarsRequiredToTrade));
return;
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
centralZone);
ntBarTimeEt = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
}
catch
{
ntBarTimeEt = Time[0];
}
if (Time[0] == _lastBarTime)
return;
bool isRthBar = ntBarTimeEt.TimeOfDay >= new TimeSpan(9, 30, 0)
&& ntBarTimeEt.TimeOfDay < new TimeSpan(16, 0, 0);
_lastBarTime = Time[0];
if (!isRthBar)
{
if (EnableVerboseLogging && CurrentBar % 500 == 0)
Print(string.Format("[SDK] Skipping ETH bar {0} at {1:HH:mm} ET",
CurrentBar, ntBarTimeEt));
return;
}
// Log first processable bar and every 100th bar.
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
@@ -212,6 +426,50 @@ namespace NinjaTrader.NinjaScript.Strategies
Close[0]));
}
// --- Breakeven and runner trailing monitor ---
if (_runnerActive && !string.IsNullOrEmpty(_runnerSignalName) && Position.Quantity != 0)
{
double entryPrice = Position.AveragePrice;
double currentClose = Close[0];
bool isLong = Position.MarketPosition == MarketPosition.Long;
double profitTicks = isLong
? (currentClose - entryPrice) / TickSize
: (entryPrice - currentClose) / TickSize;
// Move runner stop to breakeven + offset once trigger is reached
if (EnableAutoBreakeven && !_breakevenMoved && profitTicks >= BreakevenTriggerTicks)
{
double bePrice = isLong
? entryPrice + (BreakevenOffsetTicks * TickSize)
: entryPrice - (BreakevenOffsetTicks * TickSize);
SetStopLoss(_runnerSignalName, CalculationMode.Price, bePrice, false);
_breakevenMoved = true;
Print(String.Format("[SDK] Runner breakeven set at {0:F2} (profit={1:F0} ticks)",
bePrice, profitTicks));
if (EnableFileLogging)
FileLog(String.Format("BREAKEVEN runner stop -> {0:F2} profit={1:F0}ticks",
bePrice, profitTicks));
}
// Activate trailing stop on runner once breakeven is secured
if (_breakevenMoved && RunnerTrailTicks > 0)
{
SetTrailStop(_runnerSignalName, CalculationMode.Ticks, RunnerTrailTicks, false);
}
}
// Clear runner state when flat
if (Position.Quantity == 0 && _runnerActive)
{
_runnerActive = false;
_breakevenMoved = false;
if (EnableFileLogging && !string.IsNullOrEmpty(_runnerSignalName))
FileLog("RUNNER closed — position flat");
}
try
{
var barData = ConvertCurrentBar();
@@ -290,9 +548,186 @@ namespace NinjaTrader.NinjaScript.Strategies
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
return;
FileLog(string.Format("FILL {0} {1} @ {2:F2} | OrderId={3}",
execution.MarketPosition,
execution.Quantity,
execution.Price,
execution.OrderId));
var fill = new NT8.Core.Common.Models.OrderFill(
orderId,
execution.Order != null ? execution.Order.Instrument.MasterInstrument.Name : string.Empty,
execution.Quantity,
execution.Price,
time,
0.0,
executionId);
PortfolioRiskManager.Instance.ReportFill(Name, fill);
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
}
protected override void OnPositionUpdate(
NinjaTrader.Cbi.Position position,
double averagePrice,
int quantity,
MarketPosition marketPosition)
{
if (!_sdkInitialized || _riskManager == null)
return;
try
{
double dayPnL = 0.0;
if (Account != null)
{
try
{
dayPnL = Account.Get(AccountItem.RealizedProfitLoss, Currency.UsDollar);
}
catch
{
dayPnL = 0.0;
}
}
_riskManager.OnPnLUpdate(dayPnL, dayPnL);
PortfolioRiskManager.Instance.ReportPnL(Name, dayPnL);
if (EnableVerboseLogging)
Print(string.Format("[SDK] P&L update: DayPnL={0:C} Position={1} Qty={2}",
dayPnL, marketPosition, quantity));
FileLog(string.Format("PNL_UPDATE DayPnL={0:C} Position={1} Qty={2}",
dayPnL, marketPosition, quantity));
}
catch (Exception ex)
{
Print(string.Format("[SDK] OnPositionUpdate error: {0}", ex.Message));
}
}
/// <summary>
/// Handles broker connection status changes. Halts new orders on disconnect,
/// logs reconnect, and resets the connection flag when restored.
/// NinjaScript signature: single ConnectionStatusEventArgs parameter.
/// </summary>
protected override void OnConnectionStatusUpdate(
ConnectionStatusEventArgs connectionStatusUpdate)
{
if (connectionStatusUpdate == null) return;
if (connectionStatusUpdate.Status == ConnectionStatus.Connected)
{
if (_connectionLost)
{
_connectionLost = false;
Print(string.Format("[NT8-SDK] Connection RESTORED at {0} — trading resumed.",
DateTime.Now.ToString("HH:mm:ss")));
FileLog(string.Format("CONNECTION RESTORED at {0}", DateTime.Now.ToString("HH:mm:ss")));
}
}
else if (connectionStatusUpdate.Status == ConnectionStatus.Disconnected ||
connectionStatusUpdate.Status == ConnectionStatus.ConnectionLost)
{
if (!_connectionLost)
{
_connectionLost = true;
Print(string.Format("[NT8-SDK] Connection LOST at {0} — halting new orders. Status={1}",
DateTime.Now.ToString("HH:mm:ss"),
connectionStatusUpdate.Status));
FileLog(string.Format("CONNECTION LOST at {0} Status={1}",
DateTime.Now.ToString("HH:mm:ss"),
connectionStatusUpdate.Status));
}
}
}
private void InitFileLog()
{
if (!EnableFileLogging)
return;
try
{
string dir = string.IsNullOrEmpty(LogDirectory)
? System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"NinjaTrader 8", "log", "nt8-sdk")
: LogDirectory;
System.IO.Directory.CreateDirectory(dir);
string path = System.IO.Path.Combine(
dir,
string.Format("session_{0}.log", DateTime.Now.ToString("yyyyMMdd_HHmmss")));
_fileLog = new System.IO.StreamWriter(path, false);
_fileLog.AutoFlush = true;
Print(string.Format("[NT8-SDK] File log started: {0}", path));
}
catch (Exception ex)
{
Print(string.Format("[NT8-SDK] Failed to open file log: {0}", ex.Message));
}
}
private void FileLog(string message)
{
if (_fileLog == null)
return;
lock (_fileLock)
{
try
{
_fileLog.WriteLine(string.Format("[{0:HH:mm:ss.fff}] {1}", DateTime.Now, message));
}
catch
{
}
}
}
private void WriteSessionHeader()
{
FileLog("=== SESSION START " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ===");
FileLog(string.Format("Mode : {0}", State == State.Historical ? "BACKTEST" : "LIVE/SIM"));
FileLog(string.Format("Strategy : {0}", Name));
FileLog(string.Format("Account : {0}", Account != null ? Account.Name : "N/A"));
FileLog(string.Format("Symbol : {0}", Instrument != null ? Instrument.FullName : "N/A"));
FileLog(string.Format("Risk : DailyLimit=${0} MaxTradeRisk=${1} RiskPerTrade=${2}",
DailyLossLimit,
MaxTradeRisk,
RiskPerTrade));
FileLog(string.Format("Sizing : MinContracts={0} MaxContracts={1}", MinContracts, MaxContracts));
FileLog(string.Format("VerboseLog : {0} FileLog: {1}", EnableVerboseLogging, EnableFileLogging));
FileLog(string.Format("ConnectionLost : {0}", _connectionLost));
FileLog("---");
}
private void WriteSessionFooter()
{
FileLog("---");
FileLog("=== SESSION END " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ===");
if (_fileLog != null)
{
lock (_fileLock)
{
try
{
_fileLog.Close();
}
catch
{
}
_fileLog = null;
}
}
}
private void InitializeSdkComponents()
{
_logger = new BasicLogger(Name);
@@ -317,7 +752,7 @@ namespace NinjaTrader.NinjaScript.Strategies
_riskConfig,
_sizingConfig);
_riskManager = new BasicRiskManager(_logger);
_riskManager = new BasicRiskManager(_logger, DailyLossLimit);
_positionSizer = new BasicPositionSizer(_logger);
_circuitBreaker = new ExecutionCircuitBreaker(
_logger,
@@ -331,6 +766,8 @@ namespace NinjaTrader.NinjaScript.Strategies
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
ConfigureStrategyParameters();
PortfolioRiskManager.Instance.RegisterStrategy(Name, _riskConfig);
Print(string.Format("[NT8-SDK] Registered with PortfolioRiskManager: {0}", PortfolioRiskManager.Instance.GetStatusSnapshot()));
_ordersSubmittedToday = 0;
_lastBarTime = DateTime.MinValue;
@@ -341,9 +778,24 @@ namespace NinjaTrader.NinjaScript.Strategies
private BarData ConvertCurrentBar()
{
DateTime barTimeEt;
try
{
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
centralZone);
barTimeEt = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
}
catch
{
barTimeEt = Time[0];
}
return NT8DataConverter.ConvertBar(
Instrument.MasterInstrument.Name,
Time[0],
barTimeEt,
Open[0],
High[0],
Low[0],
@@ -354,6 +806,21 @@ namespace NinjaTrader.NinjaScript.Strategies
private StrategyContext BuildStrategyContext()
{
DateTime etTime;
try
{
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
centralZone);
etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
}
catch
{
etTime = Time[0];
}
var customData = new Dictionary<string, object>();
customData.Add("CurrentBar", CurrentBar);
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
@@ -361,7 +828,7 @@ namespace NinjaTrader.NinjaScript.Strategies
return NT8DataConverter.ConvertContext(
Instrument.MasterInstrument.Name,
Time[0],
etTime,
BuildPositionInfo(),
BuildAccountInfo(),
BuildSessionInfo(),
@@ -370,7 +837,23 @@ namespace NinjaTrader.NinjaScript.Strategies
private AccountInfo BuildAccountInfo()
{
var accountInfo = NT8DataConverter.ConvertAccount(100000.0, 250000.0, 0.0, 0.0, DateTime.UtcNow);
double cashValue = 100000.0;
double buyingPower = 250000.0;
try
{
if (Account != null)
{
cashValue = Account.Get(AccountItem.CashValue, Currency.UsDollar);
buyingPower = Account.Get(AccountItem.BuyingPower, Currency.UsDollar);
}
}
catch (Exception ex)
{
Print(string.Format("[NT8-SDK] WARNING: Could not read live account balance, using defaults: {0}", ex.Message));
}
var accountInfo = NT8DataConverter.ConvertAccount(cashValue, buyingPower, 0.0, 0.0, DateTime.UtcNow);
_lastAccountInfo = accountInfo;
return accountInfo;
}
@@ -391,20 +874,78 @@ namespace NinjaTrader.NinjaScript.Strategies
private MarketSession BuildSessionInfo()
{
if (_currentSession != null && _currentSession.SessionStart.Date == Time[0].Date)
return _currentSession;
DateTime etTime;
try
{
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
centralZone);
etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
}
catch
{
etTime = Time[0];
}
var sessionStart = Time[0].Date.AddHours(9).AddMinutes(30);
var sessionEnd = Time[0].Date.AddHours(16);
var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16;
var sessionName = isRth ? "RTH" : "ETH";
// Futures trade nearly 24 hours. Bars at/after 17:00 ET belong to the next
// calendar day's RTH trading session.
DateTime tradingDate;
if (etTime.TimeOfDay >= new TimeSpan(17, 0, 0))
tradingDate = etTime.Date.AddDays(1);
else
tradingDate = etTime.Date;
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, sessionName);
if (EnableVerboseLogging && (CurrentBar == BarsRequiredToTrade || CurrentBar % 500 == 0))
{
Print(string.Format("[SDK-TZ] Bar {0}: NT8 Time[0]={1:yyyy-MM-dd HH:mm:ss} | etTime={2:yyyy-MM-dd HH:mm:ss} | isRth={3}",
CurrentBar,
Time[0],
etTime,
etTime.TimeOfDay >= TimeSpan.FromHours(9.5) && etTime.TimeOfDay < TimeSpan.FromHours(16.0)));
}
var sessionStart = tradingDate.AddHours(9).AddMinutes(30);
var sessionEnd = tradingDate.AddHours(16);
var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5)
&& etTime.TimeOfDay < TimeSpan.FromHours(16.0);
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, isRth ? "RTH" : "ETH");
return _currentSession;
}
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
{
// In live/SIM: block if we haven't seen a genuine realtime bar yet (replay guard).
// In Strategy Analyzer (State.Historical): always allow — backtest must execute normally.
if (State == State.Realtime && !_realtimeBarSeen)
return;
// Portfolio-level risk check — runs before per-strategy risk validation
var portfolioDecision = PortfolioRiskManager.Instance.ValidatePortfolioRisk(Name, intent);
if (!portfolioDecision.Allow)
{
Print(string.Format("[SDK] Portfolio blocked: {0}", portfolioDecision.RejectReason));
if (_logger != null)
_logger.LogWarning("Portfolio risk blocked order: {0}", portfolioDecision.RejectReason);
return;
}
// Direction filter — checked before risk to avoid unnecessary processing
if (intent.Side == SdkOrderSide.Buy && !EnableLongTrades)
{
if (EnableVerboseLogging)
Print(string.Format("[SDK] Long trade filtered by direction setting: {0}", intent.Symbol));
return;
}
if (intent.Side == SdkOrderSide.Sell && !EnableShortTrades)
{
if (EnableVerboseLogging)
Print(string.Format("[SDK] Short trade filtered by direction setting: {0}", intent.Symbol));
return;
}
if (EnableVerboseLogging)
Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol));
@@ -460,45 +1001,82 @@ namespace NinjaTrader.NinjaScript.Strategies
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
{
// Circuit breaker gate
if (State != State.Historical)
{
if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
{
var state = _circuitBreaker.GetState();
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
var cbState = _circuitBreaker.GetState();
Print(String.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", cbState.Reason));
if (_logger != null)
_logger.LogWarning("Circuit breaker blocked order: {0}", state.Reason);
_logger.LogWarning("Circuit breaker blocked order: {0}", cbState.Reason);
return;
}
}
try
{
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks);
_executionAdapter.SubmitOrder(request, orderName);
bool useRunner = EnableRunner && request.Quantity >= 2;
int scalerQty = useRunner ? request.Quantity - 1 : request.Quantity;
int runnerQty = useRunner ? 1 : 0;
string baseId = Guid.NewGuid().ToString("N").Substring(0, 12);
_scalerSignalName = String.Format("SDK_{0}_S_{1}", intent.Symbol, baseId);
_runnerSignalName = useRunner ? String.Format("SDK_{0}_R_{1}", intent.Symbol, baseId) : null;
_breakevenMoved = false;
_runnerActive = useRunner;
if (EnableFileLogging)
{
string grade = "N/A";
string score = "N/A";
string factors = string.Empty;
if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score"))
{
var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore;
if (cs != null)
{
grade = cs.Grade.ToString();
score = cs.WeightedScore.ToString("F3");
var sb = new System.Text.StringBuilder();
foreach (var f in cs.Factors)
sb.Append(String.Format("{0}={1:F2} ", f.Type, f.Score));
factors = sb.ToString().TrimEnd();
}
}
FileLog(String.Format("SIGNAL {0} | Grade={1} | Score={2}", intent.Side, grade, score));
if (!string.IsNullOrEmpty(factors))
FileLog(String.Format(" Factors: {0}", factors));
FileLog(String.Format("SUBMIT Scaler={0} Runner={1} Stop={2} Target={3}",
scalerQty, runnerQty, intent.StopTicks,
intent.TargetTicks.HasValue ? intent.TargetTicks.Value.ToString() : "none"));
}
// --- Submit scaler leg ---
if (intent.StopTicks > 0)
SetStopLoss(_scalerSignalName, CalculationMode.Ticks, (int)intent.StopTicks, false);
if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0)
SetProfitTarget(_scalerSignalName, CalculationMode.Ticks, (int)intent.TargetTicks.Value);
if (request.Side == OmsOrderSide.Buy)
{
if (request.Type == OmsOrderType.Market)
EnterLong(request.Quantity, orderName);
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
EnterLongLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
EnterLongStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
}
else if (request.Side == OmsOrderSide.Sell)
{
if (request.Type == OmsOrderType.Market)
EnterShort(request.Quantity, orderName);
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
EnterShortLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
EnterShortStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
}
EnterLong(scalerQty, _scalerSignalName);
else
EnterShort(scalerQty, _scalerSignalName);
// --- Submit runner leg (no fixed target — exits via trailing stop) ---
if (useRunner)
{
if (intent.StopTicks > 0)
SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false);
SetStopLoss(_runnerSignalName, CalculationMode.Ticks, (int)intent.StopTicks, false);
// No SetProfitTarget on runner — trail stop will manage exit
if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0)
SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value);
if (request.Side == OmsOrderSide.Buy)
EnterLong(runnerQty, _runnerSignalName);
else
EnterShort(runnerQty, _runnerSignalName);
}
_executionAdapter.SubmitOrder(request, _scalerSignalName);
if (_circuitBreaker != null)
_circuitBreaker.OnSuccess();
@@ -507,8 +1085,7 @@ namespace NinjaTrader.NinjaScript.Strategies
{
if (_circuitBreaker != null)
_circuitBreaker.OnFailure();
Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
Print(String.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
if (_logger != null)
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
throw;
@@ -539,6 +1116,83 @@ namespace NinjaTrader.NinjaScript.Strategies
return null;
return _executionAdapter.GetOrderStatus(orderName);
}
/// <summary>
/// Returns all strategy parameter lines for the settings export file.
/// Override in subclasses to append strategy-specific parameters.
/// Call base.GetStrategySettingsLines() first then add to the list.
/// </summary>
protected virtual List<string> GetStrategySettingsLines()
{
var lines = new List<string>();
lines.Add("=== STRATEGY SETTINGS EXPORT ===");
lines.Add(string.Format("ExportTime : {0:yyyy-MM-dd HH:mm:ss}", DateTime.Now));
lines.Add(string.Format("StrategyName : {0}", Name));
lines.Add(string.Format("Description : {0}", Description));
lines.Add(string.Format("Account : {0}", Account != null ? Account.Name : "N/A"));
lines.Add(string.Format("Instrument : {0}", Instrument != null ? Instrument.FullName : "N/A"));
lines.Add(string.Format("BarsPeriod : {0} {1}", BarsPeriod != null ? BarsPeriod.Value.ToString() : "N/A", BarsPeriod != null ? BarsPeriod.BarsPeriodType.ToString() : string.Empty));
lines.Add(string.Format("BarsRequiredToTrade: {0}", BarsRequiredToTrade));
lines.Add(string.Format("Calculate : {0}", Calculate));
lines.Add("--- Risk ---");
lines.Add(string.Format("DailyLossLimit : {0:C}", DailyLossLimit));
lines.Add(string.Format("MaxTradeRisk : {0:C}", MaxTradeRisk));
lines.Add(string.Format("MaxOpenPositions : {0}", MaxOpenPositions));
lines.Add(string.Format("RiskPerTrade : {0:C}", RiskPerTrade));
lines.Add("--- Sizing ---");
lines.Add(string.Format("MinContracts : {0}", MinContracts));
lines.Add(string.Format("MaxContracts : {0}", MaxContracts));
lines.Add("--- Direction ---");
lines.Add(string.Format("EnableLongTrades : {0}", EnableLongTrades));
lines.Add(string.Format("EnableShortTrades : {0}", EnableShortTrades));
lines.Add("--- Controls ---");
lines.Add(string.Format("EnableKillSwitch : {0}", EnableKillSwitch));
lines.Add(string.Format("EnableVerboseLogging: {0}", EnableVerboseLogging));
lines.Add(string.Format("MinTradeGrade : {0}", MinTradeGrade));
lines.Add(string.Format("EnableFileLogging : {0}", EnableFileLogging));
lines.Add(string.Format("LogDirectory : {0}", string.IsNullOrEmpty(LogDirectory) ? "(default)" : LogDirectory));
lines.Add("--- Portfolio ---");
lines.Add(string.Format("PortfolioStatus : {0}", PortfolioRiskManager.Instance.GetStatusSnapshot()));
lines.Add("=== END SETTINGS ===");
return lines;
}
/// <summary>
/// Writes a settings export file to the same directory as the session log.
/// File is named settings_STRATEGYNAME_YYYYMMDD_HHmmss.txt.
/// Only writes when EnableVerboseLogging is true.
/// </summary>
private void WriteSettingsFile()
{
if (!EnableVerboseLogging) return;
try
{
string dir = string.IsNullOrEmpty(LogDirectory)
? System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"NinjaTrader 8", "log", "nt8-sdk")
: LogDirectory;
System.IO.Directory.CreateDirectory(dir);
string safeName = Name.Replace(" ", "_").Replace("/", "_").Replace("\\", "_");
string path = System.IO.Path.Combine(dir,
string.Format("settings_{0}_{1}.txt",
safeName,
DateTime.Now.ToString("yyyyMMdd_HHmmss")));
var lines = GetStrategySettingsLines();
System.IO.File.WriteAllLines(path, lines.ToArray());
Print(string.Format("[NT8-SDK] Settings exported: {0}", path));
FileLog(string.Format("SETTINGS FILE: {0}", path));
}
catch (Exception ex)
{
Print(string.Format("[NT8-SDK] WARNING: Could not write settings file: {0}", ex.Message));
}
}
}
}

View File

@@ -12,6 +12,7 @@ using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators;
using NinjaTrader.NinjaScript.Strategies;
using NT8.Core.Common.Interfaces;
using NT8.Core.Intelligence;
using NT8.Strategies.Examples;
using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy;
@@ -22,6 +23,8 @@ namespace NinjaTrader.NinjaScript.Strategies
/// </summary>
public class SimpleORBNT8 : NT8StrategyBase
{
private int _lastSignalDirection;
[NinjaScriptProperty]
[Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)]
[Range(5, 120)]
@@ -37,6 +40,10 @@ namespace NinjaTrader.NinjaScript.Strategies
[Range(1, 50)]
public int StopTicks { get; set; }
[NinjaScriptProperty]
[Display(Name = "Force Session Reset On Start", GroupName = "ORB Strategy", Order = 10)]
public bool ForceSessionReset { get; set; }
[NinjaScriptProperty]
[Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)]
[Range(1, 100)]
@@ -47,7 +54,9 @@ namespace NinjaTrader.NinjaScript.Strategies
if (State == State.SetDefaults)
{
Name = "Simple ORB NT8";
Description = "Opening Range Breakout with NT8 SDK integration";
Description = "v0.4.0 | 2026-03-19 | NR7+ORB factors, PortfolioRiskManager, connection recovery, live account balance";
// Daily bar series is added automatically via AddDataSeries in Configure.
OpeningRangeMinutes = 30;
StdDevMultiplier = 1.0;
@@ -56,18 +65,57 @@ namespace NinjaTrader.NinjaScript.Strategies
DailyLossLimit = 1000.0;
MaxTradeRisk = 200.0;
MaxOpenPositions = 1;
MaxOpenPositions = 2;
RiskPerTrade = 100.0;
MinContracts = 1;
MaxContracts = 3;
Calculate = Calculate.OnBarClose;
BarsRequiredToTrade = 50;
MinTradeGrade = 5;
EnableLongTrades = true;
// Long-only: short trades permanently disabled pending backtest confirmation
EnableShortTrades = false;
EnableAutoBreakeven = true;
BreakevenTriggerTicks = 20;
BreakevenOffsetTicks = 1;
EnableRunner = true;
RunnerTrailTicks = 20;
ForceSessionReset = false;
StartBehavior = StartBehavior.AdoptAccountPosition;
}
else if (State == State.Configure)
{
AddDataSeries(BarsPeriodType.Day, 1);
}
base.OnStateChange();
}
protected override void OnBarUpdate()
{
if (_strategyConfig != null && BarsArray != null && BarsArray.Length > 1)
{
DailyBarContext dailyContext = BuildDailyBarContext(_lastSignalDirection, 0.0, (double)Volume[0]);
_strategyConfig.Parameters["daily_bars"] = dailyContext;
}
base.OnBarUpdate();
if (Position != null)
{
if (Position.MarketPosition == MarketPosition.Long)
_lastSignalDirection = 1;
else if (Position.MarketPosition == MarketPosition.Short)
_lastSignalDirection = -1;
}
}
protected override bool GetForceSessionReset()
{
return ForceSessionReset;
}
protected override IStrategy CreateSdkStrategy()
{
return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier);
@@ -97,16 +145,114 @@ namespace NinjaTrader.NinjaScript.Strategies
_strategyConfig.Parameters["StopTicks"] = StopTicks;
_strategyConfig.Parameters["TargetTicks"] = TargetTicks;
_strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
_strategyConfig.Parameters["MinTradeGrade"] = MinTradeGrade;
if (Instrument != null && Instrument.MasterInstrument != null)
{
_strategyConfig.Parameters["TickSize"] = Instrument.MasterInstrument.TickSize;
}
if (_logger != null)
{
_logger.LogInformation(
"Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks",
"Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks, Long={3}, Short={4}",
OpeningRangeMinutes,
StopTicks,
TargetTicks);
TargetTicks,
EnableLongTrades,
EnableShortTrades);
}
}
/// <summary>
/// Appends ORB-specific parameters to the base settings export.
/// </summary>
protected override List<string> GetStrategySettingsLines()
{
var lines = base.GetStrategySettingsLines();
// Insert ORB section before the final === END SETTINGS === line
int endIdx = lines.Count - 1;
lines.Insert(endIdx, "--- ORB Strategy ---");
lines.Insert(endIdx + 1, string.Format("OpeningRangeMinutes: {0}", OpeningRangeMinutes));
lines.Insert(endIdx + 2, string.Format("StdDevMultiplier : {0:F2}", StdDevMultiplier));
lines.Insert(endIdx + 3, string.Format("StopTicks : {0}", StopTicks));
lines.Insert(endIdx + 4, string.Format("TargetTicks : {0}", TargetTicks));
lines.Insert(endIdx + 5, string.Format("MinTradeGrade : {0}", MinTradeGrade));
double tickDollarValue = 0.25 * 50.0;
if (Instrument != null && Instrument.MasterInstrument != null)
tickDollarValue = Instrument.MasterInstrument.TickSize * Instrument.MasterInstrument.PointValue;
lines.Insert(endIdx + 6, string.Format("StopDollars : {0:C}", StopTicks * tickDollarValue));
lines.Insert(endIdx + 7, string.Format("TargetDollars : {0:C}", TargetTicks * tickDollarValue));
lines.Insert(endIdx + 8, string.Format("RR_Ratio : {0:F2}:1", (double)TargetTicks / StopTicks));
lines.Insert(endIdx + 9, String.Format("AutoBreakeven : {0} @ {1}ticks + {2}tick offset",
EnableAutoBreakeven, BreakevenTriggerTicks, BreakevenOffsetTicks));
lines.Insert(endIdx + 10, String.Format("Runner : {0} | Trail={1}ticks",
EnableRunner, RunnerTrailTicks));
return lines;
}
/// <summary>
/// Builds a DailyBarContext from the secondary daily bar series.
/// Returns a context with Count=0 if fewer than 2 daily bars are available.
/// </summary>
/// <param name="tradeDirection">1 for long, -1 for short.</param>
/// <param name="orbRangeTicks">ORB range in ticks for ORB range factor.</param>
/// <param name="breakoutBarVolume">Volume of the current breakout bar.</param>
/// <returns>Populated daily context for confluence scoring.</returns>
private DailyBarContext BuildDailyBarContext(int tradeDirection, double orbRangeTicks, double breakoutBarVolume)
{
DailyBarContext ctx = new DailyBarContext();
ctx.TradeDirection = tradeDirection;
ctx.BreakoutBarVolume = breakoutBarVolume;
ctx.TodayOpen = Open[0];
if (BarsArray == null || BarsArray.Length < 2 || CurrentBars == null || CurrentBars.Length < 2)
{
ctx.Count = 0;
return ctx;
}
int dailyBarsAvailable = CurrentBars[1] + 1;
int lookback = Math.Min(10, dailyBarsAvailable);
if (lookback < 2)
{
ctx.Count = 0;
return ctx;
}
ctx.Highs = new double[lookback];
ctx.Lows = new double[lookback];
ctx.Closes = new double[lookback];
ctx.Opens = new double[lookback];
ctx.Volumes = new long[lookback];
ctx.Count = lookback;
for (int i = 0; i < lookback; i++)
{
int barsAgo = lookback - 1 - i;
ctx.Highs[i] = Highs[1][barsAgo];
ctx.Lows[i] = Lows[1][barsAgo];
ctx.Closes[i] = Closes[1][barsAgo];
ctx.Opens[i] = Opens[1][barsAgo];
ctx.Volumes[i] = (long)Volumes[1][barsAgo];
}
double sumVol = 0.0;
int intradayCount = 0;
int maxBars = Math.Min(78, CurrentBar + 1);
for (int i = 0; i < maxBars; i++)
{
sumVol += Volume[i];
intradayCount++;
}
ctx.AvgIntradayBarVolume = intradayCount > 0 ? sumVol / intradayCount : Volume[0];
return ctx;
}
}
}

View File

@@ -43,6 +43,31 @@ namespace NT8.Core.Intelligence
/// </summary>
Risk = 6,
/// <summary>
/// Narrow range contraction quality (NR4/NR7 concepts).
/// </summary>
NarrowRange = 7,
/// <summary>
/// Opening range size relative to average daily ATR/range.
/// </summary>
OrbRangeVsAtr = 8,
/// <summary>
/// Alignment between overnight gap direction and trade direction.
/// </summary>
GapDirectionAlignment = 9,
/// <summary>
/// Breakout bar volume strength relative to intraday average volume.
/// </summary>
BreakoutVolumeStrength = 10,
/// <summary>
/// Prior day close location strength in prior day range.
/// </summary>
PriorDayCloseStrength = 11,
/// <summary>
/// Additional custom factor.
/// </summary>

View File

@@ -110,6 +110,11 @@ namespace NT8.Core.Intelligence
_factorWeights.Add(FactorType.Volatility, 1.0);
_factorWeights.Add(FactorType.Timing, 1.0);
_factorWeights.Add(FactorType.ExecutionQuality, 1.0);
_factorWeights.Add(FactorType.NarrowRange, 1.0);
_factorWeights.Add(FactorType.OrbRangeVsAtr, 1.0);
_factorWeights.Add(FactorType.GapDirectionAlignment, 1.0);
_factorWeights.Add(FactorType.BreakoutVolumeStrength, 1.0);
_factorWeights.Add(FactorType.PriorDayCloseStrength, 1.0);
}
/// <summary>

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using NT8.Core.Common.Models;
using NT8.Core.Logging;
namespace NT8.Core.Intelligence
{
@@ -398,4 +399,625 @@ namespace NT8.Core.Intelligence
return defaultValue;
}
}
/// <summary>
/// Daily bar data passed to ORB-specific factor calculators.
/// Contains a lookback window of recent daily bars in chronological order,
/// oldest first, with index [Count-1] being the most recent completed day.
/// </summary>
public struct DailyBarContext
{
/// <summary>Daily high prices, oldest first.</summary>
public double[] Highs;
/// <summary>Daily low prices, oldest first.</summary>
public double[] Lows;
/// <summary>Daily close prices, oldest first.</summary>
public double[] Closes;
/// <summary>Daily open prices, oldest first.</summary>
public double[] Opens;
/// <summary>Daily volume values, oldest first.</summary>
public long[] Volumes;
/// <summary>Number of valid bars populated.</summary>
public int Count;
/// <summary>Today's RTH open price.</summary>
public double TodayOpen;
/// <summary>Volume of the breakout bar (current intraday bar).</summary>
public double BreakoutBarVolume;
/// <summary>Average intraday volume per bar for today's session so far.</summary>
public double AvgIntradayBarVolume;
/// <summary>Trade direction: 1 for long, -1 for short.</summary>
public int TradeDirection;
}
/// <summary>
/// Scores the setup based on narrow range day concepts.
/// An NR7 (range is the narrowest of the last 7 days) scores highest,
/// indicating volatility contraction and likely expansion on breakout.
/// Requires at least 7 completed daily bars in DailyBarContext.
/// </summary>
public class NarrowRangeFactorCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the NarrowRangeFactorCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public NarrowRangeFactorCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.NarrowRange; }
}
/// <summary>
/// Calculates narrow range score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"]. Returns 0.3 if context is missing.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
{
double score = 0.3;
string reason = "No daily bar context available";
if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars"))
{
DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"];
if (daily.Count >= 7 && daily.Highs != null && daily.Lows != null)
{
double todayRange = daily.Highs[daily.Count - 1] - daily.Lows[daily.Count - 1];
bool isNR4 = true;
int start4 = daily.Count - 4;
int end = daily.Count - 2;
for (int i = start4; i <= end; i++)
{
double r = daily.Highs[i] - daily.Lows[i];
if (todayRange >= r)
{
isNR4 = false;
break;
}
}
bool isNR7 = true;
int start7 = daily.Count - 7;
for (int i = start7; i <= end; i++)
{
double r = daily.Highs[i] - daily.Lows[i];
if (todayRange >= r)
{
isNR7 = false;
break;
}
}
if (isNR7)
{
score = 1.0;
reason = "NR7: Narrowest range in 7 days — strong volatility contraction";
}
else if (isNR4)
{
score = 0.75;
reason = "NR4: Narrowest range in 4 days — moderate volatility contraction";
}
else
{
double sumRanges = 0.0;
int lookback = Math.Min(7, daily.Count - 1);
int start = daily.Count - 1 - lookback;
int finish = daily.Count - 2;
for (int i = start; i <= finish; i++)
sumRanges += daily.Highs[i] - daily.Lows[i];
double avgRange = lookback > 0 ? sumRanges / lookback : todayRange;
double ratio = avgRange > 0.0 ? todayRange / avgRange : 1.0;
if (ratio <= 0.7)
{
score = 0.6;
reason = "Range below 70% of avg — mild contraction";
}
else if (ratio <= 0.9)
{
score = 0.45;
reason = "Range near avg — no significant contraction";
}
else
{
score = 0.2;
reason = "Range above avg — expansion day, low NR score";
}
}
}
else
{
reason = String.Format("Insufficient daily bars: {0} of 7 required", daily.Count);
}
}
return new ConfluenceFactor(
FactorType.NarrowRange,
"Narrow Range (NR4/NR7)",
score,
0.20,
reason,
new Dictionary<string, object>());
}
}
/// <summary>
/// Scores the ORB range relative to average daily range.
/// Prevents trading when the ORB has already consumed most of the
/// day's expected range, leaving little room for continuation.
/// </summary>
public class OrbRangeVsAtrFactorCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the OrbRangeVsAtrFactorCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public OrbRangeVsAtrFactorCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.OrbRangeVsAtr; }
}
/// <summary>
/// Calculates ORB range vs ATR score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] and double in intent.Metadata["orb_range_ticks"].
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
{
double score = 0.5;
string reason = "No daily bar context available";
if (intent != null && intent.Metadata != null &&
intent.Metadata.ContainsKey("daily_bars") &&
intent.Metadata.ContainsKey("orb_range_ticks"))
{
DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"];
double orbRangeTicks = ToDouble(intent.Metadata["orb_range_ticks"], 0.0);
if (daily.Count >= 5 && daily.Highs != null && daily.Lows != null)
{
double sumAtr = 0.0;
int lookback = Math.Min(10, daily.Count - 1);
int start = daily.Count - 1 - lookback;
int end = daily.Count - 2;
for (int i = start; i <= end; i++)
sumAtr += daily.Highs[i] - daily.Lows[i];
double avgDailyRange = lookback > 0 ? sumAtr / lookback : 0.0;
double orbRangePoints = orbRangeTicks / 4.0;
double ratio = avgDailyRange > 0.0 ? orbRangePoints / avgDailyRange : 0.5;
if (ratio <= 0.20)
{
score = 1.0;
reason = String.Format("ORB is {0:P0} of daily ATR — tight range, high expansion potential", ratio);
}
else if (ratio <= 0.35)
{
score = 0.80;
reason = String.Format("ORB is {0:P0} of daily ATR — good room to run", ratio);
}
else if (ratio <= 0.50)
{
score = 0.60;
reason = String.Format("ORB is {0:P0} of daily ATR — moderate room remaining", ratio);
}
else if (ratio <= 0.70)
{
score = 0.35;
reason = String.Format("ORB is {0:P0} of daily ATR — limited room, caution", ratio);
}
else
{
score = 0.10;
reason = String.Format("ORB is {0:P0} of daily ATR — range nearly exhausted", ratio);
}
}
}
return new ConfluenceFactor(
FactorType.OrbRangeVsAtr,
"ORB Range vs ATR",
score,
0.15,
reason,
new Dictionary<string, object>());
}
private static double ToDouble(object value, double defaultValue)
{
if (value == null)
return defaultValue;
if (value is double)
return (double)value;
if (value is float)
return (double)(float)value;
if (value is int)
return (double)(int)value;
if (value is long)
return (double)(long)value;
return defaultValue;
}
}
/// <summary>
/// Scores alignment between today's overnight gap direction and the
/// trade direction. A gap-and-go setup (gap up + long trade) scores
/// highest. A gap fade setup penalizes the score.
/// </summary>
public class GapDirectionAlignmentCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the GapDirectionAlignmentCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public GapDirectionAlignmentCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.GapDirectionAlignment; }
}
/// <summary>
/// Calculates gap alignment score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] with TodayOpen and TradeDirection populated.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
{
double score = 0.5;
string reason = "No daily bar context available";
if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars"))
{
DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"];
if (daily.Count >= 2 && daily.Closes != null)
{
double prevClose = daily.Closes[daily.Count - 2];
double todayOpen = daily.TodayOpen;
double gapPoints = todayOpen - prevClose;
int gapDirection = gapPoints > 0.25 ? 1 : (gapPoints < -0.25 ? -1 : 0);
int tradeDir = daily.TradeDirection;
if (gapDirection == 0)
{
score = 0.55;
reason = "Flat open — no gap bias, neutral score";
}
else if (gapDirection == tradeDir)
{
double gapSize = Math.Abs(gapPoints);
if (gapSize >= 5.0)
{
score = 1.0;
reason = String.Format("Large gap {0:+0.00;-0.00} aligns with trade — strong gap-and-go", gapPoints);
}
else if (gapSize >= 2.0)
{
score = 0.85;
reason = String.Format("Moderate gap {0:+0.00;-0.00} aligns with trade", gapPoints);
}
else
{
score = 0.65;
reason = String.Format("Small gap {0:+0.00;-0.00} aligns with trade", gapPoints);
}
}
else
{
double gapSize = Math.Abs(gapPoints);
if (gapSize >= 5.0)
{
score = 0.10;
reason = String.Format("Large gap {0:+0.00;-0.00} opposes trade — high fade risk", gapPoints);
}
else if (gapSize >= 2.0)
{
score = 0.25;
reason = String.Format("Moderate gap {0:+0.00;-0.00} opposes trade", gapPoints);
}
else
{
score = 0.40;
reason = String.Format("Small gap {0:+0.00;-0.00} opposes trade — minor headwind", gapPoints);
}
}
}
}
return new ConfluenceFactor(
FactorType.GapDirectionAlignment,
"Gap Direction Alignment",
score,
0.15,
reason,
new Dictionary<string, object>());
}
}
/// <summary>
/// Scores the volume of the breakout bar relative to the average
/// volume of bars seen so far in today's session.
/// A volume surge on the breakout bar strongly confirms the move.
/// </summary>
public class BreakoutVolumeStrengthCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the BreakoutVolumeStrengthCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public BreakoutVolumeStrengthCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.BreakoutVolumeStrength; }
}
/// <summary>
/// Calculates breakout volume score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] with BreakoutBarVolume and
/// AvgIntradayBarVolume populated.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
{
double score = 0.4;
string reason = "No daily bar context available";
if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars"))
{
DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"];
double breakoutVol = daily.BreakoutBarVolume;
double avgVol = daily.AvgIntradayBarVolume;
if (avgVol > 0.0)
{
double ratio = breakoutVol / avgVol;
if (ratio >= 3.0)
{
score = 1.0;
reason = String.Format("Breakout volume {0:F1}x avg — exceptional surge", ratio);
}
else if (ratio >= 2.0)
{
score = 0.85;
reason = String.Format("Breakout volume {0:F1}x avg — strong confirmation", ratio);
}
else if (ratio >= 1.5)
{
score = 0.70;
reason = String.Format("Breakout volume {0:F1}x avg — solid confirmation", ratio);
}
else if (ratio >= 1.0)
{
score = 0.50;
reason = String.Format("Breakout volume {0:F1}x avg — average, neutral", ratio);
}
else if (ratio >= 0.7)
{
score = 0.25;
reason = String.Format("Breakout volume {0:F1}x avg — below avg, low conviction", ratio);
}
else
{
score = 0.10;
reason = String.Format("Breakout volume {0:F1}x avg — weak breakout, high false-break risk", ratio);
}
}
else
{
reason = "Avg intraday volume not available";
}
}
return new ConfluenceFactor(
FactorType.BreakoutVolumeStrength,
"Breakout Volume Strength",
score,
0.20,
reason,
new Dictionary<string, object>());
}
}
/// <summary>
/// Scores where the prior day closed within its own range.
/// A strong prior close (top 25% for longs, bottom 25% for shorts)
/// indicates momentum continuation into today's session.
/// </summary>
public class PriorDayCloseStrengthCalculator : IFactorCalculator
{
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the PriorDayCloseStrengthCalculator class.
/// </summary>
/// <param name="logger">Logger instance.</param>
public PriorDayCloseStrengthCalculator(ILogger logger)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
}
/// <summary>
/// Gets the factor type identifier.
/// </summary>
public FactorType Type
{
get { return FactorType.PriorDayCloseStrength; }
}
/// <summary>
/// Calculates prior close strength score. Expects DailyBarContext in
/// intent.Metadata["daily_bars"] with at least 2 completed bars and
/// TradeDirection populated.
/// </summary>
/// <param name="intent">Current strategy intent.</param>
/// <param name="context">Current strategy context.</param>
/// <param name="bar">Current bar data.</param>
/// <returns>Calculated confluence factor.</returns>
public ConfluenceFactor Calculate(StrategyIntent intent, StrategyContext context, BarData bar)
{
double score = 0.5;
string reason = "No daily bar context available";
if (intent != null && intent.Metadata != null && intent.Metadata.ContainsKey("daily_bars"))
{
DailyBarContext daily = (DailyBarContext)intent.Metadata["daily_bars"];
if (daily.Count >= 2 && daily.Highs != null && daily.Lows != null && daily.Closes != null)
{
int prev = daily.Count - 2;
double prevHigh = daily.Highs[prev];
double prevLow = daily.Lows[prev];
double prevClose = daily.Closes[prev];
double prevRange = prevHigh - prevLow;
int tradeDir = daily.TradeDirection;
if (prevRange > 0.0)
{
double closePosition = (prevClose - prevLow) / prevRange;
if (tradeDir == 1)
{
if (closePosition >= 0.75)
{
score = 1.0;
reason = String.Format("Prior close in top {0:P0} — strong bullish close", 1.0 - closePosition);
}
else if (closePosition >= 0.50)
{
score = 0.70;
reason = "Prior close in upper half — moderate bullish bias";
}
else if (closePosition >= 0.25)
{
score = 0.40;
reason = "Prior close in lower half — weak prior close for long";
}
else
{
score = 0.15;
reason = "Prior close near low — bearish close, headwind for long";
}
}
else
{
if (closePosition <= 0.25)
{
score = 1.0;
reason = String.Format("Prior close in bottom {0:P0} — strong bearish close", closePosition);
}
else if (closePosition <= 0.50)
{
score = 0.70;
reason = "Prior close in lower half — moderate bearish bias";
}
else if (closePosition <= 0.75)
{
score = 0.40;
reason = "Prior close in upper half — weak prior close for short";
}
else
{
score = 0.15;
reason = "Prior close near high — bullish close, headwind for short";
}
}
}
else
{
reason = "Prior day range is zero — cannot score";
}
}
}
return new ConfluenceFactor(
FactorType.PriorDayCloseStrength,
"Prior Day Close Strength",
score,
0.15,
reason,
new Dictionary<string, object>());
}
}
}

View File

@@ -109,8 +109,8 @@ namespace NT8.Core.Intelligence
private void InitializeDefaults()
{
_minimumGradeByMode.Add(RiskMode.ECP, TradeGrade.B);
_minimumGradeByMode.Add(RiskMode.PCP, TradeGrade.C);
_minimumGradeByMode.Add(RiskMode.ECP, TradeGrade.C);
_minimumGradeByMode.Add(RiskMode.PCP, TradeGrade.B);
_minimumGradeByMode.Add(RiskMode.DCP, TradeGrade.A);
_minimumGradeByMode.Add(RiskMode.HR, TradeGrade.APlus);

View File

@@ -4,6 +4,11 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
// ARCHIVED: This namespace (NT8.Core.Orders) is superseded by NT8.Core.OMS.
// NT8.Core.OMS is the canonical order management implementation used by NT8StrategyBase.
// These files are retained for reference only and are not referenced by any active code.
// Do not add new code here. Do not remove these files until a full audit confirms zero references.
namespace NT8.Core.Orders
{
/// <summary>

View File

@@ -6,6 +6,11 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
// ARCHIVED: This namespace (NT8.Core.Orders) is superseded by NT8.Core.OMS.
// NT8.Core.OMS is the canonical order management implementation used by NT8StrategyBase.
// These files are retained for reference only and are not referenced by any active code.
// Do not add new code here. Do not remove these files until a full audit confirms zero references.
namespace NT8.Core.Orders
{
/// <summary>

View File

@@ -2,6 +2,11 @@ using NT8.Core.Common.Models;
using System;
using System.Collections.Generic;
// ARCHIVED: This namespace (NT8.Core.Orders) is superseded by NT8.Core.OMS.
// NT8.Core.OMS is the canonical order management implementation used by NT8StrategyBase.
// These files are retained for reference only and are not referenced by any active code.
// Do not add new code here. Do not remove these files until a full audit confirms zero references.
namespace NT8.Core.Orders
{
#region Core Order Models

View File

@@ -20,6 +20,7 @@ namespace NT8.Core.Risk
private double _dailyPnL;
private double _maxDrawdown;
private bool _tradingHalted;
private double _configuredDailyLossLimit;
private DateTime _lastUpdate = DateTime.UtcNow;
private readonly Dictionary<string, double> _symbolExposure = new Dictionary<string, double>();
@@ -27,6 +28,15 @@ namespace NT8.Core.Risk
{
if (logger == null) throw new ArgumentNullException("logger");
_logger = logger;
_configuredDailyLossLimit = 1000.0;
}
public BasicRiskManager(ILogger logger, double dailyLossLimit)
{
if (logger == null) throw new ArgumentNullException("logger");
if (dailyLossLimit <= 0.0) throw new ArgumentException("dailyLossLimit must be positive", "dailyLossLimit");
_logger = logger;
_configuredDailyLossLimit = dailyLossLimit;
}
public RiskDecision ValidateOrder(StrategyIntent intent, StrategyContext context, RiskConfig config)
@@ -216,10 +226,8 @@ namespace NT8.Core.Risk
private void CheckEmergencyConditions(double dayPnL)
{
// Emergency halt if daily loss exceeds 90% of limit
// Using a default limit of 1000 as this method doesn't have access to config
// In Phase 1, this should be improved to use the actual config value
if (dayPnL <= -(1000 * 0.9) && !_tradingHalted)
// Emergency halt if daily loss exceeds 90% of configured limit
if (dayPnL <= -(_configuredDailyLossLimit * 0.9) && !_tradingHalted)
{
_tradingHalted = true;
_logger.LogCritical("Emergency halt triggered at 90% of daily loss limit: {0:C}", dayPnL);

View File

@@ -0,0 +1,276 @@
// File: PortfolioRiskManager.cs
using System;
using System.Collections.Generic;
using NT8.Core.Common.Models;
using NT8.Core.Logging;
namespace NT8.Core.Risk
{
/// <summary>
/// Portfolio-level risk coordinator. Singleton. Enforces cross-strategy
/// daily loss limits, maximum open contract caps, and a portfolio kill switch.
/// Must be registered by each strategy on init and unregistered on terminate.
/// Thread-safe via a single lock object.
/// </summary>
public class PortfolioRiskManager
{
private static readonly object _instanceLock = new object();
private static PortfolioRiskManager _instance;
/// <summary>
/// Gets the singleton instance of PortfolioRiskManager.
/// </summary>
public static PortfolioRiskManager Instance
{
get
{
if (_instance == null)
{
lock (_instanceLock)
{
if (_instance == null)
_instance = new PortfolioRiskManager();
}
}
return _instance;
}
}
private readonly object _lock = new object();
private readonly Dictionary<string, RiskConfig> _registeredStrategies;
private readonly Dictionary<string, double> _strategyPnL;
private readonly Dictionary<string, int> _strategyOpenContracts;
/// <summary>
/// Maximum combined daily loss across all registered strategies before all trading halts.
/// Default: 2000.0
/// </summary>
public double PortfolioDailyLossLimit { get; set; }
/// <summary>
/// Maximum total open contracts across all registered strategies simultaneously.
/// Default: 6
/// </summary>
public int MaxTotalOpenContracts { get; set; }
/// <summary>
/// When true, all new orders across all strategies are blocked immediately.
/// Set to true to perform an emergency halt of the entire portfolio.
/// </summary>
public bool PortfolioKillSwitch { get; set; }
private PortfolioRiskManager()
{
_registeredStrategies = new Dictionary<string, RiskConfig>();
_strategyPnL = new Dictionary<string, double>();
_strategyOpenContracts = new Dictionary<string, int>();
PortfolioDailyLossLimit = 2000.0;
MaxTotalOpenContracts = 6;
PortfolioKillSwitch = false;
}
/// <summary>
/// Registers a strategy with the portfolio manager. Called from
/// NT8StrategyBase.InitializeSdkComponents() during State.DataLoaded.
/// </summary>
/// <param name="strategyId">Unique strategy identifier (use Name from NT8StrategyBase).</param>
/// <param name="config">The strategy's risk configuration.</param>
/// <exception cref="ArgumentNullException">strategyId or config is null.</exception>
public void RegisterStrategy(string strategyId, RiskConfig config)
{
if (string.IsNullOrEmpty(strategyId)) throw new ArgumentNullException("strategyId");
if (config == null) throw new ArgumentNullException("config");
lock (_lock)
{
_registeredStrategies[strategyId] = config;
if (!_strategyPnL.ContainsKey(strategyId))
_strategyPnL[strategyId] = 0.0;
if (!_strategyOpenContracts.ContainsKey(strategyId))
_strategyOpenContracts[strategyId] = 0;
}
}
/// <summary>
/// Unregisters a strategy. Called from NT8StrategyBase during State.Terminated.
/// </summary>
/// <param name="strategyId">Strategy identifier to unregister.</param>
public void UnregisterStrategy(string strategyId)
{
if (string.IsNullOrEmpty(strategyId)) return;
lock (_lock)
{
_registeredStrategies.Remove(strategyId);
_strategyPnL.Remove(strategyId);
_strategyOpenContracts.Remove(strategyId);
}
}
/// <summary>
/// Validates a new order intent against portfolio-level risk limits.
/// Called before per-strategy risk validation in ProcessStrategyIntent().
/// </summary>
/// <param name="strategyId">The strategy requesting the order.</param>
/// <param name="intent">The trade intent to validate.</param>
/// <returns>RiskDecision indicating whether the order is allowed.</returns>
public RiskDecision ValidatePortfolioRisk(string strategyId, StrategyIntent intent)
{
if (string.IsNullOrEmpty(strategyId)) throw new ArgumentNullException("strategyId");
if (intent == null) throw new ArgumentNullException("intent");
lock (_lock)
{
// Kill switch — blocks everything immediately
if (PortfolioKillSwitch)
{
var ksMetrics = new Dictionary<string, object>();
ksMetrics.Add("kill_switch", true);
return new RiskDecision(
allow: false,
rejectReason: "Portfolio kill switch is active — all trading halted",
modifiedIntent: null,
riskLevel: RiskLevel.Critical,
riskMetrics: ksMetrics);
}
// Portfolio daily loss limit
double totalPnL = 0.0;
foreach (var kvp in _strategyPnL)
totalPnL += kvp.Value;
if (totalPnL <= -PortfolioDailyLossLimit)
{
var pnlMetrics = new Dictionary<string, object>();
pnlMetrics.Add("portfolio_pnl", totalPnL);
pnlMetrics.Add("limit", PortfolioDailyLossLimit);
return new RiskDecision(
allow: false,
rejectReason: String.Format(
"Portfolio daily loss limit breached: {0:C} <= -{1:C}",
totalPnL, PortfolioDailyLossLimit),
modifiedIntent: null,
riskLevel: RiskLevel.Critical,
riskMetrics: pnlMetrics);
}
// Total open contract cap
int totalContracts = 0;
foreach (var kvp in _strategyOpenContracts)
totalContracts += kvp.Value;
if (totalContracts >= MaxTotalOpenContracts)
{
var contractMetrics = new Dictionary<string, object>();
contractMetrics.Add("total_contracts", totalContracts);
contractMetrics.Add("limit", MaxTotalOpenContracts);
return new RiskDecision(
allow: false,
rejectReason: String.Format(
"Portfolio contract cap reached: {0} >= {1}",
totalContracts, MaxTotalOpenContracts),
modifiedIntent: null,
riskLevel: RiskLevel.High,
riskMetrics: contractMetrics);
}
// All portfolio checks passed
var okMetrics = new Dictionary<string, object>();
okMetrics.Add("portfolio_pnl", totalPnL);
okMetrics.Add("total_contracts", totalContracts);
return new RiskDecision(
allow: true,
rejectReason: null,
modifiedIntent: null,
riskLevel: RiskLevel.Low,
riskMetrics: okMetrics);
}
}
/// <summary>
/// Reports a fill to the portfolio manager.
/// Contract tracking is handled by UpdateOpenContracts().
/// </summary>
/// <param name="strategyId">Strategy that received the fill.</param>
/// <param name="fill">Fill details.</param>
public void ReportFill(string strategyId, OrderFill fill)
{
// Contract tracking is now handled by UpdateOpenContracts() called
// from OnBarUpdate with the actual position size.
// This method is retained for API compatibility and future P&L attribution use.
if (string.IsNullOrEmpty(strategyId) || fill == null) return;
}
/// <summary>
/// Updates the open contract count for a strategy to the actual current
/// position size. Called from NT8StrategyBase on each bar close.
/// This replaces fill-based inference with authoritative position data.
/// </summary>
/// <param name="strategyId">Strategy identifier.</param>
/// <param name="openContracts">Actual number of open contracts (0 when flat).</param>
public void UpdateOpenContracts(string strategyId, int openContracts)
{
if (string.IsNullOrEmpty(strategyId)) return;
lock (_lock)
{
if (openContracts < 0) openContracts = 0;
_strategyOpenContracts[strategyId] = openContracts;
}
}
/// <summary>
/// Reports a P&L update for a strategy. Called from NT8StrategyBase
/// whenever the strategy's realized P&L changes (typically on position close).
/// </summary>
/// <param name="strategyId">Strategy reporting P&L.</param>
/// <param name="pnl">Current cumulative day P&L for this strategy.</param>
public void ReportPnL(string strategyId, double pnl)
{
if (string.IsNullOrEmpty(strategyId)) return;
lock (_lock)
{
_strategyPnL[strategyId] = pnl;
}
}
/// <summary>
/// Resets daily P&L accumulators for all strategies. Does not clear registrations
/// or open contract counts. Typically called at the start of a new trading day.
/// </summary>
public void ResetDaily()
{
lock (_lock)
{
var keys = new List<string>(_strategyPnL.Keys);
foreach (var key in keys)
_strategyPnL[key] = 0.0;
}
}
/// <summary>
/// Returns a snapshot of current portfolio state for diagnostics.
/// </summary>
public string GetStatusSnapshot()
{
lock (_lock)
{
double totalPnL = 0.0;
foreach (var kvp in _strategyPnL)
totalPnL += kvp.Value;
int totalContracts = 0;
foreach (var kvp in _strategyOpenContracts)
totalContracts += kvp.Value;
return String.Format(
"Portfolio: strategies={0} totalPnL={1:C} totalContracts={2} killSwitch={3}",
_registeredStrategies.Count,
totalPnL,
totalContracts,
PortfolioKillSwitch);
}
}
}
}

View File

@@ -112,6 +112,11 @@ namespace NT8.Strategies.Examples
_factorCalculators.Add(new VolatilityRegimeFactorCalculator());
_factorCalculators.Add(new TimeInSessionFactorCalculator());
_factorCalculators.Add(new ExecutionQualityFactorCalculator());
_factorCalculators.Add(new NarrowRangeFactorCalculator(_logger));
_factorCalculators.Add(new OrbRangeVsAtrFactorCalculator(_logger));
_factorCalculators.Add(new GapDirectionAlignmentCalculator(_logger));
_factorCalculators.Add(new BreakoutVolumeStrengthCalculator(_logger));
_factorCalculators.Add(new PriorDayCloseStrengthCalculator(_logger));
_logger.LogInformation(
"SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}",
@@ -148,9 +153,25 @@ namespace NT8.Strategies.Examples
UpdateRiskMode(context);
UpdateConfluenceInputs(bar, context);
if (_currentSessionDate != context.CurrentTime.Date)
DateTime thisSessionStart = context.Session != null
? context.Session.SessionStart
: context.CurrentTime.Date.AddHours(9.5);
// Guard: only reset when the TRADING DATE has changed, not just the session
// start timestamp. Using trading date prevents mid-session resets caused by
// _openingRangeStart holding a stale value from a previous day.
DateTime thisTradingDate = thisSessionStart.Date;
TimeSpan sessionStartTime = thisSessionStart.TimeOfDay;
bool isValidRthSessionStart = sessionStartTime >= new TimeSpan(8, 0, 0)
&& sessionStartTime <= new TimeSpan(10, 30, 0);
if (isValidRthSessionStart && thisTradingDate != _currentSessionDate)
{
ResetSession(context.Session != null ? context.Session.SessionStart : context.CurrentTime.Date);
ResetSession(thisSessionStart);
if (context.CustomData != null && context.CustomData.ContainsKey("session_open_price"))
context.CustomData.Remove("session_open_price");
}
// Only trade during RTH
@@ -171,8 +192,26 @@ namespace NT8.Strategies.Examples
return null;
}
if (_logger != null && _openingRangeReady)
{
_logger.LogDebug(
"ORB ready: High={0:F2} Low={1:F2} Range={2:F2} TradeTaken={3}",
_openingRangeHigh,
_openingRangeLow,
_openingRangeHigh - _openingRangeLow,
_tradeTaken);
}
if (_tradeTaken)
{
if (_logger != null)
_logger.LogDebug(
"SimpleORBStrategy skip: trade already taken for session {0:yyyy-MM-dd}; bar={1:yyyy-MM-dd HH:mm}; symbol={2}",
_currentSessionDate,
bar.Time,
context.Symbol);
return null;
}
var openingRange = _openingRangeHigh - _openingRangeLow;
var volatilityBuffer = openingRange * (_stdDevMultiplier - 1.0);
@@ -191,18 +230,51 @@ namespace NT8.Strategies.Examples
if (candidate == null)
return null;
AttachDailyBarContext(candidate, bar, context);
// Hard veto: Trend against trade direction is a disqualifying condition.
// AVWAP trend alignment is checked before confluence scoring to prevent
// high scores from other factors masking a directionally broken setup.
if (_config != null && _config.Parameters != null)
{
if (context.CustomData != null && context.CustomData.ContainsKey("avwap_slope"))
{
var slope = context.CustomData["avwap_slope"];
if (slope is double)
{
double slopeVal = (double)slope;
bool longTrade = candidate.Side == OrderSide.Buy;
bool trendAgainst = (longTrade && slopeVal < 0) || (!longTrade && slopeVal > 0);
if (trendAgainst)
{
if (_logger != null)
_logger.LogInformation("Trade vetoed: AVWAP slope {0:F4} against {1} direction", slopeVal, candidate.Side);
return null;
}
}
}
}
var score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators);
var mode = _riskModeManager.GetCurrentMode();
if (!_gradeFilter.ShouldAcceptTrade(score.Grade, mode))
int minGradeValue = 5;
if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("MinTradeGrade"))
{
var reason = _gradeFilter.GetRejectionReason(score.Grade, mode);
var mgv = _config.Parameters["MinTradeGrade"];
if (mgv is int)
minGradeValue = (int)mgv;
}
TradeGrade minGrade = (TradeGrade)minGradeValue;
if ((int)score.Grade < (int)minGrade)
{
if (_logger != null)
_logger.LogInformation(
"SimpleORBStrategy rejected intent for {0}: Grade={1}, Mode={2}, Reason={3}",
candidate.Symbol,
"SimpleORBStrategy filtered by grade: Score={0:F3} Grade={1} MinGrade={2}",
score.WeightedScore,
score.Grade,
mode,
reason);
minGrade);
return null;
}
@@ -212,7 +284,8 @@ namespace NT8.Strategies.Examples
candidate.Confidence = score.WeightedScore;
candidate.Reason = string.Format("{0}; grade={1}; mode={2}", candidate.Reason, score.Grade, mode);
candidate.Metadata["confluence_score"] = score.WeightedScore;
candidate.Metadata["confluence_score"] = score;
candidate.Metadata["confluence_weighted_score"] = score.WeightedScore;
candidate.Metadata["trade_grade"] = score.Grade.ToString();
candidate.Metadata["risk_mode"] = mode.ToString();
candidate.Metadata["grade_multiplier"] = gradeMultiplier;
@@ -221,6 +294,24 @@ namespace NT8.Strategies.Examples
_tradeTaken = true;
if (_logger != null)
_logger.LogDebug(
"SimpleORBStrategy flag set: tradeTaken={0} session={1:yyyy-MM-dd}; bar={2:yyyy-MM-dd HH:mm}; side={3}; symbol={4}",
_tradeTaken,
_currentSessionDate,
bar.Time,
candidate.Side,
candidate.Symbol);
if (_logger != null && score.Factors != null)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append("Factors: ");
foreach (ConfluenceFactor f in score.Factors)
sb.Append(string.Format("{0}={1:F2}({2}) ", f.Type, f.Score, f.Weight.ToString("F2")));
_logger.LogInformation("Confluence detail: {0}", sb.ToString().TrimEnd());
}
_logger.LogInformation(
"SimpleORBStrategy accepted intent for {0}: Side={1}, Grade={2}, Mode={3}, Score={4:F3}, Mult={5:F2}",
candidate.Symbol,
@@ -273,7 +364,27 @@ namespace NT8.Strategies.Examples
/// <param name="parameters">Parameter map.</param>
public void SetParameters(Dictionary<string, object> parameters)
{
// Constructor-bound parameters intentionally remain immutable for deterministic behavior.
if (parameters == null)
return;
// force_session_reset: clear _tradeTaken and ORB state so a fresh live session
// can trade even if historical replay set _tradeTaken before going realtime.
if (parameters.ContainsKey("force_session_reset"))
{
var val = parameters["force_session_reset"];
if (val is bool && (bool)val)
{
lock (_lock)
{
_tradeTaken = false;
_openingRangeReady = false;
_openingRangeHigh = Double.MinValue;
_openingRangeLow = Double.MaxValue;
if (_logger != null)
_logger.LogInformation("ForceSessionReset: _tradeTaken cleared, ORB state reset for live session");
}
}
}
}
private void EnsureInitialized()
@@ -299,21 +410,55 @@ namespace NT8.Strategies.Examples
var avwap = _avwapCalculator.GetCurrentValue();
var avwapSlope = _avwapCalculator.GetSlope(10);
var bars = new List<BarData>();
bars.Add(bar);
var valueArea = _volumeProfileAnalyzer.CalculateValueArea(bars);
if (context.CustomData == null)
context.CustomData = new Dictionary<string, object>();
// Use pre-calculated intraday average from daily bar context when available.
// Fall back to current bar volume only if daily context is not yet populated.
double avgVol = (double)bar.Volume;
double normalAtr = bar.High - bar.Low;
if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("daily_bars"))
{
var src = _config.Parameters["daily_bars"];
if (src is DailyBarContext)
{
DailyBarContext dc = (DailyBarContext)src;
if (dc.AvgIntradayBarVolume > 0.0)
avgVol = dc.AvgIntradayBarVolume;
// Use 10-day average daily range as the volatility baseline.
if (dc.Count >= 5 && dc.Highs != null && dc.Lows != null)
{
double sumRanges = 0.0;
int lookback = Math.Min(10, dc.Count - 1);
int start = dc.Count - 1 - lookback;
int end = dc.Count - 2;
for (int i = start; i <= end; i++)
sumRanges += dc.Highs[i] - dc.Lows[i];
if (lookback > 0)
normalAtr = sumRanges / lookback;
}
}
}
context.CustomData["current_bar"] = bar;
context.CustomData["avwap"] = avwap;
context.CustomData["avwap_slope"] = avwapSlope;
context.CustomData["trend_confirm"] = avwapSlope > 0.0 ? 1.0 : 0.0;
context.CustomData["current_atr"] = Math.Max(0.01, bar.High - bar.Low);
context.CustomData["normal_atr"] = Math.Max(0.01, valueArea.ValueAreaHigh - valueArea.ValueAreaLow);
context.CustomData["normal_atr"] = Math.Max(0.01, normalAtr);
context.CustomData["recent_execution_quality"] = 0.8;
context.CustomData["avg_volume"] = (double)bar.Volume;
context.CustomData["avg_volume"] = avgVol;
// Track the first bar open of the RTH session as the session open price.
// Only set once per session (when session_open_price is not yet in custom data).
if (!context.CustomData.ContainsKey("session_open_price"))
{
context.CustomData["session_open_price"] = bar.Open;
}
}
private void ResetSession(DateTime sessionStart)
@@ -325,6 +470,13 @@ namespace NT8.Strategies.Examples
_openingRangeLow = Double.MaxValue;
_openingRangeReady = false;
_tradeTaken = false;
if (_logger != null)
_logger.LogInformation(
"Session reset: Date={0:yyyy-MM-dd} ORB window={1:HH:mm}-{2:HH:mm}",
_currentSessionDate,
_openingRangeStart,
_openingRangeEnd);
}
private void UpdateOpeningRange(BarData bar)
@@ -341,7 +493,7 @@ namespace NT8.Strategies.Examples
var stopTicks = _config != null && _config.Parameters.ContainsKey("StopTicks")
? (int)_config.Parameters["StopTicks"]
: 8;
var targetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks")
int baseTargetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks")
? (int)_config.Parameters["TargetTicks"]
: 16;
@@ -349,10 +501,87 @@ namespace NT8.Strategies.Examples
metadata.Add("orb_high", _openingRangeHigh);
metadata.Add("orb_low", _openingRangeLow);
metadata.Add("orb_range", openingRange);
double tickSize = 0.25;
if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("TickSize"))
{
var tickValue = _config.Parameters["TickSize"];
if (tickValue is double)
tickSize = (double)tickValue;
else if (tickValue is decimal)
tickSize = (double)(decimal)tickValue;
else if (tickValue is float)
tickSize = (double)(float)tickValue;
}
if (tickSize <= 0.0)
tickSize = 0.25;
// Scale target dynamically based on ORB range vs average daily range.
// Tight ORBs (< 20% of daily ATR) leave the most room — extend target.
// Wide ORBs (> 50% of daily ATR) have consumed range — keep target tight.
// Requires daily bar context with at least 5 bars; falls back to base target otherwise.
int targetTicks = baseTargetTicks;
if (_config != null && _config.Parameters != null && _config.Parameters.ContainsKey("daily_bars"))
{
var dailySrc = _config.Parameters["daily_bars"];
if (dailySrc is DailyBarContext)
{
DailyBarContext dc = (DailyBarContext)dailySrc;
if (dc.Count >= 5 && dc.Highs != null && dc.Lows != null && tickSize > 0.0)
{
double sumAtr = 0.0;
int lookback = Math.Min(10, dc.Count - 1);
int start = dc.Count - 1 - lookback;
int end = dc.Count - 2;
for (int i = start; i <= end; i++)
sumAtr += dc.Highs[i] - dc.Lows[i];
double avgDailyRange = lookback > 0 ? sumAtr / lookback : 0.0;
double orbRangePoints = openingRange;
double ratio = avgDailyRange > 0.0 ? orbRangePoints / avgDailyRange : 0.5;
// Ratio tiers map to target multipliers:
// <= 0.20 (tight ORB) → 1.75x (28 ticks on 16-tick base)
// <= 0.30 → 1.50x (24 ticks)
// <= 0.45 → 1.25x (20 ticks)
// <= 0.60 → 1.00x (16 ticks — base, no change)
// > 0.60 (wide ORB) → 0.75x (12 ticks — tighten)
double multiplier;
if (ratio <= 0.20)
multiplier = 1.75;
else if (ratio <= 0.30)
multiplier = 1.50;
else if (ratio <= 0.45)
multiplier = 1.25;
else if (ratio <= 0.60)
multiplier = 1.00;
else
multiplier = 0.75;
targetTicks = (int)Math.Round(baseTargetTicks * multiplier);
// Enforce hard floor of stopTicks + 4 (minimum 1:1 R plus 4 ticks)
int minTarget = stopTicks + 4;
if (targetTicks < minTarget)
targetTicks = minTarget;
}
}
}
if (_logger != null && targetTicks != baseTargetTicks)
_logger.LogInformation(
"Dynamic target: base={0} adjusted={1} (ORB/ATR scaling)",
baseTargetTicks, targetTicks);
var orbRangeTicks = openingRange / tickSize;
metadata.Add("orb_range_ticks", orbRangeTicks);
metadata.Add("trigger_price", lastPrice);
metadata.Add("multiplier", _stdDevMultiplier);
metadata.Add("opening_range_start", _openingRangeStart);
metadata.Add("opening_range_end", _openingRangeEnd);
metadata.Add("base_target_ticks", baseTargetTicks);
metadata.Add("dynamic_target_ticks", targetTicks);
return new StrategyIntent(
symbol,
@@ -365,5 +594,49 @@ namespace NT8.Strategies.Examples
"ORB breakout signal",
metadata);
}
private void AttachDailyBarContext(StrategyIntent intent, BarData bar, StrategyContext context)
{
if (intent == null || intent.Metadata == null)
return;
if (_config == null || _config.Parameters == null || !_config.Parameters.ContainsKey("daily_bars"))
return;
var source = _config.Parameters["daily_bars"];
if (!(source is DailyBarContext))
return;
DailyBarContext baseContext = (DailyBarContext)source;
DailyBarContext daily = baseContext;
daily.TradeDirection = intent.Side == OrderSide.Buy ? 1 : -1;
daily.BreakoutBarVolume = (double)bar.Volume;
double todayOpen = bar.Open;
if (context != null && context.CustomData != null && context.CustomData.ContainsKey("session_open_price"))
{
object sop = context.CustomData["session_open_price"];
if (sop is double)
todayOpen = (double)sop;
}
daily.TodayOpen = todayOpen;
if (context != null && context.CustomData != null && context.CustomData.ContainsKey("avg_volume"))
{
var avg = context.CustomData["avg_volume"];
if (avg is double)
daily.AvgIntradayBarVolume = (double)avg;
else if (avg is float)
daily.AvgIntradayBarVolume = (double)(float)avg;
else if (avg is int)
daily.AvgIntradayBarVolume = (double)(int)avg;
else if (avg is long)
daily.AvgIntradayBarVolume = (double)(long)avg;
}
// orb_range_ticks is set in CreateIntent() and preserved here.
intent.Metadata["daily_bars"] = daily;
}
}
}

View File

@@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NT8.Core.Common.Models;
using NT8.Core.Intelligence;
using NT8.Core.Logging;
namespace NT8.Core.Tests.Intelligence
{
[TestClass]
public class OrbConfluenceFactorTests
{
[TestMethod]
public void NarrowRange_NR7_ScoresOne()
{
var calc = new NarrowRangeFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 10, 10, 10, 10, 10, 10, 5 });
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(1.0, result.Score, 0.000001);
}
[TestMethod]
public void NarrowRange_NR4_Scores075()
{
var calc = new NarrowRangeFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 5, 5, 5, 10, 9, 8, 7 });
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(0.75, result.Score, 0.000001);
}
[TestMethod]
public void NarrowRange_WideRange_ScoresLow()
{
var calc = new NarrowRangeFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 5, 5, 5, 5, 5, 5, 12 });
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.IsTrue(result.Score <= 0.3);
}
[TestMethod]
public void NarrowRange_MissingContext_DefaultsTo03()
{
var calc = new NarrowRangeFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(0.3, result.Score, 0.000001);
}
[TestMethod]
public void NarrowRange_InsufficientBars_DefaultsTo03()
{
var calc = new NarrowRangeFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
intent.Metadata["daily_bars"] = CreateDailyContext(new double[] { 8, 7, 6, 5 });
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(0.3, result.Score, 0.000001);
}
[TestMethod]
public void OrbRangeVsAtr_SmallRange_ScoresOne()
{
var calc = new OrbRangeVsAtrFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 10, 10, 10, 10, 10, 10, 10 });
intent.Metadata["daily_bars"] = daily;
intent.Metadata["orb_range_ticks"] = 8.0;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(1.0, result.Score, 0.000001);
}
[TestMethod]
public void OrbRangeVsAtr_LargeRange_ScoresVeryLow()
{
var calc = new OrbRangeVsAtrFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 10, 10, 10, 10, 10, 10, 10 });
intent.Metadata["daily_bars"] = daily;
intent.Metadata["orb_range_ticks"] = 40.0;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.IsTrue(result.Score <= 0.15);
}
[TestMethod]
public void OrbRangeVsAtr_MissingContext_DefaultsTo05()
{
var calc = new OrbRangeVsAtrFactorCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(0.5, result.Score, 0.000001);
}
[TestMethod]
public void GapDirection_LargeAlignedGap_ScoresOne()
{
var calc = new GapDirectionAlignmentCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
daily.Closes[daily.Count - 2] = 100.0;
daily.TodayOpen = 106.0;
daily.TradeDirection = 1;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(1.0, result.Score, 0.000001);
}
[TestMethod]
public void GapDirection_LargeOpposingGap_ScoresVeryLow()
{
var calc = new GapDirectionAlignmentCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
daily.Closes[daily.Count - 2] = 100.0;
daily.TodayOpen = 106.0;
daily.TradeDirection = -1;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.IsTrue(result.Score <= 0.15);
}
[TestMethod]
public void GapDirection_FlatOpen_ScoresNeutral()
{
var calc = new GapDirectionAlignmentCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
daily.Closes[daily.Count - 2] = 100.0;
daily.TodayOpen = 100.1;
daily.TradeDirection = 1;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(0.55, result.Score, 0.000001);
}
[TestMethod]
public void BreakoutVolume_ThreeX_ScoresOne()
{
var calc = new BreakoutVolumeStrengthCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
daily.BreakoutBarVolume = 3000.0;
daily.AvgIntradayBarVolume = 1000.0;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(1.0, result.Score, 0.000001);
}
[TestMethod]
public void BreakoutVolume_BelowAverage_ScoresLow()
{
var calc = new BreakoutVolumeStrengthCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
daily.BreakoutBarVolume = 800.0;
daily.AvgIntradayBarVolume = 1200.0;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.IsTrue(result.Score <= 0.25);
}
[TestMethod]
public void PriorCloseStrength_LongTopQuartile_ScoresOne()
{
var calc = new PriorDayCloseStrengthCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
int prev = daily.Count - 2;
daily.Lows[prev] = 100.0;
daily.Highs[prev] = 120.0;
daily.Closes[prev] = 118.0;
daily.TradeDirection = 1;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(1.0, result.Score, 0.000001);
}
[TestMethod]
public void PriorCloseStrength_LongBottomQuartile_ScoresLow()
{
var calc = new PriorDayCloseStrengthCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
int prev = daily.Count - 2;
daily.Lows[prev] = 100.0;
daily.Highs[prev] = 120.0;
daily.Closes[prev] = 101.0;
daily.TradeDirection = 1;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.IsTrue(result.Score <= 0.20);
}
[TestMethod]
public void PriorCloseStrength_ShortBottomQuartile_ScoresOne()
{
var calc = new PriorDayCloseStrengthCalculator(new BasicLogger("test"));
var intent = CreateIntent();
var daily = CreateDailyContext(new double[] { 8, 8, 8, 8, 8, 8, 8 });
int prev = daily.Count - 2;
daily.Lows[prev] = 100.0;
daily.Highs[prev] = 120.0;
daily.Closes[prev] = 101.0;
daily.TradeDirection = -1;
intent.Metadata["daily_bars"] = daily;
var result = calc.Calculate(intent, CreateContext(), CreateBar());
Assert.AreEqual(1.0, result.Score, 0.000001);
}
private static StrategyIntent CreateIntent()
{
return new StrategyIntent(
"ES",
OrderSide.Buy,
OrderType.Market,
null,
8,
16,
0.8,
"test",
new Dictionary<string, object>());
}
private static StrategyContext CreateContext()
{
return new StrategyContext(
"ES",
DateTime.UtcNow,
new Position("ES", 0, 0, 0, 0, DateTime.UtcNow),
new AccountInfo(100000, 100000, 0, 0, DateTime.UtcNow),
new MarketSession(DateTime.Today.AddHours(9.5), DateTime.Today.AddHours(16), true, "RTH"),
new Dictionary<string, object>());
}
private static BarData CreateBar()
{
return new BarData("ES", DateTime.UtcNow, 5000, 5005, 4998, 5002, 1000, TimeSpan.FromMinutes(5));
}
private static DailyBarContext CreateDailyContext(double[] ranges)
{
DailyBarContext context = new DailyBarContext();
context.Count = ranges.Length;
context.Highs = new double[ranges.Length];
context.Lows = new double[ranges.Length];
context.Closes = new double[ranges.Length];
context.Opens = new double[ranges.Length];
context.Volumes = new long[ranges.Length];
for (int i = 0; i < ranges.Length; i++)
{
context.Lows[i] = 100.0;
context.Highs[i] = 100.0 + ranges[i];
context.Opens[i] = 100.0 + (ranges[i] * 0.25);
context.Closes[i] = 100.0 + (ranges[i] * 0.75);
context.Volumes[i] = 100000;
}
context.TodayOpen = context.Closes[Math.Max(0, context.Count - 2)] + 1.0;
context.BreakoutBarVolume = 1000.0;
context.AvgIntradayBarVolume = 1000.0;
context.TradeDirection = 1;
return context;
}
}
}

View File

@@ -0,0 +1,134 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NT8.Core.Common.Models;
using NT8.Core.Risk;
namespace NT8.Core.Tests.Risk
{
[TestClass]
public class PortfolioRiskManagerTests
{
private PortfolioRiskManager _manager;
[TestInitialize]
public void TestInitialize()
{
_manager = PortfolioRiskManager.Instance;
}
[TestCleanup]
public void TestCleanup()
{
_manager.UnregisterStrategy("strat1");
_manager.UnregisterStrategy("strat2");
_manager.UnregisterStrategy("strat3");
_manager.UnregisterStrategy("strat4");
_manager.UnregisterStrategy("strat5");
_manager.PortfolioKillSwitch = false;
_manager.PortfolioDailyLossLimit = 2000.0;
_manager.MaxTotalOpenContracts = 6;
_manager.ResetDaily();
}
[TestMethod]
public void PortfolioDailyLossLimit_WhenBreached_BlocksNewOrder()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.PortfolioDailyLossLimit = 500;
_manager.ReportPnL("strat1", -501);
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(decision.Allow);
}
[TestMethod]
public void MaxTotalOpenContracts_WhenAtCap_BlocksNewOrder()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.MaxTotalOpenContracts = 2;
_manager.UpdateOpenContracts("strat1", 2);
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(decision.Allow);
}
[TestMethod]
public void UpdateOpenContracts_WhenPositionCloses_UnblocksTrading()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.MaxTotalOpenContracts = 6;
var intent = TestDataBuilder.CreateValidIntent();
// Act
_manager.UpdateOpenContracts("strat1", 6);
var blocked = _manager.ValidatePortfolioRisk("strat1", intent);
_manager.UpdateOpenContracts("strat1", 0);
var unblocked = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(blocked.Allow);
Assert.IsTrue(unblocked.Allow);
}
[TestMethod]
public void PortfolioKillSwitch_WhenTrue_BlocksAllOrders()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.PortfolioKillSwitch = true;
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(decision.Allow);
Assert.IsTrue(decision.RejectReason.ToLowerInvariant().Contains("kill switch"));
}
[TestMethod]
public void ValidatePortfolioRisk_WhenWithinLimits_Passes()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
var intent = TestDataBuilder.CreateValidIntent();
// Act
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsTrue(decision.Allow);
}
[TestMethod]
public void ResetDaily_ClearsPnL_UnblocksTrading()
{
// Arrange
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
_manager.PortfolioDailyLossLimit = 500;
_manager.ReportPnL("strat1", -600);
var intent = TestDataBuilder.CreateValidIntent();
// Act
var blocked = _manager.ValidatePortfolioRisk("strat1", intent);
_manager.ResetDaily();
var unblocked = _manager.ValidatePortfolioRisk("strat1", intent);
// Assert
Assert.IsFalse(blocked.Allow);
Assert.IsTrue(unblocked.Allow);
}
}
}

View File

@@ -149,9 +149,9 @@ namespace NT8.Core.Tests.Sizing
var intent = CreateIntent();
var context = CreateContext();
var confluence = CreateScore(TradeGrade.C, 0.61);
var confluence = CreateScore(TradeGrade.B, 0.71);
var config = new SizingConfig(SizingMethod.FixedDollarRisk, 2, 10, 500.0, new Dictionary<string, object>());
var modeConfig = CreateModeConfig(RiskMode.PCP, 1.0, TradeGrade.C);
var modeConfig = CreateModeConfig(RiskMode.PCP, 1.0, TradeGrade.B);
var result = sizer.CalculateGradeBasedSize(
intent,

View File

@@ -15,11 +15,20 @@ namespace NT8.Integration.Tests
[TestClass]
public class NT8OrderAdapterIntegrationTests
{
private class FakeBridge : INT8ExecutionBridge
{
public void EnterLongManaged(int q, string n, int s, int t, double ts) { }
public void EnterShortManaged(int q, string n, int s, int t, double ts) { }
public void ExitLongManaged(string n) { }
public void ExitShortManaged(string n) { }
public void FlattenAll() { }
}
[TestMethod]
public void Initialize_NullRiskManager_ThrowsArgumentNullException()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
var sizer = new TestPositionSizer(1);
// Act / Assert
@@ -31,7 +40,7 @@ namespace NT8.Integration.Tests
public void Initialize_NullPositionSizer_ThrowsArgumentNullException()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(true);
// Act / Assert
@@ -43,7 +52,7 @@ namespace NT8.Integration.Tests
public void ExecuteIntent_NotInitialized_ThrowsInvalidOperationException()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert
Assert.ThrowsException<InvalidOperationException>(
@@ -54,7 +63,7 @@ namespace NT8.Integration.Tests
public void ExecuteIntent_RiskRejected_DoesNotRecordExecution()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(false);
var sizer = new TestPositionSizer(3);
adapter.Initialize(risk, sizer);
@@ -71,7 +80,7 @@ namespace NT8.Integration.Tests
public void ExecuteIntent_AllowedAndSized_RecordsExecution()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(true);
var sizer = new TestPositionSizer(4);
adapter.Initialize(risk, sizer);
@@ -94,7 +103,7 @@ namespace NT8.Integration.Tests
public void GetExecutionHistory_ReturnsCopy_NotMutableInternalReference()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
var risk = new TestRiskManager(true);
var sizer = new TestPositionSizer(2);
adapter.Initialize(risk, sizer);
@@ -113,7 +122,7 @@ namespace NT8.Integration.Tests
public void OnOrderUpdate_EmptyOrderId_ThrowsArgumentException()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert
Assert.ThrowsException<ArgumentException>(
@@ -124,7 +133,7 @@ namespace NT8.Integration.Tests
public void OnExecutionUpdate_EmptyExecutionId_ThrowsArgumentException()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert
Assert.ThrowsException<ArgumentException>(
@@ -135,7 +144,7 @@ namespace NT8.Integration.Tests
public void OnExecutionUpdate_EmptyOrderId_ThrowsArgumentException()
{
// Arrange
var adapter = new NT8OrderAdapter();
var adapter = new NT8OrderAdapter(new FakeBridge());
// Act / Assert
Assert.ThrowsException<ArgumentException>(