Skip to content

Commit cf3d0b2

Browse files
tdobrowolski1claude
andcommitted
fix: JsonMapper / data.source.parse don't touch LEAN-inherited properties (v0.1.1)
v0.1.0 had a parse-time bug surfaced the moment a consumer actually ran the bridge against the live API: FlashAlphaJsonMapper.PopulateProperties (C#) iterates ALL public properties on the bar — including the inherited BaseData.Symbol whose declared type is QuantConnect.Symbol, not string. The JSON key "symbol":"SPY" snake-cased to "Symbol" and the deserializer threw JsonException trying to convert "SPY" into a QC Symbol object. The Python parse() loop hit the same hazard via setattr(bar, "Symbol", "SPY") silently clobbering the QC Symbol instance with the raw ticker string. Fix: only walk properties declared on the bar SUBCLASS (and any non- QuantConnect ancestors). LEAN-owned surface (Symbol/Time/EndTime/Value/Price) is set explicitly by FlashAlphaSource.Parse before this method runs. CI tests passed previously only because they skipped when FLASHALPHA_API_KEY was absent — no live data reached the parse path. Setting the secret on the next CI run will exercise it for real. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a4051c4 commit cf3d0b2

6 files changed

Lines changed: 46 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
88

99
_No changes yet._
1010

11+
## [0.1.1] — 2026-05-30
12+
13+
### Fixed
14+
15+
- **`FlashAlphaJsonMapper` (C#) and `data.source.parse` (Python) no longer overwrite inherited LEAN properties from the JSON.** Previously the snake-case auto-mapper would walk every public property on the bar — including inherited `BaseData.Symbol` / `PythonData.Symbol` — and try to set them from JSON keys like `"symbol":"SPY"`. In C# this threw `JsonException` ("could not convert to `QuantConnect.Symbol`") at the very first parse; in Python it silently clobbered the QC Symbol object with the raw ticker string. The mapper now walks only attributes declared on the bar subclass (and any non-`QuantConnect.*` ancestors), so LEAN-owned surface is left alone. Caught by running v0.1.0 as a real NuGet consumer — surfaces on the first `FlashAlphaSource.Parse` against the live API.
16+
1117
## [0.1.0] — 2026-05-30
1218

1319
Initial public release. The bridge ships with full coverage of the FlashAlpha historical API surface as native QuantConnect LEAN custom-data bars, for both C# and Python.
@@ -26,5 +32,6 @@ Initial public release. The bridge ships with full coverage of the FlashAlpha hi
2632
- **Documentation corpus.** Repo-root `README.md` with side-by-side C# + Python examples, `docs/getting-started.md`, `docs/data-types.md` (per-bar field reference for all 17 endpoints), `docs/auth.md`, `docs/troubleshooting.md`, and five `docs/recipes/*.md` cookbooks.
2733
- **`llms.txt`** site map per [llmstxt.org](https://llmstxt.org/).
2834

29-
[Unreleased]: https://github.com/FlashAlpha-lab/flashalpha-quantconnect/compare/v0.1.0...HEAD
35+
[Unreleased]: https://github.com/FlashAlpha-lab/flashalpha-quantconnect/compare/v0.1.1...HEAD
36+
[0.1.1]: https://github.com/FlashAlpha-lab/flashalpha-quantconnect/compare/v0.1.0...v0.1.1
3037
[0.1.0]: https://github.com/FlashAlpha-lab/flashalpha-quantconnect/releases/tag/v0.1.0

src/csharp/FlashAlpha.QuantConnect/Data/FlashAlphaJsonMapper.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,28 @@ internal static class FlashAlphaJsonMapper
2828
public static void PopulateProperties(object bar, JsonElement root)
2929
{
3030
if (root.ValueKind != JsonValueKind.Object) return;
31+
32+
// Walk only properties declared on the bar SUBCLASS — never inherited
33+
// ones. BaseData ships an inherited Symbol/Time/Price/Value/EndTime
34+
// surface; the JSON's "symbol" key would otherwise auto-route into
35+
// BaseData.Symbol (typed QuantConnect.Symbol) and the deserializer
36+
// would throw on the string value. FlashAlphaSource.Parse already
37+
// sets Symbol/Time/EndTime explicitly before calling this method.
3138
var type = bar.GetType();
39+
for (var t = type; t != null && t != typeof(object); t = t.BaseType)
40+
{
41+
// Stop walking once we hit anything from QuantConnect.* — that's
42+
// LEAN's base hierarchy (BaseData, IBaseDataBar, ...). Subclass
43+
// properties remain in play.
44+
if (t.Namespace?.StartsWith("QuantConnect", StringComparison.Ordinal) == true) break;
45+
PopulateFromType(bar, t, root);
46+
}
47+
}
3248

33-
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
49+
private static void PopulateFromType(object bar, Type declaring, JsonElement root)
50+
{
51+
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
52+
foreach (var prop in declaring.GetProperties(flags))
3453
{
3554
if (!prop.CanWrite) continue;
3655
var jsonName = prop.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name

src/csharp/FlashAlpha.QuantConnect/FlashAlpha.QuantConnect.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<PackageId>FlashAlpha.QuantConnect</PackageId>
44
<RootNamespace>FlashAlpha.QuantConnect</RootNamespace>
5-
<Version>0.1.0</Version>
5+
<Version>0.1.1</Version>
66
<Authors>FlashAlpha</Authors>
77
<Company>FlashAlpha</Company>
88
<Copyright>Copyright (c) 2026 FlashAlpha</Copyright>

src/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "flashalpha-quantconnect"
7-
version = "0.1.0"
7+
version = "0.1.1"
88
description = "FlashAlpha options-flow and dealer-positioning data as QuantConnect LEAN custom-data bars. GEX, DEX, VEX, vol surface, 0DTE, VRP, max-pain."
99
readme = "README.md"
1010
license = "MIT"

src/python/src/flashalpha_quantconnect/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""FlashAlpha options-flow data as QuantConnect LEAN custom-data bars."""
22

3-
__version__ = "0.1.0"
3+
__version__ = "0.1.1"
44

55
from . import config
66
from .data.exposure import (

src/python/src/flashalpha_quantconnect/data/source.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,23 @@ def parse(bar_cls: Type, line: str, symbol: Any, date: datetime) -> Any:
8484
if not isinstance(obj, dict):
8585
return bar
8686

87+
# Collect attribute names declared on the bar SUBCLASS (and any non-LEAN
88+
# ancestors). We deliberately stop walking at the LEAN base — its
89+
# ``Symbol`` / ``Time`` / ``EndTime`` / ``Value`` / ``Price`` attributes
90+
# are already populated above, and the JSON's ``symbol`` key would
91+
# otherwise auto-route into ``BaseData.Symbol`` and clobber the QC
92+
# Symbol object with the raw ticker string.
93+
declared: set[str] = set()
94+
for cls in bar_cls.__mro__:
95+
mod = getattr(cls, "__module__", "") or ""
96+
if mod.startswith("QuantConnect") or cls is object:
97+
break
98+
declared.update(vars(cls).keys())
99+
declared.update(getattr(cls, "__annotations__", {}).keys())
100+
87101
for snake, value in obj.items():
88102
prop = _to_pascal_case(snake)
89-
if hasattr(bar, prop):
103+
if prop in declared:
90104
setattr(bar, prop, value)
91105

92106
# Honor explicit field-name aliases declared on the bar class.

0 commit comments

Comments
 (0)