Compare commits
15 Commits
ee4da1b607
...
cleanup/ar
| Author | SHA1 | Date | |
|---|---|---|---|
| 2be9c843e5 | |||
| a6ececaf73 | |||
| ce74f68e54 | |||
| 3ccd3a8bfd | |||
| e2ea45b58f | |||
| 9a28a49292 | |||
| d856f3949d | |||
| c9e8b35f15 | |||
| ae8ac05017 | |||
| 229f4e413e | |||
| c094e65b10 | |||
| a2af272d73 | |||
| 2f623dc2f8 | |||
| 3282254572 | |||
| 498f298975 |
BIN
.gitattributes
vendored
Normal file
BIN
.gitattributes
vendored
Normal file
Binary file not shown.
@@ -1,9 +1,66 @@
|
|||||||
# Coding Patterns — NT8 SDK Required Patterns
|
# Coding Patterns — NT8 SDK Required Patterns
|
||||||
|
**Last Updated:** 2026-03-27
|
||||||
|
|
||||||
All code in the NT8 SDK MUST follow these patterns without exception.
|
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
|
## 1. Thread Safety — Lock Everything Shared
|
||||||
|
|
||||||
Every class with shared state must have a lock object:
|
Every class with shared state must have a lock object:
|
||||||
|
|||||||
@@ -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 `=>` — if on a property or method, rewrite as full block
|
||||||
- Search for `nameof` — replace with string literal
|
- Search for `nameof` — replace with string literal
|
||||||
- Search for `out var` — split into declaration + assignment
|
- 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.
|
||||||
|
|||||||
@@ -1,7 +1,108 @@
|
|||||||
# NT8 Institutional SDK - Development Workflow
|
# NT8-SDK — Kilocode Development Workflow
|
||||||
|
**Last Updated:** 2026-03-27
|
||||||
|
|
||||||
## Overview
|
This is the authoritative workflow for all development work on NT8-SDK using Kilocode.
|
||||||
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.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
## Archon Workflow Principles
|
||||||
|
|
||||||
|
|||||||
@@ -137,3 +137,126 @@ Compile Checklist (Preflight)
|
|||||||
""").format(date=datetime.date.today().isoformat())
|
""").format(date=datetime.date.today().isoformat())
|
||||||
|
|
||||||
files["NT8_Templates.md"] = textwrap.dedent("""
|
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.
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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 | Jan–Mar 2026 | 20 | 75% | **7.00** | $1,200 | trail=20 ✅ Production config |
|
||||||
|
| 2026-03-27 | Jan–Mar 2026 | 40 | 75% | 3.69 | $1,075 | trail=12 |
|
||||||
|
| 2026-03-27 | Mar 2025–Mar 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
|
**Date:** February 17, 2026
|
||||||
**Status:** Post Phase A-B-C NT8 Integration
|
**Status:** Post Phase A-B-C NT8 Integration
|
||||||
|
|||||||
@@ -1,9 +1,140 @@
|
|||||||
# NT8 SDK Project - Comprehensive Recap & Handover
|
> ⚠️ HISTORICAL — see docs/00-governance/ for current state
|
||||||
|
|
||||||
**Document Version:** 2.0
|
This file may contain outdated or mixed historical information.
|
||||||
**Date:** February 16, 2026
|
Canonical current-state documentation lives in docs/00-governance/.
|
||||||
**Current Phase:** Phase 5 Complete
|
This file is retained for history/reference only.
|
||||||
**Project Completion:** ~85%
|
|
||||||
|
# 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:30–10: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 2–20 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 | Jan–Mar 2026 | 20 | 75% | **7.00** | $1,200 | 1 ct, trail=20 ✅ Best |
|
||||||
|
| 2026-03-27 | Jan–Mar 2026 | 40 | 75% | 3.69 | $1,075 | 1 ct, trail=12 |
|
||||||
|
| 2026-03-27 | Mar 2025–Mar 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`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -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
|
# NT8 Institutional Trading SDK
|
||||||
|
|
||||||
**Version:** 0.2.0
|
**Version:** 0.2.0
|
||||||
|
|||||||
130
cleanup-repo.ps1
Normal file
130
cleanup-repo.ps1
Normal 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
|
||||||
@@ -173,7 +173,7 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $SkipVerification) {
|
if (-not $SkipVerification) {
|
||||||
Write-Step "6/6" "Verifying deployment"
|
Write-Step "6/7" "Verifying deployment"
|
||||||
$ok = $true
|
$ok = $true
|
||||||
|
|
||||||
if (-not (Test-Path "$nt8Custom\NT8.Core.dll")) {
|
if (-not (Test-Path "$nt8Custom\NT8.Core.dll")) {
|
||||||
@@ -195,13 +195,43 @@ if (-not $SkipVerification) {
|
|||||||
Write-Success "Deployment verification passed"
|
Write-Success "Deployment verification passed"
|
||||||
}
|
}
|
||||||
else {
|
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
|
$duration = (Get-Date) - $startTime
|
||||||
Write-Header "Deployment Complete"
|
Write-Header "Deployment Complete"
|
||||||
Write-Host ("Duration: {0:F1} seconds" -f $duration.TotalSeconds)
|
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
|
exit 0
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ set "NT8_CUSTOM=%USERPROFILE%\Documents\NinjaTrader 8\bin\Custom"
|
|||||||
set "NT8_STRATEGIES=%NT8_CUSTOM%\Strategies"
|
set "NT8_STRATEGIES=%NT8_CUSTOM%\Strategies"
|
||||||
set "CORE_BIN=%PROJECT_ROOT%\src\NT8.Core\bin\Release\net48"
|
set "CORE_BIN=%PROJECT_ROOT%\src\NT8.Core\bin\Release\net48"
|
||||||
set "ADAPTERS_BIN=%PROJECT_ROOT%\src\NT8.Adapters\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 "WRAPPERS_SRC=%PROJECT_ROOT%\src\NT8.Adapters\Wrappers"
|
||||||
set "BACKUP_ROOT=%SCRIPT_DIR%backups"
|
set "BACKUP_ROOT=%SCRIPT_DIR%backups"
|
||||||
|
|
||||||
@@ -40,6 +41,13 @@ if not exist "%ADAPTERS_BIN%\NT8.Adapters.dll" (
|
|||||||
exit /b 1
|
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%" (
|
if not exist "%NT8_STRATEGIES%" (
|
||||||
mkdir "%NT8_STRATEGIES%"
|
mkdir "%NT8_STRATEGIES%"
|
||||||
)
|
)
|
||||||
@@ -52,6 +60,7 @@ mkdir "%BACKUP_ROOT%\%STAMP%" >nul 2>&1
|
|||||||
echo Backing up existing NT8 SDK files...
|
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.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.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%\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
|
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 Timestamp: %STAMP%>> "%MANIFEST_FILE%"
|
||||||
echo Source Core DLL: %CORE_BIN%\NT8.Core.dll>> "%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 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 Custom Folder: %NT8_CUSTOM%>> "%MANIFEST_FILE%"
|
||||||
echo Destination Strategies Folder: %NT8_STRATEGIES%>> "%MANIFEST_FILE%"
|
echo Destination Strategies Folder: %NT8_STRATEGIES%>> "%MANIFEST_FILE%"
|
||||||
|
|
||||||
@@ -75,6 +85,12 @@ if errorlevel 1 (
|
|||||||
exit /b 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...
|
echo Deploying wrapper sources...
|
||||||
copy /Y "%WRAPPERS_SRC%\BaseNT8StrategyWrapper.cs" "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" >nul
|
copy /Y "%WRAPPERS_SRC%\BaseNT8StrategyWrapper.cs" "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" >nul
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
@@ -112,6 +128,11 @@ if not exist "%NT8_CUSTOM%\NT8.Adapters.dll" (
|
|||||||
exit /b 1
|
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" (
|
if not exist "%NT8_STRATEGIES%\BaseNT8StrategyWrapper.cs" (
|
||||||
echo ERROR: Verification failed for BaseNT8StrategyWrapper.cs
|
echo ERROR: Verification failed for BaseNT8StrategyWrapper.cs
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -126,11 +147,32 @@ echo.
|
|||||||
echo Deployment complete.
|
echo Deployment complete.
|
||||||
echo Backup location: %BACKUP_DIR%
|
echo Backup location: %BACKUP_DIR%
|
||||||
echo Manifest file : %MANIFEST_FILE%
|
echo Manifest file : %MANIFEST_FILE%
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Next steps:
|
echo Invalidating NinjaScript compiled assembly...
|
||||||
echo 1. Open NinjaTrader 8.
|
set "COMPILED_DLL=%NT8_CUSTOM%\NinjaTrader.Custom.dll"
|
||||||
echo 2. Open NinjaScript Editor and press F5 (Compile).
|
set "COMPILED_PDB=%NT8_CUSTOM%\NinjaTrader.Custom.pdb"
|
||||||
echo 3. Verify strategies appear in the Strategies list.
|
|
||||||
|
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
|
exit /b 0
|
||||||
|
|
||||||
|
|||||||
22
docs/00-governance/CLEANUP_LOG.md
Normal file
22
docs/00-governance/CLEANUP_LOG.md
Normal 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.
|
||||||
17
docs/00-governance/active_work.md
Normal file
17
docs/00-governance/active_work.md
Normal 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.
|
||||||
28
docs/00-governance/architecture.md
Normal file
28
docs/00-governance/architecture.md
Normal 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.
|
||||||
0
docs/00-governance/common_failures.md
Normal file
0
docs/00-governance/common_failures.md
Normal file
0
docs/00-governance/compile_guardrails.md
Normal file
0
docs/00-governance/compile_guardrails.md
Normal file
20
docs/00-governance/current_status.md
Normal file
20
docs/00-governance/current_status.md
Normal 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.
|
||||||
0
docs/00-governance/decisions.md
Normal file
0
docs/00-governance/decisions.md
Normal file
20
docs/00-governance/executive_summary.md
Normal file
20
docs/00-governance/executive_summary.md
Normal 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.
|
||||||
0
docs/00-governance/patterns_and_antipatterns.md
Normal file
0
docs/00-governance/patterns_and_antipatterns.md
Normal file
21
docs/00-governance/roadmap.md
Normal file
21
docs/00-governance/roadmap.md
Normal 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.
|
||||||
@@ -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
|
# NT8 SDK - Documentation Index
|
||||||
|
|
||||||
**Complete documentation for the NT8 Institutional Trading SDK**
|
**Complete documentation for the NT8 Institutional Trading SDK**
|
||||||
|
|||||||
@@ -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
|
# NT8 Institutional Trading SDK
|
||||||
|
|
||||||
**Version:** 0.2.0
|
**Version:** 0.2.0
|
||||||
|
|||||||
@@ -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 | Mar–Sep 2025 train / Oct–Mar 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 | Jan–Mar 2026 | 20 | 75% | 7.00 | $1,200 | trail=20, grade=B ✅ Use this |
|
||||||
|
| 2026-03-27 | Jan–Mar 2026 | 40 | 75% | 3.69 | $1,075 | trail=12, grade=B |
|
||||||
|
| 2026-03-27 | Mar 2025–Mar 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
|
## Sprint Goals
|
||||||
1. Implement Order Management System (OMS) with smart order routing
|
1. Implement Order Management System (OMS) with smart order routing
|
||||||
|
|||||||
@@ -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
|
# Phase 2 Completion Report
|
||||||
|
|
||||||
**Project:** NT8 Institutional Trading SDK
|
**Project:** NT8 Institutional Trading SDK
|
||||||
26
src/NT8.Adapters/NinjaTrader/INT8ExecutionBridge.cs
Normal file
26
src/NT8.Adapters/NinjaTrader/INT8ExecutionBridge.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,11 +27,34 @@ namespace NT8.Adapters.NinjaTrader
|
|||||||
public NT8Adapter()
|
public NT8Adapter()
|
||||||
{
|
{
|
||||||
_dataAdapter = new NT8DataAdapter();
|
_dataAdapter = new NT8DataAdapter();
|
||||||
_orderAdapter = new NT8OrderAdapter();
|
_orderAdapter = new NT8OrderAdapter(new NullExecutionBridge());
|
||||||
_loggingAdapter = new NT8LoggingAdapter();
|
_loggingAdapter = new NT8LoggingAdapter();
|
||||||
_executionHistory = new List<NT8OrderExecutionRecord>();
|
_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>
|
/// <summary>
|
||||||
/// Initialize the adapter with required components
|
/// Initialize the adapter with required components
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace NT8.Adapters.NinjaTrader
|
|||||||
public class NT8OrderAdapter
|
public class NT8OrderAdapter
|
||||||
{
|
{
|
||||||
private readonly object _lock = new object();
|
private readonly object _lock = new object();
|
||||||
|
private readonly INT8ExecutionBridge _bridge;
|
||||||
private IRiskManager _riskManager;
|
private IRiskManager _riskManager;
|
||||||
private IPositionSizer _positionSizer;
|
private IPositionSizer _positionSizer;
|
||||||
private readonly List<NT8OrderExecutionRecord> _executionHistory;
|
private readonly List<NT8OrderExecutionRecord> _executionHistory;
|
||||||
@@ -19,8 +20,11 @@ namespace NT8.Adapters.NinjaTrader
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor for NT8OrderAdapter.
|
/// Constructor for NT8OrderAdapter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NT8OrderAdapter()
|
public NT8OrderAdapter(INT8ExecutionBridge bridge)
|
||||||
{
|
{
|
||||||
|
if (bridge == null)
|
||||||
|
throw new ArgumentNullException("bridge");
|
||||||
|
_bridge = bridge;
|
||||||
_executionHistory = new List<NT8OrderExecutionRecord>();
|
_executionHistory = new List<NT8OrderExecutionRecord>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,18 +131,30 @@ namespace NT8.Adapters.NinjaTrader
|
|||||||
private void ExecuteInNT8(StrategyIntent intent, SizingResult sizing)
|
private void ExecuteInNT8(StrategyIntent intent, SizingResult sizing)
|
||||||
{
|
{
|
||||||
if (intent == null)
|
if (intent == null)
|
||||||
{
|
|
||||||
throw new ArgumentNullException("intent");
|
throw new ArgumentNullException("intent");
|
||||||
}
|
|
||||||
|
|
||||||
if (sizing == null)
|
if (sizing == null)
|
||||||
{
|
|
||||||
throw new ArgumentNullException("sizing");
|
throw new ArgumentNullException("sizing");
|
||||||
}
|
|
||||||
|
|
||||||
// This is where the actual NT8 order execution would happen
|
var signalName = string.Format("SDK_{0}_{1}", intent.Symbol, intent.Side);
|
||||||
// In a real implementation, this would call NT8's EnterLong/EnterShort methods
|
|
||||||
// along with SetStopLoss, SetProfitTarget, etc.
|
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)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
@@ -151,28 +167,6 @@ namespace NT8.Adapters.NinjaTrader
|
|||||||
intent.TargetTicks,
|
intent.TargetTicks,
|
||||||
DateTime.UtcNow));
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for strategies that integrate NT8 SDK components.
|
/// Base class for strategies that integrate NT8 SDK components.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class NT8StrategyBase : Strategy
|
public abstract class NT8StrategyBase : Strategy, INT8ExecutionBridge
|
||||||
{
|
{
|
||||||
private readonly object _lock = new object();
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
@@ -53,7 +53,15 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
private int _ordersSubmittedToday;
|
private int _ordersSubmittedToday;
|
||||||
private DateTime _lastBarTime;
|
private DateTime _lastBarTime;
|
||||||
private bool _killSwitchTriggered;
|
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 ExecutionCircuitBreaker _circuitBreaker;
|
||||||
|
private System.IO.StreamWriter _fileLog;
|
||||||
|
private readonly object _fileLock = new object();
|
||||||
|
|
||||||
#region User-Configurable Properties
|
#region User-Configurable Properties
|
||||||
|
|
||||||
@@ -93,8 +101,97 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
|
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
|
||||||
public bool EnableVerboseLogging { get; set; }
|
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
|
#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>
|
/// <summary>
|
||||||
/// Create the SDK strategy instance.
|
/// Create the SDK strategy instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -112,7 +209,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
Description = "SDK-integrated strategy base";
|
Description = "SDK-integrated strategy base";
|
||||||
// Name intentionally not set - this is an abstract base class
|
// Name intentionally not set - this is an abstract base class
|
||||||
Calculate = Calculate.OnBarClose;
|
Calculate = Calculate.OnBarClose;
|
||||||
EntriesPerDirection = 1;
|
EntriesPerDirection = 2;
|
||||||
EntryHandling = EntryHandling.AllEntries;
|
EntryHandling = EntryHandling.AllEntries;
|
||||||
IsExitOnSessionCloseStrategy = true;
|
IsExitOnSessionCloseStrategy = true;
|
||||||
ExitOnSessionCloseSeconds = 30;
|
ExitOnSessionCloseSeconds = 30;
|
||||||
@@ -120,12 +217,12 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;
|
MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;
|
||||||
OrderFillResolution = OrderFillResolution.Standard;
|
OrderFillResolution = OrderFillResolution.Standard;
|
||||||
Slippage = 0;
|
Slippage = 0;
|
||||||
StartBehavior = StartBehavior.WaitUntilFlat;
|
StartBehavior = StartBehavior.AdoptAccountPosition;
|
||||||
TimeInForce = TimeInForce.Gtc;
|
TimeInForce = TimeInForce.Gtc;
|
||||||
TraceOrders = false;
|
TraceOrders = false;
|
||||||
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
|
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
|
||||||
StopTargetHandling = StopTargetHandling.PerEntryExecution;
|
StopTargetHandling = StopTargetHandling.PerEntryExecution;
|
||||||
BarsRequiredToTrade = 20;
|
BarsRequiredToTrade = 1;
|
||||||
|
|
||||||
EnableSDK = true;
|
EnableSDK = true;
|
||||||
DailyLossLimit = 1000.0;
|
DailyLossLimit = 1000.0;
|
||||||
@@ -133,10 +230,21 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
MaxOpenPositions = 3;
|
MaxOpenPositions = 3;
|
||||||
RiskPerTrade = 100.0;
|
RiskPerTrade = 100.0;
|
||||||
MinContracts = 1;
|
MinContracts = 1;
|
||||||
MaxContracts = 10;
|
MaxContracts = 3;
|
||||||
EnableKillSwitch = false;
|
EnableKillSwitch = false;
|
||||||
EnableVerboseLogging = 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;
|
_killSwitchTriggered = false;
|
||||||
|
_connectionLost = false;
|
||||||
}
|
}
|
||||||
else if (State == State.DataLoaded)
|
else if (State == State.DataLoaded)
|
||||||
{
|
{
|
||||||
@@ -144,9 +252,20 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
{
|
{
|
||||||
try
|
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();
|
InitializeSdkComponents();
|
||||||
_sdkInitialized = true;
|
_sdkInitialized = true;
|
||||||
Print(string.Format("[SDK] {0} initialized successfully", Name));
|
Print(string.Format("[SDK] {0} initialized successfully", Name));
|
||||||
|
WriteSettingsFile();
|
||||||
|
WriteSessionHeader();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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()
|
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 (EnableKillSwitch)
|
||||||
{
|
{
|
||||||
if (!_killSwitchTriggered)
|
if (!_killSwitchTriggered)
|
||||||
@@ -177,28 +375,44 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
|
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_sdkInitialized || _sdkStrategy == null)
|
// Connection loss guard — do not submit new orders if broker is disconnected
|
||||||
|
if (_connectionLost)
|
||||||
{
|
{
|
||||||
if (CurrentBar == 0)
|
if (EnableVerboseLogging)
|
||||||
Print(string.Format("[SDK] Not initialized: sdkInit={0}, strategy={1}", _sdkInitialized, _sdkStrategy != null));
|
Print(string.Format("[NT8-SDK] Bar skipped — connection lost: {0}", Time[0]));
|
||||||
return;
|
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)
|
var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
|
||||||
Print(string.Format("[SDK] Waiting for bars: current={0}, required={1}", CurrentBar, BarsRequiredToTrade));
|
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
|
||||||
return;
|
DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc(
|
||||||
|
DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified),
|
||||||
|
centralZone);
|
||||||
|
ntBarTimeEt = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ntBarTimeEt = Time[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Time[0] == _lastBarTime)
|
bool isRthBar = ntBarTimeEt.TimeOfDay >= new TimeSpan(9, 30, 0)
|
||||||
return;
|
&& 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.
|
// Log first processable bar and every 100th bar.
|
||||||
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
|
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
|
||||||
@@ -212,6 +426,50 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
Close[0]));
|
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
|
try
|
||||||
{
|
{
|
||||||
var barData = ConvertCurrentBar();
|
var barData = ConvertCurrentBar();
|
||||||
@@ -290,9 +548,186 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
|
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
|
||||||
return;
|
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);
|
_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()
|
private void InitializeSdkComponents()
|
||||||
{
|
{
|
||||||
_logger = new BasicLogger(Name);
|
_logger = new BasicLogger(Name);
|
||||||
@@ -317,7 +752,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
_riskConfig,
|
_riskConfig,
|
||||||
_sizingConfig);
|
_sizingConfig);
|
||||||
|
|
||||||
_riskManager = new BasicRiskManager(_logger);
|
_riskManager = new BasicRiskManager(_logger, DailyLossLimit);
|
||||||
_positionSizer = new BasicPositionSizer(_logger);
|
_positionSizer = new BasicPositionSizer(_logger);
|
||||||
_circuitBreaker = new ExecutionCircuitBreaker(
|
_circuitBreaker = new ExecutionCircuitBreaker(
|
||||||
_logger,
|
_logger,
|
||||||
@@ -331,6 +766,8 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
|
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
|
||||||
ConfigureStrategyParameters();
|
ConfigureStrategyParameters();
|
||||||
|
PortfolioRiskManager.Instance.RegisterStrategy(Name, _riskConfig);
|
||||||
|
Print(string.Format("[NT8-SDK] Registered with PortfolioRiskManager: {0}", PortfolioRiskManager.Instance.GetStatusSnapshot()));
|
||||||
|
|
||||||
_ordersSubmittedToday = 0;
|
_ordersSubmittedToday = 0;
|
||||||
_lastBarTime = DateTime.MinValue;
|
_lastBarTime = DateTime.MinValue;
|
||||||
@@ -341,9 +778,24 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
private BarData ConvertCurrentBar()
|
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(
|
return NT8DataConverter.ConvertBar(
|
||||||
Instrument.MasterInstrument.Name,
|
Instrument.MasterInstrument.Name,
|
||||||
Time[0],
|
barTimeEt,
|
||||||
Open[0],
|
Open[0],
|
||||||
High[0],
|
High[0],
|
||||||
Low[0],
|
Low[0],
|
||||||
@@ -354,6 +806,21 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
private StrategyContext BuildStrategyContext()
|
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>();
|
var customData = new Dictionary<string, object>();
|
||||||
customData.Add("CurrentBar", CurrentBar);
|
customData.Add("CurrentBar", CurrentBar);
|
||||||
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
|
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
|
||||||
@@ -361,7 +828,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
return NT8DataConverter.ConvertContext(
|
return NT8DataConverter.ConvertContext(
|
||||||
Instrument.MasterInstrument.Name,
|
Instrument.MasterInstrument.Name,
|
||||||
Time[0],
|
etTime,
|
||||||
BuildPositionInfo(),
|
BuildPositionInfo(),
|
||||||
BuildAccountInfo(),
|
BuildAccountInfo(),
|
||||||
BuildSessionInfo(),
|
BuildSessionInfo(),
|
||||||
@@ -370,7 +837,23 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
private AccountInfo BuildAccountInfo()
|
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;
|
_lastAccountInfo = accountInfo;
|
||||||
return accountInfo;
|
return accountInfo;
|
||||||
}
|
}
|
||||||
@@ -391,20 +874,78 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
private MarketSession BuildSessionInfo()
|
private MarketSession BuildSessionInfo()
|
||||||
{
|
{
|
||||||
if (_currentSession != null && _currentSession.SessionStart.Date == Time[0].Date)
|
DateTime etTime;
|
||||||
return _currentSession;
|
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);
|
// Futures trade nearly 24 hours. Bars at/after 17:00 ET belong to the next
|
||||||
var sessionEnd = Time[0].Date.AddHours(16);
|
// calendar day's RTH trading session.
|
||||||
var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16;
|
DateTime tradingDate;
|
||||||
var sessionName = isRth ? "RTH" : "ETH";
|
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;
|
return _currentSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
|
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)
|
if (EnableVerboseLogging)
|
||||||
Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol));
|
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)
|
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
|
||||||
{
|
{
|
||||||
// Circuit breaker gate
|
if (State != State.Historical)
|
||||||
|
{
|
||||||
if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
|
if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
|
||||||
{
|
{
|
||||||
var state = _circuitBreaker.GetState();
|
var cbState = _circuitBreaker.GetState();
|
||||||
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
|
Print(String.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", cbState.Reason));
|
||||||
if (_logger != null)
|
if (_logger != null)
|
||||||
_logger.LogWarning("Circuit breaker blocked order: {0}", state.Reason);
|
_logger.LogWarning("Circuit breaker blocked order: {0}", cbState.Reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks);
|
bool useRunner = EnableRunner && request.Quantity >= 2;
|
||||||
_executionAdapter.SubmitOrder(request, orderName);
|
|
||||||
|
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.Side == OmsOrderSide.Buy)
|
||||||
{
|
EnterLong(scalerQty, _scalerSignalName);
|
||||||
if (request.Type == OmsOrderType.Market)
|
else
|
||||||
EnterLong(request.Quantity, orderName);
|
EnterShort(scalerQty, _scalerSignalName);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// --- Submit runner leg (no fixed target — exits via trailing stop) ---
|
||||||
|
if (useRunner)
|
||||||
|
{
|
||||||
if (intent.StopTicks > 0)
|
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)
|
if (request.Side == OmsOrderSide.Buy)
|
||||||
SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value);
|
EnterLong(runnerQty, _runnerSignalName);
|
||||||
|
else
|
||||||
|
EnterShort(runnerQty, _runnerSignalName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_executionAdapter.SubmitOrder(request, _scalerSignalName);
|
||||||
|
|
||||||
if (_circuitBreaker != null)
|
if (_circuitBreaker != null)
|
||||||
_circuitBreaker.OnSuccess();
|
_circuitBreaker.OnSuccess();
|
||||||
@@ -507,8 +1085,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
{
|
{
|
||||||
if (_circuitBreaker != null)
|
if (_circuitBreaker != null)
|
||||||
_circuitBreaker.OnFailure();
|
_circuitBreaker.OnFailure();
|
||||||
|
Print(String.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
|
||||||
Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
|
|
||||||
if (_logger != null)
|
if (_logger != null)
|
||||||
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
|
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
|
||||||
throw;
|
throw;
|
||||||
@@ -539,6 +1116,83 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
return null;
|
return null;
|
||||||
return _executionAdapter.GetOrderStatus(orderName);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using NinjaTrader.NinjaScript;
|
|||||||
using NinjaTrader.NinjaScript.Indicators;
|
using NinjaTrader.NinjaScript.Indicators;
|
||||||
using NinjaTrader.NinjaScript.Strategies;
|
using NinjaTrader.NinjaScript.Strategies;
|
||||||
using NT8.Core.Common.Interfaces;
|
using NT8.Core.Common.Interfaces;
|
||||||
|
using NT8.Core.Intelligence;
|
||||||
using NT8.Strategies.Examples;
|
using NT8.Strategies.Examples;
|
||||||
using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy;
|
using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy;
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SimpleORBNT8 : NT8StrategyBase
|
public class SimpleORBNT8 : NT8StrategyBase
|
||||||
{
|
{
|
||||||
|
private int _lastSignalDirection;
|
||||||
|
|
||||||
[NinjaScriptProperty]
|
[NinjaScriptProperty]
|
||||||
[Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)]
|
[Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)]
|
||||||
[Range(5, 120)]
|
[Range(5, 120)]
|
||||||
@@ -37,6 +40,10 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
[Range(1, 50)]
|
[Range(1, 50)]
|
||||||
public int StopTicks { get; set; }
|
public int StopTicks { get; set; }
|
||||||
|
|
||||||
|
[NinjaScriptProperty]
|
||||||
|
[Display(Name = "Force Session Reset On Start", GroupName = "ORB Strategy", Order = 10)]
|
||||||
|
public bool ForceSessionReset { get; set; }
|
||||||
|
|
||||||
[NinjaScriptProperty]
|
[NinjaScriptProperty]
|
||||||
[Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)]
|
[Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)]
|
||||||
[Range(1, 100)]
|
[Range(1, 100)]
|
||||||
@@ -47,7 +54,9 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
if (State == State.SetDefaults)
|
if (State == State.SetDefaults)
|
||||||
{
|
{
|
||||||
Name = "Simple ORB NT8";
|
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;
|
OpeningRangeMinutes = 30;
|
||||||
StdDevMultiplier = 1.0;
|
StdDevMultiplier = 1.0;
|
||||||
@@ -56,18 +65,57 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
DailyLossLimit = 1000.0;
|
DailyLossLimit = 1000.0;
|
||||||
MaxTradeRisk = 200.0;
|
MaxTradeRisk = 200.0;
|
||||||
MaxOpenPositions = 1;
|
MaxOpenPositions = 2;
|
||||||
RiskPerTrade = 100.0;
|
RiskPerTrade = 100.0;
|
||||||
MinContracts = 1;
|
MinContracts = 1;
|
||||||
MaxContracts = 3;
|
MaxContracts = 3;
|
||||||
|
|
||||||
Calculate = Calculate.OnBarClose;
|
Calculate = Calculate.OnBarClose;
|
||||||
BarsRequiredToTrade = 50;
|
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();
|
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()
|
protected override IStrategy CreateSdkStrategy()
|
||||||
{
|
{
|
||||||
return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier);
|
return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier);
|
||||||
@@ -97,16 +145,114 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
_strategyConfig.Parameters["StopTicks"] = StopTicks;
|
_strategyConfig.Parameters["StopTicks"] = StopTicks;
|
||||||
_strategyConfig.Parameters["TargetTicks"] = TargetTicks;
|
_strategyConfig.Parameters["TargetTicks"] = TargetTicks;
|
||||||
_strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
|
_strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
|
||||||
|
_strategyConfig.Parameters["MinTradeGrade"] = MinTradeGrade;
|
||||||
|
|
||||||
|
if (Instrument != null && Instrument.MasterInstrument != null)
|
||||||
|
{
|
||||||
|
_strategyConfig.Parameters["TickSize"] = Instrument.MasterInstrument.TickSize;
|
||||||
|
}
|
||||||
|
|
||||||
if (_logger != null)
|
if (_logger != null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_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,
|
OpeningRangeMinutes,
|
||||||
StopTicks,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,31 @@ namespace NT8.Core.Intelligence
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Risk = 6,
|
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>
|
/// <summary>
|
||||||
/// Additional custom factor.
|
/// Additional custom factor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ namespace NT8.Core.Intelligence
|
|||||||
_factorWeights.Add(FactorType.Volatility, 1.0);
|
_factorWeights.Add(FactorType.Volatility, 1.0);
|
||||||
_factorWeights.Add(FactorType.Timing, 1.0);
|
_factorWeights.Add(FactorType.Timing, 1.0);
|
||||||
_factorWeights.Add(FactorType.ExecutionQuality, 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>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NT8.Core.Common.Models;
|
using NT8.Core.Common.Models;
|
||||||
|
using NT8.Core.Logging;
|
||||||
|
|
||||||
namespace NT8.Core.Intelligence
|
namespace NT8.Core.Intelligence
|
||||||
{
|
{
|
||||||
@@ -398,4 +399,625 @@ namespace NT8.Core.Intelligence
|
|||||||
return defaultValue;
|
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>());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,8 +109,8 @@ namespace NT8.Core.Intelligence
|
|||||||
|
|
||||||
private void InitializeDefaults()
|
private void InitializeDefaults()
|
||||||
{
|
{
|
||||||
_minimumGradeByMode.Add(RiskMode.ECP, TradeGrade.B);
|
_minimumGradeByMode.Add(RiskMode.ECP, TradeGrade.C);
|
||||||
_minimumGradeByMode.Add(RiskMode.PCP, TradeGrade.C);
|
_minimumGradeByMode.Add(RiskMode.PCP, TradeGrade.B);
|
||||||
_minimumGradeByMode.Add(RiskMode.DCP, TradeGrade.A);
|
_minimumGradeByMode.Add(RiskMode.DCP, TradeGrade.A);
|
||||||
_minimumGradeByMode.Add(RiskMode.HR, TradeGrade.APlus);
|
_minimumGradeByMode.Add(RiskMode.HR, TradeGrade.APlus);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
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
|
namespace NT8.Core.Orders
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
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
|
namespace NT8.Core.Orders
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ using NT8.Core.Common.Models;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
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
|
namespace NT8.Core.Orders
|
||||||
{
|
{
|
||||||
#region Core Order Models
|
#region Core Order Models
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace NT8.Core.Risk
|
|||||||
private double _dailyPnL;
|
private double _dailyPnL;
|
||||||
private double _maxDrawdown;
|
private double _maxDrawdown;
|
||||||
private bool _tradingHalted;
|
private bool _tradingHalted;
|
||||||
|
private double _configuredDailyLossLimit;
|
||||||
private DateTime _lastUpdate = DateTime.UtcNow;
|
private DateTime _lastUpdate = DateTime.UtcNow;
|
||||||
private readonly Dictionary<string, double> _symbolExposure = new Dictionary<string, double>();
|
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");
|
if (logger == null) throw new ArgumentNullException("logger");
|
||||||
_logger = 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)
|
public RiskDecision ValidateOrder(StrategyIntent intent, StrategyContext context, RiskConfig config)
|
||||||
@@ -216,10 +226,8 @@ namespace NT8.Core.Risk
|
|||||||
|
|
||||||
private void CheckEmergencyConditions(double dayPnL)
|
private void CheckEmergencyConditions(double dayPnL)
|
||||||
{
|
{
|
||||||
// Emergency halt if daily loss exceeds 90% of limit
|
// Emergency halt if daily loss exceeds 90% of configured limit
|
||||||
// Using a default limit of 1000 as this method doesn't have access to config
|
if (dayPnL <= -(_configuredDailyLossLimit * 0.9) && !_tradingHalted)
|
||||||
// In Phase 1, this should be improved to use the actual config value
|
|
||||||
if (dayPnL <= -(1000 * 0.9) && !_tradingHalted)
|
|
||||||
{
|
{
|
||||||
_tradingHalted = true;
|
_tradingHalted = true;
|
||||||
_logger.LogCritical("Emergency halt triggered at 90% of daily loss limit: {0:C}", dayPnL);
|
_logger.LogCritical("Emergency halt triggered at 90% of daily loss limit: {0:C}", dayPnL);
|
||||||
|
|||||||
276
src/NT8.Core/Risk/PortfolioRiskManager.cs
Normal file
276
src/NT8.Core/Risk/PortfolioRiskManager.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,6 +112,11 @@ namespace NT8.Strategies.Examples
|
|||||||
_factorCalculators.Add(new VolatilityRegimeFactorCalculator());
|
_factorCalculators.Add(new VolatilityRegimeFactorCalculator());
|
||||||
_factorCalculators.Add(new TimeInSessionFactorCalculator());
|
_factorCalculators.Add(new TimeInSessionFactorCalculator());
|
||||||
_factorCalculators.Add(new ExecutionQualityFactorCalculator());
|
_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(
|
_logger.LogInformation(
|
||||||
"SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}",
|
"SimpleORBStrategy initialized with OR period {0} minutes and multiplier {1:F2}",
|
||||||
@@ -148,9 +153,25 @@ namespace NT8.Strategies.Examples
|
|||||||
UpdateRiskMode(context);
|
UpdateRiskMode(context);
|
||||||
UpdateConfluenceInputs(bar, 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
|
// Only trade during RTH
|
||||||
@@ -171,8 +192,26 @@ namespace NT8.Strategies.Examples
|
|||||||
return null;
|
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 (_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;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var openingRange = _openingRangeHigh - _openingRangeLow;
|
var openingRange = _openingRangeHigh - _openingRangeLow;
|
||||||
var volatilityBuffer = openingRange * (_stdDevMultiplier - 1.0);
|
var volatilityBuffer = openingRange * (_stdDevMultiplier - 1.0);
|
||||||
@@ -191,18 +230,51 @@ namespace NT8.Strategies.Examples
|
|||||||
if (candidate == null)
|
if (candidate == null)
|
||||||
return 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 score = _scorer.CalculateScore(candidate, context, bar, _factorCalculators);
|
||||||
var mode = _riskModeManager.GetCurrentMode();
|
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(
|
_logger.LogInformation(
|
||||||
"SimpleORBStrategy rejected intent for {0}: Grade={1}, Mode={2}, Reason={3}",
|
"SimpleORBStrategy filtered by grade: Score={0:F3} Grade={1} MinGrade={2}",
|
||||||
candidate.Symbol,
|
score.WeightedScore,
|
||||||
score.Grade,
|
score.Grade,
|
||||||
mode,
|
minGrade);
|
||||||
reason);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +284,8 @@ namespace NT8.Strategies.Examples
|
|||||||
|
|
||||||
candidate.Confidence = score.WeightedScore;
|
candidate.Confidence = score.WeightedScore;
|
||||||
candidate.Reason = string.Format("{0}; grade={1}; mode={2}", candidate.Reason, score.Grade, mode);
|
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["trade_grade"] = score.Grade.ToString();
|
||||||
candidate.Metadata["risk_mode"] = mode.ToString();
|
candidate.Metadata["risk_mode"] = mode.ToString();
|
||||||
candidate.Metadata["grade_multiplier"] = gradeMultiplier;
|
candidate.Metadata["grade_multiplier"] = gradeMultiplier;
|
||||||
@@ -221,6 +294,24 @@ namespace NT8.Strategies.Examples
|
|||||||
|
|
||||||
_tradeTaken = true;
|
_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(
|
_logger.LogInformation(
|
||||||
"SimpleORBStrategy accepted intent for {0}: Side={1}, Grade={2}, Mode={3}, Score={4:F3}, Mult={5:F2}",
|
"SimpleORBStrategy accepted intent for {0}: Side={1}, Grade={2}, Mode={3}, Score={4:F3}, Mult={5:F2}",
|
||||||
candidate.Symbol,
|
candidate.Symbol,
|
||||||
@@ -273,7 +364,27 @@ namespace NT8.Strategies.Examples
|
|||||||
/// <param name="parameters">Parameter map.</param>
|
/// <param name="parameters">Parameter map.</param>
|
||||||
public void SetParameters(Dictionary<string, object> parameters)
|
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()
|
private void EnsureInitialized()
|
||||||
@@ -299,21 +410,55 @@ namespace NT8.Strategies.Examples
|
|||||||
var avwap = _avwapCalculator.GetCurrentValue();
|
var avwap = _avwapCalculator.GetCurrentValue();
|
||||||
var avwapSlope = _avwapCalculator.GetSlope(10);
|
var avwapSlope = _avwapCalculator.GetSlope(10);
|
||||||
|
|
||||||
var bars = new List<BarData>();
|
|
||||||
bars.Add(bar);
|
|
||||||
var valueArea = _volumeProfileAnalyzer.CalculateValueArea(bars);
|
|
||||||
|
|
||||||
if (context.CustomData == null)
|
if (context.CustomData == null)
|
||||||
context.CustomData = new Dictionary<string, object>();
|
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["current_bar"] = bar;
|
||||||
context.CustomData["avwap"] = avwap;
|
context.CustomData["avwap"] = avwap;
|
||||||
context.CustomData["avwap_slope"] = avwapSlope;
|
context.CustomData["avwap_slope"] = avwapSlope;
|
||||||
context.CustomData["trend_confirm"] = avwapSlope > 0.0 ? 1.0 : 0.0;
|
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["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["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)
|
private void ResetSession(DateTime sessionStart)
|
||||||
@@ -325,6 +470,13 @@ namespace NT8.Strategies.Examples
|
|||||||
_openingRangeLow = Double.MaxValue;
|
_openingRangeLow = Double.MaxValue;
|
||||||
_openingRangeReady = false;
|
_openingRangeReady = false;
|
||||||
_tradeTaken = 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)
|
private void UpdateOpeningRange(BarData bar)
|
||||||
@@ -341,7 +493,7 @@ namespace NT8.Strategies.Examples
|
|||||||
var stopTicks = _config != null && _config.Parameters.ContainsKey("StopTicks")
|
var stopTicks = _config != null && _config.Parameters.ContainsKey("StopTicks")
|
||||||
? (int)_config.Parameters["StopTicks"]
|
? (int)_config.Parameters["StopTicks"]
|
||||||
: 8;
|
: 8;
|
||||||
var targetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks")
|
int baseTargetTicks = _config != null && _config.Parameters.ContainsKey("TargetTicks")
|
||||||
? (int)_config.Parameters["TargetTicks"]
|
? (int)_config.Parameters["TargetTicks"]
|
||||||
: 16;
|
: 16;
|
||||||
|
|
||||||
@@ -349,10 +501,87 @@ namespace NT8.Strategies.Examples
|
|||||||
metadata.Add("orb_high", _openingRangeHigh);
|
metadata.Add("orb_high", _openingRangeHigh);
|
||||||
metadata.Add("orb_low", _openingRangeLow);
|
metadata.Add("orb_low", _openingRangeLow);
|
||||||
metadata.Add("orb_range", openingRange);
|
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("trigger_price", lastPrice);
|
||||||
metadata.Add("multiplier", _stdDevMultiplier);
|
metadata.Add("multiplier", _stdDevMultiplier);
|
||||||
metadata.Add("opening_range_start", _openingRangeStart);
|
metadata.Add("opening_range_start", _openingRangeStart);
|
||||||
metadata.Add("opening_range_end", _openingRangeEnd);
|
metadata.Add("opening_range_end", _openingRangeEnd);
|
||||||
|
metadata.Add("base_target_ticks", baseTargetTicks);
|
||||||
|
metadata.Add("dynamic_target_ticks", targetTicks);
|
||||||
|
|
||||||
return new StrategyIntent(
|
return new StrategyIntent(
|
||||||
symbol,
|
symbol,
|
||||||
@@ -365,5 +594,49 @@ namespace NT8.Strategies.Examples
|
|||||||
"ORB breakout signal",
|
"ORB breakout signal",
|
||||||
metadata);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
299
tests/NT8.Core.Tests/Intelligence/OrbConfluenceFactorTests.cs
Normal file
299
tests/NT8.Core.Tests/Intelligence/OrbConfluenceFactorTests.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs
Normal file
134
tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,9 +149,9 @@ namespace NT8.Core.Tests.Sizing
|
|||||||
|
|
||||||
var intent = CreateIntent();
|
var intent = CreateIntent();
|
||||||
var context = CreateContext();
|
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 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(
|
var result = sizer.CalculateGradeBasedSize(
|
||||||
intent,
|
intent,
|
||||||
|
|||||||
@@ -15,11 +15,20 @@ namespace NT8.Integration.Tests
|
|||||||
[TestClass]
|
[TestClass]
|
||||||
public class NT8OrderAdapterIntegrationTests
|
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]
|
[TestMethod]
|
||||||
public void Initialize_NullRiskManager_ThrowsArgumentNullException()
|
public void Initialize_NullRiskManager_ThrowsArgumentNullException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
var sizer = new TestPositionSizer(1);
|
var sizer = new TestPositionSizer(1);
|
||||||
|
|
||||||
// Act / Assert
|
// Act / Assert
|
||||||
@@ -31,7 +40,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void Initialize_NullPositionSizer_ThrowsArgumentNullException()
|
public void Initialize_NullPositionSizer_ThrowsArgumentNullException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
var risk = new TestRiskManager(true);
|
var risk = new TestRiskManager(true);
|
||||||
|
|
||||||
// Act / Assert
|
// Act / Assert
|
||||||
@@ -43,7 +52,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void ExecuteIntent_NotInitialized_ThrowsInvalidOperationException()
|
public void ExecuteIntent_NotInitialized_ThrowsInvalidOperationException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
|
|
||||||
// Act / Assert
|
// Act / Assert
|
||||||
Assert.ThrowsException<InvalidOperationException>(
|
Assert.ThrowsException<InvalidOperationException>(
|
||||||
@@ -54,7 +63,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void ExecuteIntent_RiskRejected_DoesNotRecordExecution()
|
public void ExecuteIntent_RiskRejected_DoesNotRecordExecution()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
var risk = new TestRiskManager(false);
|
var risk = new TestRiskManager(false);
|
||||||
var sizer = new TestPositionSizer(3);
|
var sizer = new TestPositionSizer(3);
|
||||||
adapter.Initialize(risk, sizer);
|
adapter.Initialize(risk, sizer);
|
||||||
@@ -71,7 +80,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void ExecuteIntent_AllowedAndSized_RecordsExecution()
|
public void ExecuteIntent_AllowedAndSized_RecordsExecution()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
var risk = new TestRiskManager(true);
|
var risk = new TestRiskManager(true);
|
||||||
var sizer = new TestPositionSizer(4);
|
var sizer = new TestPositionSizer(4);
|
||||||
adapter.Initialize(risk, sizer);
|
adapter.Initialize(risk, sizer);
|
||||||
@@ -94,7 +103,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void GetExecutionHistory_ReturnsCopy_NotMutableInternalReference()
|
public void GetExecutionHistory_ReturnsCopy_NotMutableInternalReference()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
var risk = new TestRiskManager(true);
|
var risk = new TestRiskManager(true);
|
||||||
var sizer = new TestPositionSizer(2);
|
var sizer = new TestPositionSizer(2);
|
||||||
adapter.Initialize(risk, sizer);
|
adapter.Initialize(risk, sizer);
|
||||||
@@ -113,7 +122,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void OnOrderUpdate_EmptyOrderId_ThrowsArgumentException()
|
public void OnOrderUpdate_EmptyOrderId_ThrowsArgumentException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
|
|
||||||
// Act / Assert
|
// Act / Assert
|
||||||
Assert.ThrowsException<ArgumentException>(
|
Assert.ThrowsException<ArgumentException>(
|
||||||
@@ -124,7 +133,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void OnExecutionUpdate_EmptyExecutionId_ThrowsArgumentException()
|
public void OnExecutionUpdate_EmptyExecutionId_ThrowsArgumentException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
|
|
||||||
// Act / Assert
|
// Act / Assert
|
||||||
Assert.ThrowsException<ArgumentException>(
|
Assert.ThrowsException<ArgumentException>(
|
||||||
@@ -135,7 +144,7 @@ namespace NT8.Integration.Tests
|
|||||||
public void OnExecutionUpdate_EmptyOrderId_ThrowsArgumentException()
|
public void OnExecutionUpdate_EmptyOrderId_ThrowsArgumentException()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var adapter = new NT8OrderAdapter();
|
var adapter = new NT8OrderAdapter(new FakeBridge());
|
||||||
|
|
||||||
// Act / Assert
|
// Act / Assert
|
||||||
Assert.ThrowsException<ArgumentException>(
|
Assert.ThrowsException<ArgumentException>(
|
||||||
|
|||||||
Reference in New Issue
Block a user