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 +3 -0
- narf-0.1.0/PKG-INFO +16 -0
- narf-0.1.0/README.md +0 -0
- narf-0.1.0/narf/__init__.py +4 -0
- narf-0.1.0/narf/data/__init__.py +4 -0
- narf-0.1.0/narf/data/generate.py +141 -0
- narf-0.1.0/narf/data/loaders/__init__.py +0 -0
- narf-0.1.0/narf/data/loaders/base_loader.py +94 -0
- narf-0.1.0/narf/data/loaders/base_spec.py +44 -0
- narf-0.1.0/narf/data/loaders/binance/__init__.pyi +47 -0
- narf-0.1.0/narf/data/loaders/binance_vision/__init__.py +21 -0
- narf-0.1.0/narf/data/loaders/binance_vision/__init__.pyi +47 -0
- narf-0.1.0/narf/data/loaders/binance_vision/loader.py +106 -0
- narf-0.1.0/narf/data/loaders/binance_vision/spec.py +21 -0
- narf-0.1.0/narf.egg-info/PKG-INFO +16 -0
- narf-0.1.0/narf.egg-info/SOURCES.txt +19 -0
- narf-0.1.0/narf.egg-info/dependency_links.txt +1 -0
- narf-0.1.0/narf.egg-info/requires.txt +7 -0
- narf-0.1.0/narf.egg-info/top_level.txt +3 -0
- narf-0.1.0/pyproject.toml +30 -0
- narf-0.1.0/setup.cfg +4 -0
narf-0.1.0/MANIFEST.in
ADDED
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,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 @@
|
|
|
1
|
+
|
|
@@ -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