narf 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
narf-0.1.0/MANIFEST.in ADDED
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ recursive-include narf *.pyi
3
+
narf-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: narf
3
+ Version: 0.1.0
4
+ Summary: Market data loader library for cryptocurrency exchanges
5
+ Project-URL: Homepage, https://github.com/numan-narf/narf-market-data
6
+ Project-URL: Repository, https://github.com/numan-narf/narf-market-data
7
+ Project-URL: Issues, https://github.com/numan-narf/narf-market-data/issues
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: build>=1.3.0
11
+ Requires-Dist: matplotlib>=3.10.7
12
+ Requires-Dist: pandas>=2.3.3
13
+ Requires-Dist: pandas-stubs>=2.3.2.250926
14
+ Requires-Dist: requests>=2.32.5
15
+ Requires-Dist: twine>=6.2.0
16
+ Requires-Dist: types-requests>=2.32.4.20250913
narf-0.1.0/README.md ADDED
File without changes
@@ -0,0 +1,4 @@
1
+ """Narf - Market data loader library for cryptocurrency exchanges."""
2
+
3
+ __version__ = "0.1.0"
4
+
@@ -0,0 +1,4 @@
1
+ from .loaders.binance_vision import binance
2
+
3
+
4
+ __all__ = ["binance"]
@@ -0,0 +1,141 @@
1
+ import ast
2
+ import os
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Dict
5
+
6
+
7
+ @dataclass
8
+ class NodeSpec:
9
+ children: Optional[Dict[str, "NodeSpec"]] = None
10
+ loader: Optional[type] = None
11
+
12
+
13
+ def generate_pyi(
14
+ spec: NodeSpec,
15
+ root_class_name: str,
16
+ root_var_name: str,
17
+ package_name: str,
18
+ loader_import: str,
19
+ loader_class_name: str = "LoaderType",
20
+ ):
21
+ """
22
+ Generates:
23
+
24
+ narf/data/loaders/<package_name>/__init__.pyi
25
+
26
+ The loader class name is taken from `loader_class_name`.
27
+ """
28
+
29
+ module = ast.Module(body=[], type_ignores=[])
30
+
31
+ # ------------------------------------------------------------
32
+ # Import loader dynamically under unified name LoaderType
33
+ # ------------------------------------------------------------
34
+ # Example:
35
+ # from narf.data.loaders.binance_vision.loader import BinanceVisionLoader as LoaderType
36
+ import_stmt = f"from {loader_import} import {loader_class_name} as LoaderType"
37
+ module.body.extend(ast.parse(import_stmt).body)
38
+
39
+ class_defs: list[ast.ClassDef] = []
40
+
41
+ # ------------------------------------------------------------
42
+ # Class builder
43
+ # ------------------------------------------------------------
44
+ def make_class(name: str, fields: Dict[str, str], bases: list[str]):
45
+ return ast.ClassDef(
46
+ name=name,
47
+ bases=[ast.Name(id=b, ctx=ast.Load()) for b in bases],
48
+ keywords=[],
49
+ decorator_list=[],
50
+ body=[
51
+ ast.AnnAssign(
52
+ target=ast.Name(id=field, ctx=ast.Store()),
53
+ annotation=ast.Name(id=typ, ctx=ast.Load()),
54
+ value=None,
55
+ simple=1,
56
+ )
57
+ for field, typ in fields.items()
58
+ ] or [ast.Pass()],
59
+ )
60
+
61
+ # ------------------------------------------------------------
62
+ # DFS
63
+ # ------------------------------------------------------------
64
+ def visit(node: NodeSpec, name: str):
65
+ bases = []
66
+
67
+ # This node also behaves as a loader
68
+ if node.loader:
69
+ bases.append("LoaderType")
70
+
71
+ fields: Dict[str, str] = {}
72
+
73
+ if node.children:
74
+ for ch_name, ch_spec in node.children.items():
75
+
76
+ # child namespace OR non-leaf loader
77
+ if ch_spec.children or ch_spec.loader:
78
+ child_class_name = name + ch_name.capitalize()
79
+ fields[ch_name] = child_class_name
80
+ visit(ch_spec, child_class_name)
81
+
82
+ else:
83
+ # pure leaf loader
84
+ fields[ch_name] = "LoaderType"
85
+
86
+ # Always create a class for this node
87
+ class_defs.append(make_class(name, fields, bases))
88
+
89
+ # Start from root class
90
+ visit(spec, root_class_name)
91
+
92
+ # ------------------------------------------------------------
93
+ # Add classes
94
+ # ------------------------------------------------------------
95
+ module.body.extend(class_defs)
96
+
97
+ # ------------------------------------------------------------
98
+ # Add final variable: <root_var_name>: <root_class_name>
99
+ # ------------------------------------------------------------
100
+ module.body.extend([
101
+ ast.AnnAssign(
102
+ target=ast.Name(id=root_var_name, ctx=ast.Store()),
103
+ annotation=ast.Name(id=root_class_name, ctx=ast.Load()),
104
+ value=None,
105
+ simple=1,
106
+ ),
107
+ # Keep your dummy function (unchanged)
108
+ ast.FunctionDef(
109
+ name="generate_pyi",
110
+ args=ast.arguments(
111
+ posonlyargs=[],
112
+ args=[],
113
+ vararg=None,
114
+ kwonlyargs=[],
115
+ kw_defaults=[],
116
+ kwarg=None,
117
+ defaults=[],
118
+ ),
119
+ body=[ast.Pass()],
120
+ decorator_list=[],
121
+ returns=None,
122
+ )
123
+ ])
124
+
125
+ # ------------------------------------------------------------
126
+ # Write file to:
127
+ # narf/data/loaders/<package_name>/__init__.pyi
128
+ # ------------------------------------------------------------
129
+ ast.fix_missing_locations(module)
130
+ source = ast.unparse(module)
131
+
132
+ this_file = os.path.abspath(__file__)
133
+ this_dir = os.path.dirname(this_file)
134
+ out_path = os.path.join(this_dir, "loaders", package_name, "__init__.pyi")
135
+
136
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
137
+
138
+ with open(out_path, "w", encoding="utf-8") as f:
139
+ f.write(source)
140
+
141
+ return out_path
File without changes
@@ -0,0 +1,94 @@
1
+ import abc
2
+ from datetime import datetime
3
+ from typing import Optional
4
+ from narf.data.loaders.base_spec import NodeSpec
5
+
6
+ class Loader(abc.ABC):
7
+ BASE_URL: str
8
+ URL_PATTERNS: list[tuple[tuple[str, ...], str]]
9
+
10
+ def __init__(self, path: tuple[str, ...]):
11
+ self.__path = path
12
+
13
+ def get_parsed_path(self):
14
+ from narf.data.loaders.binance_vision.spec import binance_vision_spec
15
+ return extract_semantic_path(binance_vision_spec, self.__path)
16
+
17
+ def _build_url(self, **kwargs):
18
+ from narf.data.loaders.binance_vision.spec import binance_vision_spec
19
+ template, semantic = select_pattern(self.URL_PATTERNS, self.__path, binance_vision_spec)
20
+
21
+ ctx = {
22
+ "base": self.BASE_URL,
23
+ **kwargs,
24
+ **semantic,
25
+ }
26
+
27
+ return template.format(**ctx)
28
+
29
+ def load(self, date: str, start: datetime, end: Optional[datetime] = None, interval: str = "1m") -> None:
30
+ print("Loading", self.__path, date, interval, start, end)
31
+
32
+
33
+ def extract_semantic_path(spec: NodeSpec, path: tuple[str, ...]):
34
+ """
35
+ Returns a dict mapping semantic key -> actual fragment.
36
+ Example:
37
+ ("futures","um","klines") →
38
+ {"market":"futures","margination":"um","datatype":"klines"}
39
+ """
40
+ mapping = {}
41
+ node = spec
42
+ i = 0
43
+
44
+ while i < len(path):
45
+ fragment = path[i]
46
+ if not node.children or fragment not in node.children:
47
+ raise ValueError("Invalid path fragment: " + fragment)
48
+
49
+ child = node.children[fragment]
50
+
51
+ # assign semantic key
52
+ mapping[node.key] = fragment
53
+
54
+ node = child
55
+ i += 1
56
+
57
+ return mapping
58
+
59
+
60
+ def match_pattern(pattern, path, semantic_keys):
61
+ """
62
+ pattern: ("futures","<margination>","<datatype>")
63
+ path: ("futures","um","klines")
64
+ semantic_keys: ["market","margination","datatype"]
65
+ """
66
+
67
+ if len(pattern) != len(path):
68
+ return False
69
+
70
+ for element, frag, key in zip(pattern, path, semantic_keys):
71
+
72
+ if element.startswith("<") and element.endswith(">"):
73
+ # semantic placeholder — must match the same semantic key
74
+ expected_key = element[1:-1]
75
+ if expected_key != key:
76
+ return False
77
+
78
+ else:
79
+ # literal element — must match fragment exactly
80
+ if element != frag:
81
+ return False
82
+
83
+ return True
84
+
85
+
86
+ def select_pattern(patterns: list[tuple[tuple[str, ...], str]], path: tuple[str, ...], spec: NodeSpec) -> tuple[str, dict[str, str]]:
87
+ semantic = extract_semantic_path(spec, path)
88
+ semantic_keys = list(semantic.keys()) # ["market","margination","datatype"]
89
+
90
+ for pattern, template in patterns:
91
+ if match_pattern(pattern, path, semantic_keys):
92
+ return template, semantic
93
+
94
+ raise ValueError(f"No pattern matches path: {path}")
@@ -0,0 +1,44 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ @dataclass
5
+ class NodeSpec:
6
+ key: str = ""
7
+ children: Optional[dict[str, "NodeSpec"]] = None
8
+ loader: Optional[type] = None
9
+
10
+
11
+ class Namespace:
12
+ def __init__(self, **children):
13
+ for name, val in children.items():
14
+ setattr(self, name, val)
15
+
16
+
17
+ def build(spec: NodeSpec, path: tuple[str, ...] = ()) -> object:
18
+ # Case A: namespace
19
+ if spec.children:
20
+ # build children
21
+ attrs = {
22
+ name: build(sub, path + (name,))
23
+ for name, sub in spec.children.items()
24
+ }
25
+
26
+ # create namespace object
27
+ ns = Namespace()
28
+ for k, v in attrs.items():
29
+ setattr(ns, k, v)
30
+
31
+ # also attach loader behavior
32
+ if spec.loader:
33
+ loader = spec.loader(path)
34
+ # copy loader methods/attrs into namespace
35
+ ns.load = loader.load
36
+ ns.__loader__ = loader # optional: keep reference to real loader
37
+
38
+ return ns
39
+
40
+ # Case B: pure leaf
41
+ if spec.loader:
42
+ return spec.loader(path)
43
+
44
+ raise ValueError("NodeSpec must have children or leaf")
@@ -0,0 +1,47 @@
1
+ from narf.data.loaders.binance_vision.loader import BinanceVisionLoader as LoaderType
2
+
3
+ class BinanceVisionFuturesUmKlines(LoaderType):
4
+ pass
5
+
6
+ class BinanceVisionFuturesUmTrades(LoaderType):
7
+ pass
8
+
9
+ class BinanceVisionFuturesUm:
10
+ klines: BinanceVisionFuturesUmKlines
11
+ trades: BinanceVisionFuturesUmTrades
12
+
13
+ class BinanceVisionFuturesCmKlines(LoaderType):
14
+ pass
15
+
16
+ class BinanceVisionFuturesCmTrades(LoaderType):
17
+ pass
18
+
19
+ class BinanceVisionFuturesCm:
20
+ klines: BinanceVisionFuturesCmKlines
21
+ trades: BinanceVisionFuturesCmTrades
22
+
23
+ class BinanceVisionFutures(LoaderType):
24
+ um: BinanceVisionFuturesUm
25
+ cm: BinanceVisionFuturesCm
26
+
27
+ class BinanceVisionSpotKlines(LoaderType):
28
+ pass
29
+
30
+ class BinanceVisionSpotTrades(LoaderType):
31
+ pass
32
+
33
+ class BinanceVisionSpotAggtrades(LoaderType):
34
+ pass
35
+
36
+ class BinanceVisionSpot(LoaderType):
37
+ klines: BinanceVisionSpotKlines
38
+ trades: BinanceVisionSpotTrades
39
+ aggTrades: BinanceVisionSpotAggtrades
40
+
41
+ class BinanceVision:
42
+ futures: BinanceVisionFutures
43
+ spot: BinanceVisionSpot
44
+ binance: BinanceVision
45
+
46
+ def generate_pyi():
47
+ pass
@@ -0,0 +1,21 @@
1
+ from narf.data.loaders.base_spec import build
2
+ from .spec import binance_vision_spec
3
+
4
+
5
+ binance = build(binance_vision_spec)
6
+
7
+
8
+ def generate_pyi():
9
+ from narf.data.generate import generate_pyi
10
+
11
+ generate_pyi(
12
+ spec=binance_vision_spec,
13
+ root_class_name="BinanceVision",
14
+ root_var_name="binance",
15
+ package_name="binance_vision",
16
+ loader_import="narf.data.loaders.binance_vision.loader",
17
+ loader_class_name="BinanceVisionLoader",
18
+ )
19
+
20
+
21
+ __all__ = ["binance", "generate_pyi"]
@@ -0,0 +1,47 @@
1
+ from narf.data.loaders.binance_vision.loader import BinanceVisionLoader as LoaderType
2
+
3
+ class BinanceVisionFuturesUmKlines(LoaderType):
4
+ pass
5
+
6
+ class BinanceVisionFuturesUmTrades(LoaderType):
7
+ pass
8
+
9
+ class BinanceVisionFuturesUm:
10
+ klines: BinanceVisionFuturesUmKlines
11
+ trades: BinanceVisionFuturesUmTrades
12
+
13
+ class BinanceVisionFuturesCmKlines(LoaderType):
14
+ pass
15
+
16
+ class BinanceVisionFuturesCmTrades(LoaderType):
17
+ pass
18
+
19
+ class BinanceVisionFuturesCm:
20
+ klines: BinanceVisionFuturesCmKlines
21
+ trades: BinanceVisionFuturesCmTrades
22
+
23
+ class BinanceVisionFutures(LoaderType):
24
+ um: BinanceVisionFuturesUm
25
+ cm: BinanceVisionFuturesCm
26
+
27
+ class BinanceVisionSpotKlines(LoaderType):
28
+ pass
29
+
30
+ class BinanceVisionSpotTrades(LoaderType):
31
+ pass
32
+
33
+ class BinanceVisionSpotAggtrades(LoaderType):
34
+ pass
35
+
36
+ class BinanceVisionSpot(LoaderType):
37
+ klines: BinanceVisionSpotKlines
38
+ trades: BinanceVisionSpotTrades
39
+ aggTrades: BinanceVisionSpotAggtrades
40
+
41
+ class BinanceVision:
42
+ futures: BinanceVisionFutures
43
+ spot: BinanceVisionSpot
44
+ binance: BinanceVision
45
+
46
+ def generate_pyi():
47
+ pass
@@ -0,0 +1,106 @@
1
+ import hashlib
2
+ import io
3
+ import os
4
+ from typing import List, Optional
5
+ from datetime import datetime
6
+ import zipfile
7
+ from matplotlib.dates import relativedelta
8
+ import requests
9
+ import pandas as pd
10
+
11
+ from narf.data.loaders.base_loader import Loader, extract_semantic_path
12
+
13
+
14
+ URL_PATTERNS = [
15
+ (
16
+ ("futures",),
17
+ "{base}/{market}/um/monthly/klines/{symbol}/{interval}/{symbol}-{interval}-{year}-{month:02d}.zip",
18
+ ),
19
+ (
20
+ ("futures", "<margination>", "<datatype>"),
21
+ "{base}/{market}/{margination}/monthly/{datatype}/{symbol}/{interval}/{symbol}-{interval}-{year}-{month:02d}.zip",
22
+ ),
23
+
24
+ (
25
+ ("spot",),
26
+ "{base}/{market}/monthly/{datatype}/{symbol}/{interval}/{symbol}-{interval}-{year}-{month:02d}.zip",
27
+ ),
28
+ (
29
+ ("spot", "aggTrades"),
30
+ "{base}/{market}/monthly/{datatype}/{symbol}/{symbol}-{datatype}-{year}-{month:02d}.zip",
31
+ ),
32
+ (
33
+ ("spot", "<datatype>"),
34
+ "{base}/{market}/monthly/{datatype}/{symbol}/{interval}/{symbol}-{interval}-{year}-{month:02d}.zip",
35
+ ),
36
+ ]
37
+
38
+
39
+ INDEX_COLUMN = {
40
+ "klines": "open_time",
41
+ "trades": "timestamp",
42
+ "aggTrades": "timestamp",
43
+ }
44
+
45
+
46
+ def cache_to_file(func):
47
+ def wrapper(url: str, *args, **kwargs):
48
+ url_hash = hashlib.sha256(url.encode()).hexdigest()
49
+ cache_file = f"cache/{url_hash}.csv"
50
+ if not os.path.exists(cache_file):
51
+ df = func(url, *args, **kwargs)
52
+ df.to_csv(cache_file)
53
+ return pd.read_csv(cache_file)
54
+ return wrapper
55
+
56
+
57
+ @cache_to_file
58
+ def load_zipped_csv(url: str, header: Optional[List[str]] = None) -> pd.DataFrame:
59
+ r = requests.get(url)
60
+ r.raise_for_status()
61
+
62
+ zf = zipfile.ZipFile(io.BytesIO(r.content))
63
+ name = zf.namelist()[0]
64
+
65
+ with zf.open(name) as f:
66
+ df = pd.read_csv(f)#, header=None if header else 0, names=header, index_col=False)
67
+
68
+ return df
69
+
70
+
71
+ class BinanceVisionLoader(Loader):
72
+ BASE_URL = "https://data.binance.vision/data"
73
+ URL_PATTERNS = URL_PATTERNS
74
+
75
+ def _load_month(self, symbol: str, interval: str, year: int, month: int) -> pd.DataFrame:
76
+ url = self._build_url(**{
77
+ "symbol": symbol,
78
+ "interval": interval,
79
+ "year": year,
80
+ "month": month,
81
+ })
82
+ print('Loading', url)
83
+ df = load_zipped_csv(url)
84
+
85
+ path = self.get_parsed_path()
86
+ idx_col = INDEX_COLUMN[path["datatype"]]
87
+
88
+ df[idx_col] = pd.to_datetime(df[idx_col] * 1000000)
89
+ df.set_index(idx_col, inplace=True)
90
+
91
+ return df
92
+
93
+ def load(self, symbol: str, start: datetime, end: Optional[datetime] = None, interval: str = "1m") -> pd.DataFrame:
94
+ if end is None:
95
+ end = datetime.now()
96
+
97
+ cursor = start
98
+ df = pd.DataFrame()
99
+ while cursor <= end:
100
+ df = pd.concat([
101
+ df,
102
+ self._load_month(symbol, interval, cursor.year, cursor.month),
103
+ ])
104
+ cursor += relativedelta(months=1)
105
+
106
+ return df
@@ -0,0 +1,21 @@
1
+ from narf.data.loaders.base_spec import NodeSpec
2
+ from narf.data.loaders.binance_vision.loader import BinanceVisionLoader
3
+
4
+
5
+ binance_vision_spec = NodeSpec("market", {
6
+ "futures": NodeSpec("margination", {
7
+ "um": NodeSpec("datatype", {
8
+ "klines": NodeSpec(loader=BinanceVisionLoader),
9
+ "trades": NodeSpec(loader=BinanceVisionLoader),
10
+ }),
11
+ "cm": NodeSpec("datatype", {
12
+ "klines": NodeSpec(loader=BinanceVisionLoader),
13
+ "trades": NodeSpec(loader=BinanceVisionLoader),
14
+ }),
15
+ }, loader=BinanceVisionLoader),
16
+ "spot": NodeSpec("datatype", {
17
+ "klines": NodeSpec(loader=BinanceVisionLoader),
18
+ "trades": NodeSpec(loader=BinanceVisionLoader),
19
+ "aggTrades": NodeSpec(loader=BinanceVisionLoader),
20
+ }, loader=BinanceVisionLoader),
21
+ })
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: narf
3
+ Version: 0.1.0
4
+ Summary: Market data loader library for cryptocurrency exchanges
5
+ Project-URL: Homepage, https://github.com/numan-narf/narf-market-data
6
+ Project-URL: Repository, https://github.com/numan-narf/narf-market-data
7
+ Project-URL: Issues, https://github.com/numan-narf/narf-market-data/issues
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: build>=1.3.0
11
+ Requires-Dist: matplotlib>=3.10.7
12
+ Requires-Dist: pandas>=2.3.3
13
+ Requires-Dist: pandas-stubs>=2.3.2.250926
14
+ Requires-Dist: requests>=2.32.5
15
+ Requires-Dist: twine>=6.2.0
16
+ Requires-Dist: types-requests>=2.32.4.20250913
@@ -0,0 +1,19 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ narf/__init__.py
5
+ narf.egg-info/PKG-INFO
6
+ narf.egg-info/SOURCES.txt
7
+ narf.egg-info/dependency_links.txt
8
+ narf.egg-info/requires.txt
9
+ narf.egg-info/top_level.txt
10
+ narf/data/__init__.py
11
+ narf/data/generate.py
12
+ narf/data/loaders/__init__.py
13
+ narf/data/loaders/base_loader.py
14
+ narf/data/loaders/base_spec.py
15
+ narf/data/loaders/binance/__init__.pyi
16
+ narf/data/loaders/binance_vision/__init__.py
17
+ narf/data/loaders/binance_vision/__init__.pyi
18
+ narf/data/loaders/binance_vision/loader.py
19
+ narf/data/loaders/binance_vision/spec.py
@@ -0,0 +1,7 @@
1
+ build>=1.3.0
2
+ matplotlib>=3.10.7
3
+ pandas>=2.3.3
4
+ pandas-stubs>=2.3.2.250926
5
+ requests>=2.32.5
6
+ twine>=6.2.0
7
+ types-requests>=2.32.4.20250913
@@ -0,0 +1,3 @@
1
+ cache
2
+ dist
3
+ narf
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "narf"
7
+ version = "0.1.0"
8
+ description = "Market data loader library for cryptocurrency exchanges"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "build>=1.3.0",
13
+ "matplotlib>=3.10.7",
14
+ "pandas>=2.3.3",
15
+ "pandas-stubs>=2.3.2.250926",
16
+ "requests>=2.32.5",
17
+ "twine>=6.2.0",
18
+ "types-requests>=2.32.4.20250913",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/numan-narf/narf-market-data"
23
+ Repository = "https://github.com/numan-narf/narf-market-data"
24
+ Issues = "https://github.com/numan-narf/narf-market-data/issues"
25
+
26
+ [tool.setuptools]
27
+ packages = {find = {}}
28
+
29
+ [tool.setuptools.package-data]
30
+ "*" = ["*.pyi"]
narf-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+