trade-database-manager 0.0.1.dev1__tar.gz → 0.0.1.dev2__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.
- {trade_database_manager-0.0.1.dev1/trade_database_manager.egg-info → trade_database_manager-0.0.1.dev2}/PKG-INFO +23 -2
- trade_database_manager-0.0.1.dev2/README.md +55 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/pyproject.toml +2 -1
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/setup.py +0 -1
- trade_database_manager-0.0.1.dev2/trade_database_manager/core/sql/sqlmanager.py +224 -0
- trade_database_manager-0.0.1.dev2/trade_database_manager/core/typedefs.py +17 -0
- trade_database_manager-0.0.1.dev2/trade_database_manager/manager/__init__.py +5 -0
- trade_database_manager-0.0.1.dev2/trade_database_manager/manager/fields_data_type.py +23 -0
- trade_database_manager-0.0.1.dev2/trade_database_manager/manager/metadata_sql.py +155 -0
- trade_database_manager-0.0.1.dev2/trade_database_manager/manager/typedefs.py +48 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2/trade_database_manager.egg-info}/PKG-INFO +23 -2
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager.egg-info/SOURCES.txt +5 -2
- trade_database_manager-0.0.1.dev1/README.md +0 -34
- trade_database_manager-0.0.1.dev1/trade_database_manager/core/sql/sqlmanager.py +0 -102
- trade_database_manager-0.0.1.dev1/trade_database_manager/tools/__init__.py +0 -0
- trade_database_manager-0.0.1.dev1/trade_database_manager/tools/init_config.py +0 -42
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/LICENSE +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/setup.cfg +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager/config.py +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager/core/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager/core/kdb/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager/core/sql/__init__.py +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager/core/sql/sqlreader.py +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager/core/sql/sqlwriter.py +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager.egg-info/dependency_links.txt +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager.egg-info/requires.txt +0 -0
- {trade_database_manager-0.0.1.dev1 → trade_database_manager-0.0.1.dev2}/trade_database_manager.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev2
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
5
|
Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -52,5 +52,26 @@ host all tradedbadmin ::/0 scram-sha-256
|
|
|
52
52
|
|
|
53
53
|
### KDB+ Initialization
|
|
54
54
|
|
|
55
|
+
#### get license
|
|
56
|
+
Request a 64-bit license from [Kx Systems](https://kx.com/kdb-insights-personal-edition-license-download). Fill the form
|
|
57
|
+
then the system will send an email with the license file and a base64 key (Either can be used to license the product).
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
#### Server side (Linux system)
|
|
60
|
+
1. Put the executable `q` and license in desired folder, say `/opt/l64` directory.
|
|
61
|
+
2. Create a folder to store the data, say `/opt/data` directory.
|
|
62
|
+
3. Set the environment variable `QLIC` to the directory where the license file is stored. And `QHOME` to the data directory.
|
|
63
|
+
4. Run the `q` executable with the following command:
|
|
64
|
+
```bash
|
|
65
|
+
$ q -p 5000 -s 1 -w 100 -t 1000 -T 1000 -U /opt/l64/trade.q
|
|
66
|
+
```
|
|
67
|
+
- `-p 5000`: This sets the port number for the kdb+ process. In this case, the port number is 5000.
|
|
68
|
+
|
|
69
|
+
- `-s 1`: This sets the number of secondary threads. In this case, it's set to 1.
|
|
70
|
+
|
|
71
|
+
- `-w 100`: This sets the workspace heap size. In this case, it's set to 100 MB.
|
|
72
|
+
|
|
73
|
+
- `-t 1000`: This sets the timer interval in milliseconds. In this case, it's set to 1000 milliseconds, or 1 second.
|
|
74
|
+
|
|
75
|
+
- `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
|
|
76
|
+
|
|
77
|
+
- `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<table border=1 cellpadding=10>
|
|
2
|
+
<tr>
|
|
3
|
+
<td style="color: red;">
|
|
4
|
+
|
|
5
|
+
#### \*\*\* IMPORTANT NOTICE \*\*\*
|
|
6
|
+
|
|
7
|
+
<p style="color: red">This package is <b>not</b> in a usable stage. It is only uploaded for convenience of developing and testing.</p>
|
|
8
|
+
|
|
9
|
+
</td></tr></table>
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Postgreslq Initialization
|
|
14
|
+
|
|
15
|
+
```sql
|
|
16
|
+
CREATE DATABASE trade_data;
|
|
17
|
+
CREATE USER tradedbadmin WITH PASSWORD 'trade_password';
|
|
18
|
+
GRANT ALL PRIVILEGES ON DATABASE trade_data TO tradedbadmin;
|
|
19
|
+
\connect trade_data;
|
|
20
|
+
GRANT ALL ON SCHEMA public TO tradedbadmin;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Add the following lines in the `pg_hba.conf` file to allow password authentication for the `tradedbadmin` role.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
# trade database
|
|
27
|
+
host all tradedbadmin 0.0.0.0/0 scram-sha-256
|
|
28
|
+
host all tradedbadmin ::/0 scram-sha-256
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### KDB+ Initialization
|
|
32
|
+
|
|
33
|
+
#### get license
|
|
34
|
+
Request a 64-bit license from [Kx Systems](https://kx.com/kdb-insights-personal-edition-license-download). Fill the form
|
|
35
|
+
then the system will send an email with the license file and a base64 key (Either can be used to license the product).
|
|
36
|
+
|
|
37
|
+
#### Server side (Linux system)
|
|
38
|
+
1. Put the executable `q` and license in desired folder, say `/opt/l64` directory.
|
|
39
|
+
2. Create a folder to store the data, say `/opt/data` directory.
|
|
40
|
+
3. Set the environment variable `QLIC` to the directory where the license file is stored. And `QHOME` to the data directory.
|
|
41
|
+
4. Run the `q` executable with the following command:
|
|
42
|
+
```bash
|
|
43
|
+
$ q -p 5000 -s 1 -w 100 -t 1000 -T 1000 -U /opt/l64/trade.q
|
|
44
|
+
```
|
|
45
|
+
- `-p 5000`: This sets the port number for the kdb+ process. In this case, the port number is 5000.
|
|
46
|
+
|
|
47
|
+
- `-s 1`: This sets the number of secondary threads. In this case, it's set to 1.
|
|
48
|
+
|
|
49
|
+
- `-w 100`: This sets the workspace heap size. In this case, it's set to 100 MB.
|
|
50
|
+
|
|
51
|
+
- `-t 1000`: This sets the timer interval in milliseconds. In this case, it's set to 1000 milliseconds, or 1 second.
|
|
52
|
+
|
|
53
|
+
- `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
|
|
54
|
+
|
|
55
|
+
- `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trade_database_manager"
|
|
7
|
-
version = "0.0.1.
|
|
7
|
+
version = "0.0.1.dev2"
|
|
8
8
|
description = "A wrapper of kdb and sql for convenient trade data management."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -42,3 +42,4 @@ target-version = ['py310']
|
|
|
42
42
|
|
|
43
43
|
[tool.isort]
|
|
44
44
|
profile = "black"
|
|
45
|
+
line_length = 120
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Container
|
|
3
|
+
from functools import partial, reduce
|
|
4
|
+
from typing import Any, Literal, Sequence, Type, Union
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from sqlalchemy import Float, Index, Integer, MetaData, String, Table, create_engine, inspect, select, sql, text
|
|
8
|
+
from sqlalchemy.dialects.postgresql import insert
|
|
9
|
+
|
|
10
|
+
from ...config import CONFIG
|
|
11
|
+
from ..typedefs import FILTERFIELD_TYPE, QUERYFIELD_TYPE
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _insert_on_conflict_update(table, conn, keys, data_iter, indexes):
|
|
15
|
+
data = [dict(zip(keys, row)) for row in data_iter]
|
|
16
|
+
stmt = insert(table.table).values(data)
|
|
17
|
+
stmt = stmt.on_conflict_do_update(index_elements=indexes, set_={k: getattr(stmt.excluded, k) for k in keys})
|
|
18
|
+
result = conn.execute(stmt)
|
|
19
|
+
return result.rowcount
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _insert_on_conflict_nothing(table, conn, keys, data_iter):
|
|
23
|
+
data = [dict(zip(keys, row)) for row in data_iter]
|
|
24
|
+
stmt = insert(table.table).values(data).on_conflict_do_nothing(index_elements=keys)
|
|
25
|
+
result = conn.execute(stmt)
|
|
26
|
+
return result.rowcount
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SqlManager:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.engine = create_engine(CONFIG["sqlconnstr"])
|
|
32
|
+
|
|
33
|
+
def _execute(self, sql_executable: Union[str, sql.base.Executable]) -> Any:
|
|
34
|
+
if isinstance(sql_executable, str):
|
|
35
|
+
sql_executable = text(sql_executable)
|
|
36
|
+
with self.engine.begin() as conn:
|
|
37
|
+
return conn.execute(sql_executable)
|
|
38
|
+
|
|
39
|
+
def add_index(self, table_name: str, columns: Union[str, list[str]], unique: bool = True):
|
|
40
|
+
if isinstance(columns, str):
|
|
41
|
+
columns = [columns]
|
|
42
|
+
|
|
43
|
+
index_name = f"uix_{table_name}_{'_'.join(columns)}"
|
|
44
|
+
columns_str = ", ".join(columns)
|
|
45
|
+
unique_str = "UNIQUE" if unique else ""
|
|
46
|
+
table_meta = MetaData()
|
|
47
|
+
table = Table(table_name, table_meta, autoload_with=self.engine)
|
|
48
|
+
columns = [getattr(table.c, colname) for colname in columns if colname in table.c.keys()]
|
|
49
|
+
index = Index(index_name, *columns, unique=unique)
|
|
50
|
+
index.create(bind=self.engine)
|
|
51
|
+
|
|
52
|
+
def insert(
|
|
53
|
+
self,
|
|
54
|
+
table_name: str,
|
|
55
|
+
df: pd.DataFrame,
|
|
56
|
+
upsert: bool = True,
|
|
57
|
+
other_unique_index_columns: Sequence[str] = (),
|
|
58
|
+
other_non_unique_index_columns: Sequence[str] = (),
|
|
59
|
+
):
|
|
60
|
+
if_exists: Literal["replace", "append"] = "append"
|
|
61
|
+
inspector = inspect(self.engine)
|
|
62
|
+
new_table = not inspector.has_table(table_name)
|
|
63
|
+
method = partial(_insert_on_conflict_update, indexes=df.index.names) if upsert and not new_table else None
|
|
64
|
+
num_rows = df.to_sql(
|
|
65
|
+
table_name,
|
|
66
|
+
self.engine,
|
|
67
|
+
if_exists=if_exists,
|
|
68
|
+
index=True,
|
|
69
|
+
index_label=df.index.names,
|
|
70
|
+
method=method,
|
|
71
|
+
)
|
|
72
|
+
if new_table:
|
|
73
|
+
self.add_index(table_name, df.index.names, unique=True)
|
|
74
|
+
for column in other_unique_index_columns:
|
|
75
|
+
self.add_index(table_name, column, unique=True)
|
|
76
|
+
for column in other_non_unique_index_columns:
|
|
77
|
+
self.add_index(table_name, column, unique=False)
|
|
78
|
+
return num_rows
|
|
79
|
+
|
|
80
|
+
def _convert_to_sqlalchemy_type(self, column_type: Type, **kwargs):
|
|
81
|
+
if isinstance(column_type, str):
|
|
82
|
+
column_type = type(column_type)
|
|
83
|
+
if column_type == str:
|
|
84
|
+
return String(**kwargs)
|
|
85
|
+
if column_type == int:
|
|
86
|
+
return Integer()
|
|
87
|
+
if column_type == float:
|
|
88
|
+
return Float()
|
|
89
|
+
raise ValueError(f"Unsupported column type {column_type}")
|
|
90
|
+
|
|
91
|
+
def insert_column(self, table_name: str, column_name: str, column_type: Union[str, Type], type_kwargs: dict = None):
|
|
92
|
+
type_kwargs = type_kwargs or {}
|
|
93
|
+
sql_code = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {str(self._convert_to_sqlalchemy_type(column_type, **type_kwargs))}"
|
|
94
|
+
self._execute(sql_code)
|
|
95
|
+
|
|
96
|
+
def delete_column(self, table_name: str, column_name: str):
|
|
97
|
+
self._execute(f"ALTER TABLE {table_name} DROP COLUMN {column_name}")
|
|
98
|
+
|
|
99
|
+
def rename_column(self, table_name: str, old_column_name: str, new_column_name: str):
|
|
100
|
+
# check if any index is referring to the column
|
|
101
|
+
sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
|
|
102
|
+
index_refering_column = dict(self.engine.execute(sql_code).fetchall())
|
|
103
|
+
|
|
104
|
+
# drop indexes referring to the column
|
|
105
|
+
for index_name in index_refering_column:
|
|
106
|
+
self._execute(f"DROP INDEX IF EXISTS {index_name}")
|
|
107
|
+
|
|
108
|
+
# rename the column
|
|
109
|
+
self._execute(f"ALTER TABLE {table_name} RENAME COLUMN {old_column_name} TO {new_column_name}")
|
|
110
|
+
|
|
111
|
+
# recreate the indexes
|
|
112
|
+
for index_name, index_def in index_refering_column.items():
|
|
113
|
+
new_index_name = index_name.replace(old_column_name, new_column_name)
|
|
114
|
+
new_index_def = re.sub(rf"\(([^)]*?){old_column_name}([^)]*?)\)", rf"(\1{new_column_name}\2)", index_def)
|
|
115
|
+
new_index_def = new_index_def.replace(index_name, new_index_name)
|
|
116
|
+
self._execute(new_index_def)
|
|
117
|
+
|
|
118
|
+
def read_range_data(
|
|
119
|
+
self,
|
|
120
|
+
table_name: str,
|
|
121
|
+
query_fields="*",
|
|
122
|
+
start_time: pd.Timestamp = None,
|
|
123
|
+
end_time: pd.Timestamp = None,
|
|
124
|
+
filter_fields=None,
|
|
125
|
+
):
|
|
126
|
+
meta = MetaData()
|
|
127
|
+
table = Table(table_name, meta, autoload_with=self.engine)
|
|
128
|
+
|
|
129
|
+
if query_fields != "*":
|
|
130
|
+
query_fields = [table.columns[field] for field in query_fields]
|
|
131
|
+
else:
|
|
132
|
+
query_fields = table
|
|
133
|
+
|
|
134
|
+
stmt = select(*query_fields)
|
|
135
|
+
|
|
136
|
+
conditions = []
|
|
137
|
+
if start_time:
|
|
138
|
+
conditions.append(getattr(table.c, "timestamp", table.c.end_time) >= start_time)
|
|
139
|
+
if end_time:
|
|
140
|
+
conditions.append(getattr(table.c, "timestamp", table.c.start_time) <= end_time)
|
|
141
|
+
if filter_fields:
|
|
142
|
+
conditions.extend(
|
|
143
|
+
[
|
|
144
|
+
(
|
|
145
|
+
table.columns[field].in_(filter_values)
|
|
146
|
+
if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
|
|
147
|
+
else table.columns[field] == filter_values
|
|
148
|
+
)
|
|
149
|
+
for field, filter_values in filter_fields.items()
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
stmt = stmt.where(sql.and_(*conditions))
|
|
153
|
+
res = self._execute(stmt)
|
|
154
|
+
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
155
|
+
|
|
156
|
+
def read_data(self, table_name: str, query_fields: QUERYFIELD_TYPE = "*", filter_fields=None):
|
|
157
|
+
meta = MetaData()
|
|
158
|
+
table = Table(table_name, meta, autoload_with=self.engine)
|
|
159
|
+
|
|
160
|
+
if query_fields != "*":
|
|
161
|
+
query_fields = [table.columns[field] for field in query_fields]
|
|
162
|
+
stmt = select(*query_fields)
|
|
163
|
+
else:
|
|
164
|
+
query_fields = table
|
|
165
|
+
stmt = select(table)
|
|
166
|
+
|
|
167
|
+
if filter_fields:
|
|
168
|
+
conditions = [
|
|
169
|
+
(
|
|
170
|
+
table.columns[field].in_(filter_values)
|
|
171
|
+
if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
|
|
172
|
+
else table.columns[field] == filter_values
|
|
173
|
+
)
|
|
174
|
+
for field, filter_values in filter_fields.items()
|
|
175
|
+
]
|
|
176
|
+
stmt = stmt.where(sql.and_(*conditions))
|
|
177
|
+
|
|
178
|
+
# cannot use pandas.read_sql here as it discards timezone info
|
|
179
|
+
res = self._execute(stmt)
|
|
180
|
+
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
181
|
+
|
|
182
|
+
def read_data_across_tables(
|
|
183
|
+
self,
|
|
184
|
+
table_names: Sequence[str],
|
|
185
|
+
joined_columns: Sequence[str],
|
|
186
|
+
query_fields: QUERYFIELD_TYPE = "*",
|
|
187
|
+
filter_fields: FILTERFIELD_TYPE = None,
|
|
188
|
+
):
|
|
189
|
+
meta = MetaData()
|
|
190
|
+
tables = {table_name: Table(table_name, meta, autoload_with=self.engine) for table_name in table_names}
|
|
191
|
+
|
|
192
|
+
joined_table = reduce(
|
|
193
|
+
lambda x, y: x.join(y, sql.and_(*[x.columns[col] == y.columns[col] for col in joined_columns])),
|
|
194
|
+
tables.values(),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
query_fields_rel = []
|
|
198
|
+
if query_fields != "*":
|
|
199
|
+
for table_name, colnames in query_fields.items():
|
|
200
|
+
if isinstance(colnames, str):
|
|
201
|
+
query_fields_rel.append(tables[table_name].columns[colnames])
|
|
202
|
+
else:
|
|
203
|
+
query_fields_rel.extend([tables[table_name].columns[colname] for colname in colnames])
|
|
204
|
+
else:
|
|
205
|
+
query_fields_rel = [text("*")]
|
|
206
|
+
|
|
207
|
+
stmt = select(*query_fields_rel).select_from(joined_table)
|
|
208
|
+
|
|
209
|
+
if filter_fields:
|
|
210
|
+
conditions = []
|
|
211
|
+
for table_name, colnames in filter_fields.items():
|
|
212
|
+
table = tables[table_name]
|
|
213
|
+
for field, filter_values in colnames.items():
|
|
214
|
+
conditions.append(
|
|
215
|
+
table.columns[field].in_(filter_values)
|
|
216
|
+
if isinstance(filter_values, Container) and not isinstance(filter_values, (str, bytes))
|
|
217
|
+
else table.columns[field] == filter_values
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
stmt = stmt.where(sql.and_(*conditions))
|
|
221
|
+
|
|
222
|
+
# cannot use pandas.read_sql here as it discards timezone info
|
|
223
|
+
res = self._execute(stmt)
|
|
224
|
+
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/22 17:48
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : typedefs.py
|
|
5
|
+
# @Purpose :
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Literal, Optional, Sequence, TypeVar, Union
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
T_SeqT = Union[T, Sequence[T]]
|
|
11
|
+
Opt_T_SeqT = Optional[Union[T, Sequence[T]]]
|
|
12
|
+
|
|
13
|
+
T_DictT = Union[T, Dict[str, T]]
|
|
14
|
+
Opt_T_DictT = Optional[Union[T, Dict[str, T]]]
|
|
15
|
+
|
|
16
|
+
QUERYFIELD_TYPE = T_DictT[Union[Literal["*"], Sequence[str]]]
|
|
17
|
+
FILTERFIELD_TYPE = Opt_T_DictT[Dict[str, Union[str, Sequence[str], int, float, bool]]]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/19 16:44
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : fields_data_type.py
|
|
5
|
+
# @Purpose :
|
|
6
|
+
|
|
7
|
+
FIELD_DATA_TYPE_SQL = {
|
|
8
|
+
"ticker": "VARCHAR(20)",
|
|
9
|
+
"name": "VARCHAR(20)",
|
|
10
|
+
"currency": "VARCHAR(6)",
|
|
11
|
+
"exchange": "VARCHAR(10)",
|
|
12
|
+
"timezone": "VARCHAR(30)",
|
|
13
|
+
"tick_size": "REAL",
|
|
14
|
+
"lot_size": "REAL",
|
|
15
|
+
"min_lots": "REAL",
|
|
16
|
+
"market_tplus": "INTEGER",
|
|
17
|
+
"listed_date": "DATE",
|
|
18
|
+
"delisted_date": "DATE",
|
|
19
|
+
"sector": "VARCHAR(30)",
|
|
20
|
+
"industry": "VARCHAR(36)",
|
|
21
|
+
"country": "VARCHAR(36)",
|
|
22
|
+
"board_type": "VARCHAR(20)",
|
|
23
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/15 20:28
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : metadata_sql.py
|
|
5
|
+
# @Purpose : Instrument metadata stored in SQL database
|
|
6
|
+
|
|
7
|
+
from collections.abc import Container
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from ..core.sql.sqlmanager import SqlManager
|
|
13
|
+
from .typedefs import EXCHANGE_LITERALS, INST_TYPE_LITERALS, Opt_T_SeqT, T_DictT
|
|
14
|
+
|
|
15
|
+
COMMON_METADATA_COLUMNS = [
|
|
16
|
+
"name",
|
|
17
|
+
"inst_type",
|
|
18
|
+
"currency",
|
|
19
|
+
"timezone",
|
|
20
|
+
"tick_size",
|
|
21
|
+
"lot_size",
|
|
22
|
+
"min_lots",
|
|
23
|
+
"market_tplus",
|
|
24
|
+
"listed_date",
|
|
25
|
+
"delisted_date",
|
|
26
|
+
]
|
|
27
|
+
TYPE_METADATA_COLUMNS = {
|
|
28
|
+
"STK": ["sector", "industry", "country", "state", "board_type"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MetadataSql:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.manager = SqlManager()
|
|
35
|
+
|
|
36
|
+
def update_instrument_metadata(self, data: Union[pd.DataFrame, list[dict], dict]):
|
|
37
|
+
type_specific_columns = set(data.columns) - set(COMMON_METADATA_COLUMNS)
|
|
38
|
+
if "inst_type" not in data.columns and bool(type_specific_columns):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Non-common columns found ({','.join(type_specific_columns)}) but inst_type column not provided."
|
|
41
|
+
)
|
|
42
|
+
if isinstance(data, dict):
|
|
43
|
+
data = [data]
|
|
44
|
+
if isinstance(data, list):
|
|
45
|
+
data = pd.DataFrame(data)
|
|
46
|
+
data.set_index(["ticker", "exchange"], inplace=True)
|
|
47
|
+
data_common = data[data.columns.intersection(COMMON_METADATA_COLUMNS)]
|
|
48
|
+
|
|
49
|
+
self.manager.insert("instruments", data_common, upsert=True)
|
|
50
|
+
if "inst_type" in data.columns:
|
|
51
|
+
for inst_type, columns in TYPE_METADATA_COLUMNS.items():
|
|
52
|
+
data_type_df = data.loc[data.inst_type == inst_type, data.columns.intersection(columns)]
|
|
53
|
+
if not data_type_df.empty:
|
|
54
|
+
self.manager.insert(f"instruments_{inst_type.lower()}", data_type_df, upsert=True)
|
|
55
|
+
|
|
56
|
+
def read_metadata(
|
|
57
|
+
self,
|
|
58
|
+
ticker: Opt_T_SeqT[str] = None,
|
|
59
|
+
exchange: Opt_T_SeqT[EXCHANGE_LITERALS] = None,
|
|
60
|
+
query_fields="*",
|
|
61
|
+
filter_fields=None,
|
|
62
|
+
) -> T_DictT[pd.DataFrame]:
|
|
63
|
+
filter_fields = filter_fields or {}
|
|
64
|
+
if ticker is not None:
|
|
65
|
+
filter_fields["ticker"] = ticker
|
|
66
|
+
if exchange is not None:
|
|
67
|
+
if not isinstance(exchange, str) and isinstance(exchange, Container):
|
|
68
|
+
assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
|
|
69
|
+
filter_fields["exchange"] = exchange
|
|
70
|
+
query_fields_common = (
|
|
71
|
+
["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
|
|
72
|
+
if query_fields != "*"
|
|
73
|
+
else "*"
|
|
74
|
+
)
|
|
75
|
+
filter_fields_common = {
|
|
76
|
+
k: v for k, v in filter_fields.items() if k in ["ticker", "exchange"] + COMMON_METADATA_COLUMNS
|
|
77
|
+
}
|
|
78
|
+
all_fields_common = (
|
|
79
|
+
len(query_fields_common) == len(query_fields)
|
|
80
|
+
and len(filter_fields_common) == len(filter_fields)
|
|
81
|
+
and query_fields != "*"
|
|
82
|
+
)
|
|
83
|
+
if not (query_fields == "*" or "inst_type" in query_fields):
|
|
84
|
+
query_fields_common.append("inst_type")
|
|
85
|
+
common_df = self.manager.read_data("instruments", query_fields=query_fields_common, filter_fields=filter_fields)
|
|
86
|
+
if common_df.empty:
|
|
87
|
+
return {}
|
|
88
|
+
res = {}
|
|
89
|
+
for inst_type, common_df_by_type in common_df.groupby("inst_type"):
|
|
90
|
+
if all_fields_common:
|
|
91
|
+
res[inst_type] = common_df_by_type
|
|
92
|
+
continue
|
|
93
|
+
query_fields_type = (
|
|
94
|
+
["ticker", "exchange"] + [f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]]
|
|
95
|
+
if query_fields != "*"
|
|
96
|
+
else "*"
|
|
97
|
+
)
|
|
98
|
+
filter_fields_type = {k: v for k, v in filter_fields.items() if k in TYPE_METADATA_COLUMNS[inst_type]}
|
|
99
|
+
filter_fields_type["ticker"] = common_df_by_type["ticker"].to_list()
|
|
100
|
+
filter_fields_type["exchange"] = common_df_by_type["exchange"].to_list()
|
|
101
|
+
type_df = self.manager.read_data(
|
|
102
|
+
f"instruments_{inst_type.lower()}", query_fields=query_fields_type, filter_fields=filter_fields_type
|
|
103
|
+
)
|
|
104
|
+
type_df = common_df_by_type.merge(type_df, on=["ticker", "exchange"], how="inner")
|
|
105
|
+
res[inst_type] = type_df.set_index(["ticker", "exchange"])
|
|
106
|
+
return res
|
|
107
|
+
|
|
108
|
+
def read_metadata_for_insttype(
|
|
109
|
+
self,
|
|
110
|
+
inst_type: INST_TYPE_LITERALS,
|
|
111
|
+
ticker: Opt_T_SeqT[str] = None,
|
|
112
|
+
exchange: Opt_T_SeqT[EXCHANGE_LITERALS] = None,
|
|
113
|
+
query_fields="*",
|
|
114
|
+
filter_fields=None,
|
|
115
|
+
) -> pd.DataFrame:
|
|
116
|
+
filter_fields = filter_fields or {}
|
|
117
|
+
filter_fields["inst_type"] = inst_type
|
|
118
|
+
if ticker is not None:
|
|
119
|
+
filter_fields["ticker"] = ticker
|
|
120
|
+
if exchange is not None:
|
|
121
|
+
if not isinstance(exchange, str) and isinstance(exchange, Container):
|
|
122
|
+
assert len(exchange) == len(ticker), "Exchange must be a single value or the same length as ticker."
|
|
123
|
+
filter_fields["exchange"] = exchange
|
|
124
|
+
|
|
125
|
+
if query_fields == "*":
|
|
126
|
+
query_fields_cross = "*"
|
|
127
|
+
else:
|
|
128
|
+
query_fields_common = ["ticker", "exchange"] + [f for f in query_fields if f in COMMON_METADATA_COLUMNS]
|
|
129
|
+
query_fields_type = ["ticker", "exchange"] + [
|
|
130
|
+
f for f in query_fields if f in TYPE_METADATA_COLUMNS[inst_type]
|
|
131
|
+
]
|
|
132
|
+
query_fields_cross = {
|
|
133
|
+
"instruments": query_fields_common,
|
|
134
|
+
f"instruments_{inst_type.lower()}": query_fields_type,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
filter_fields_common = {
|
|
138
|
+
k: v for k, v in filter_fields.items() if k in ["ticker", "exchange"] + COMMON_METADATA_COLUMNS
|
|
139
|
+
}
|
|
140
|
+
filter_fields_type = {
|
|
141
|
+
k: v for k, v in filter_fields.items() if k in ["ticker", "exchange"] + TYPE_METADATA_COLUMNS[inst_type]
|
|
142
|
+
}
|
|
143
|
+
filter_fields_cross = {
|
|
144
|
+
"instruments": filter_fields_common,
|
|
145
|
+
f"instruments_{inst_type.lower()}": filter_fields_type,
|
|
146
|
+
}
|
|
147
|
+
df = self.manager.read_data_across_tables(
|
|
148
|
+
["instruments", f"instruments_{inst_type.lower()}"],
|
|
149
|
+
joined_columns=["ticker", "exchange"],
|
|
150
|
+
query_fields=query_fields_cross,
|
|
151
|
+
filter_fields=filter_fields_cross,
|
|
152
|
+
)
|
|
153
|
+
if isinstance(df.columns, pd.Index):
|
|
154
|
+
return df.loc[:, ~df.columns.duplicated()].set_index(["ticker", "exchange"])
|
|
155
|
+
return df.set_index(["ticker", "exchange"])
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Time : 2024/4/22 16:18
|
|
3
|
+
# @Author : YQ Tsui
|
|
4
|
+
# @File : typedefs.py
|
|
5
|
+
# @Purpose : Type hints for convenience
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Literal, Optional, Sequence, TypeVar, Union
|
|
8
|
+
|
|
9
|
+
INST_TYPE_LITERALS = Literal["STK", "FUT", "OPT", "IDX", "ETF", "FND", "BND", "CASH", "CRYPTO"]
|
|
10
|
+
EXCHANGE_LITERALS = Literal[
|
|
11
|
+
"SSE",
|
|
12
|
+
"SZSE",
|
|
13
|
+
"HKEX",
|
|
14
|
+
"CFFEX",
|
|
15
|
+
"SHFE",
|
|
16
|
+
"DCE",
|
|
17
|
+
"CZCE",
|
|
18
|
+
"SGX",
|
|
19
|
+
"CBOT",
|
|
20
|
+
"CME",
|
|
21
|
+
"COMEX",
|
|
22
|
+
"NYMEX",
|
|
23
|
+
"ICE",
|
|
24
|
+
"LME",
|
|
25
|
+
"TOCOM",
|
|
26
|
+
"JPX",
|
|
27
|
+
"KRX",
|
|
28
|
+
"ASX",
|
|
29
|
+
"NSE",
|
|
30
|
+
"BSE",
|
|
31
|
+
"NSE",
|
|
32
|
+
"BSE",
|
|
33
|
+
"MCX",
|
|
34
|
+
"MOEX",
|
|
35
|
+
"TSE",
|
|
36
|
+
"TWSE",
|
|
37
|
+
"SET",
|
|
38
|
+
"IDX",
|
|
39
|
+
"CRYPTO",
|
|
40
|
+
"SMART",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
T = TypeVar("T")
|
|
44
|
+
T_SeqT = Union[T, Sequence[T]]
|
|
45
|
+
Opt_T_SeqT = Optional[Union[T, Sequence[T]]]
|
|
46
|
+
|
|
47
|
+
T_DictT = Union[T, Dict[str, T]]
|
|
48
|
+
Opt_T_DictT = Optional[Union[T, Dict[str, T]]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trade_database_manager
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev2
|
|
4
4
|
Summary: A wrapper of kdb and sql for convenient trade data management.
|
|
5
5
|
Author-email: "Y.Q. Cui" <qianyun210603@hotmail.com>
|
|
6
6
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -52,5 +52,26 @@ host all tradedbadmin ::/0 scram-sha-256
|
|
|
52
52
|
|
|
53
53
|
### KDB+ Initialization
|
|
54
54
|
|
|
55
|
+
#### get license
|
|
56
|
+
Request a 64-bit license from [Kx Systems](https://kx.com/kdb-insights-personal-edition-license-download). Fill the form
|
|
57
|
+
then the system will send an email with the license file and a base64 key (Either can be used to license the product).
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
#### Server side (Linux system)
|
|
60
|
+
1. Put the executable `q` and license in desired folder, say `/opt/l64` directory.
|
|
61
|
+
2. Create a folder to store the data, say `/opt/data` directory.
|
|
62
|
+
3. Set the environment variable `QLIC` to the directory where the license file is stored. And `QHOME` to the data directory.
|
|
63
|
+
4. Run the `q` executable with the following command:
|
|
64
|
+
```bash
|
|
65
|
+
$ q -p 5000 -s 1 -w 100 -t 1000 -T 1000 -U /opt/l64/trade.q
|
|
66
|
+
```
|
|
67
|
+
- `-p 5000`: This sets the port number for the kdb+ process. In this case, the port number is 5000.
|
|
68
|
+
|
|
69
|
+
- `-s 1`: This sets the number of secondary threads. In this case, it's set to 1.
|
|
70
|
+
|
|
71
|
+
- `-w 100`: This sets the workspace heap size. In this case, it's set to 100 MB.
|
|
72
|
+
|
|
73
|
+
- `-t 1000`: This sets the timer interval in milliseconds. In this case, it's set to 1000 milliseconds, or 1 second.
|
|
74
|
+
|
|
75
|
+
- `-T 1000`: This sets the timeout in seconds for client queries. In this case, it's set to 1000 seconds.
|
|
76
|
+
|
|
77
|
+
- `-U /opt/l64/trade.q`: This sets the access control list file. In this case, the file is located at `/opt/l64/trade.q`. This file contains a list of usernames and passwords for clients that are allowed to connect to the kdb+ process.
|
|
@@ -10,10 +10,13 @@ trade_database_manager.egg-info/dependency_links.txt
|
|
|
10
10
|
trade_database_manager.egg-info/requires.txt
|
|
11
11
|
trade_database_manager.egg-info/top_level.txt
|
|
12
12
|
trade_database_manager/core/__init__.py
|
|
13
|
+
trade_database_manager/core/typedefs.py
|
|
13
14
|
trade_database_manager/core/kdb/__init__.py
|
|
14
15
|
trade_database_manager/core/sql/__init__.py
|
|
15
16
|
trade_database_manager/core/sql/sqlmanager.py
|
|
16
17
|
trade_database_manager/core/sql/sqlreader.py
|
|
17
18
|
trade_database_manager/core/sql/sqlwriter.py
|
|
18
|
-
trade_database_manager/
|
|
19
|
-
trade_database_manager/
|
|
19
|
+
trade_database_manager/manager/__init__.py
|
|
20
|
+
trade_database_manager/manager/fields_data_type.py
|
|
21
|
+
trade_database_manager/manager/metadata_sql.py
|
|
22
|
+
trade_database_manager/manager/typedefs.py
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<table border=1 cellpadding=10>
|
|
2
|
-
<tr>
|
|
3
|
-
<td style="color: red;">
|
|
4
|
-
|
|
5
|
-
#### \*\*\* IMPORTANT NOTICE \*\*\*
|
|
6
|
-
|
|
7
|
-
<p style="color: red">This package is <b>not</b> in a usable stage. It is only uploaded for convenience of developing and testing.</p>
|
|
8
|
-
|
|
9
|
-
</td></tr></table>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
### Postgreslq Initialization
|
|
14
|
-
|
|
15
|
-
```sql
|
|
16
|
-
CREATE DATABASE trade_data;
|
|
17
|
-
CREATE USER tradedbadmin WITH PASSWORD 'trade_password';
|
|
18
|
-
GRANT ALL PRIVILEGES ON DATABASE trade_data TO tradedbadmin;
|
|
19
|
-
\connect trade_data;
|
|
20
|
-
GRANT ALL ON SCHEMA public TO tradedbadmin;
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Add the following lines in the `pg_hba.conf` file to allow password authentication for the `tradedbadmin` role.
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
# trade database
|
|
27
|
-
host all tradedbadmin 0.0.0.0/0 scram-sha-256
|
|
28
|
-
host all tradedbadmin ::/0 scram-sha-256
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### KDB+ Initialization
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```q
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
from functools import partial
|
|
2
|
-
from typing import Literal, Sequence, Union
|
|
3
|
-
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from sqlalchemy import create_engine, inspect
|
|
6
|
-
from sqlalchemy.dialects.postgresql import insert
|
|
7
|
-
import re
|
|
8
|
-
from ...config import CONFIG
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _insert_on_conflict_update(table, conn, keys, data_iter, indexes):
|
|
12
|
-
data = [dict(zip(keys, row)) for row in data_iter]
|
|
13
|
-
stmt = insert(table.table).values(data)
|
|
14
|
-
stmt = stmt.on_conflict_do_update(index_elements=indexes, set_={k: getattr(stmt.excluded, k) for k in keys})
|
|
15
|
-
result = conn.execute(stmt)
|
|
16
|
-
return result.rowcount
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _insert_on_conflict_nothing(table, conn, keys, data_iter):
|
|
20
|
-
data = [dict(zip(keys, row)) for row in data_iter]
|
|
21
|
-
stmt = insert(table.table).values(data).on_conflict_do_nothing(index_elements=keys)
|
|
22
|
-
result = conn.execute(stmt)
|
|
23
|
-
return result.rowcount
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class SqlManager:
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
):
|
|
30
|
-
self.engine = create_engine(CONFIG["sqlconnstr"])
|
|
31
|
-
|
|
32
|
-
def add_index(self, table_name: str, columns: Union[str, Sequence[str]], unique: bool = True):
|
|
33
|
-
if isinstance(columns, str):
|
|
34
|
-
columns = [columns]
|
|
35
|
-
index_name = f"uix_{table_name}_{'_'.join(columns)}"
|
|
36
|
-
columns_str = ", ".join(columns)
|
|
37
|
-
unique_str = "UNIQUE" if unique else ""
|
|
38
|
-
self.engine.execute(f"CREATE {unique_str} INDEX {index_name} ON {table_name} ({columns_str})")
|
|
39
|
-
|
|
40
|
-
def insert(
|
|
41
|
-
self,
|
|
42
|
-
table_name: str,
|
|
43
|
-
df: pd.DataFrame,
|
|
44
|
-
upsert: bool = True,
|
|
45
|
-
other_unique_index_columns: Sequence[str] = (),
|
|
46
|
-
other_non_unique_index_columns: Sequence[str] = (),
|
|
47
|
-
):
|
|
48
|
-
if_exists: Literal["replace", "append"] = "append"
|
|
49
|
-
inspector = inspect(self.engine)
|
|
50
|
-
new_table = not inspector.has_table(table_name)
|
|
51
|
-
method = (
|
|
52
|
-
partial(_insert_on_conflict_update, indexes=df.index.names)
|
|
53
|
-
if upsert and not new_table
|
|
54
|
-
else None
|
|
55
|
-
)
|
|
56
|
-
num_rows = df.to_sql(
|
|
57
|
-
table_name,
|
|
58
|
-
self.engine,
|
|
59
|
-
if_exists=if_exists,
|
|
60
|
-
index=True,
|
|
61
|
-
index_label=df.index.names,
|
|
62
|
-
method=method,
|
|
63
|
-
)
|
|
64
|
-
if new_table:
|
|
65
|
-
self.add_index(table_name, df.index.names)
|
|
66
|
-
for column in other_unique_index_columns:
|
|
67
|
-
self.add_index(table_name, column, unique=True)
|
|
68
|
-
for column in other_non_unique_index_columns:
|
|
69
|
-
self.add_index(table_name, column, unique=False)
|
|
70
|
-
return num_rows
|
|
71
|
-
|
|
72
|
-
def rename_column(self, table_name: str, old_column_name: str, new_column_name: str):
|
|
73
|
-
# check if any index is referring to the column
|
|
74
|
-
sql_code = f"SELECT indexname, indexdef FROM pg_indexes WHERE indexdef LIKE '%%(%%{old_column_name}%%)%%' and tablename = '{table_name}'"
|
|
75
|
-
index_refering_column = dict(self.engine.execute(sql_code).fetchall())
|
|
76
|
-
|
|
77
|
-
# drop indexes referring to the column
|
|
78
|
-
for index_name in index_refering_column:
|
|
79
|
-
self.engine.execute(f"DROP INDEX IF EXISTS {index_name}")
|
|
80
|
-
|
|
81
|
-
# rename the column
|
|
82
|
-
self.engine.execute(f"ALTER TABLE {table_name} RENAME COLUMN {old_column_name} TO {new_column_name}")
|
|
83
|
-
|
|
84
|
-
# recreate the indexes
|
|
85
|
-
for index_name, index_def in index_refering_column.items():
|
|
86
|
-
new_index_name = index_name.replace(old_column_name, new_column_name)
|
|
87
|
-
new_index_def = re.sub(rf"\(([^)]*?){old_column_name}([^)]*?)\)", rf"(\1{new_column_name}\2)", index_def)
|
|
88
|
-
new_index_def = new_index_def.replace(index_name, new_index_name)
|
|
89
|
-
self.engine.execute(new_index_def)
|
|
90
|
-
|
|
91
|
-
def read_range_data(
|
|
92
|
-
self, table_name: str, query_fields, start_time: pd.Timestamp, end_time: pd.Timestamp, filter_fields=None
|
|
93
|
-
):
|
|
94
|
-
sql_code = f"SELECT {', '.join(query_fields)} FROM {table_name} WHERE end_time >= '{start_time}' AND start_time <= '{end_time}'"
|
|
95
|
-
if filter_fields:
|
|
96
|
-
sql_code += (
|
|
97
|
-
f" AND {' AND '.join([f'{field} IN {tuple(filter_values)}' for field, filter_values in filter_fields])}"
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# cannot use pandas.read_sql here as it discards timezone info
|
|
101
|
-
res = self.engine.execute(sql_code)
|
|
102
|
-
return pd.DataFrame(res.fetchall(), columns=res.keys())
|
|
File without changes
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import importlib.util
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from ruamel.yaml import YAML
|
|
6
|
-
|
|
7
|
-
if __name__ == "__main__":
|
|
8
|
-
parser = argparse.ArgumentParser()
|
|
9
|
-
parser.add_argument("-u", "--username", type=str)
|
|
10
|
-
parser.add_argument("-pw", "--password", type=str)
|
|
11
|
-
parser.add_argument(
|
|
12
|
-
"--host",
|
|
13
|
-
type=str,
|
|
14
|
-
default="localhost",
|
|
15
|
-
help="host of both kdb and sql, will be overwritten if sqlhost or kdbhost is specified",
|
|
16
|
-
)
|
|
17
|
-
parser.add_argument("--sqlhost", type=str, default="", help="host of sql")
|
|
18
|
-
parser.add_argument("--sqldb", type=str, default="trade_data", help="database name of sql")
|
|
19
|
-
parser.add_argument("--kdbhost", type=str, default="", help="host of kdb")
|
|
20
|
-
parser.add_argument("--sqlport", type=str, default="5432", help="port of sql")
|
|
21
|
-
parser.add_argument("--kdbport", type=str, default="5000", help="port of kdb")
|
|
22
|
-
|
|
23
|
-
args = parser.parse_args()
|
|
24
|
-
|
|
25
|
-
kdb_host = args.host if args.kdbhost == "" else args.kdbhost
|
|
26
|
-
sql_host = args.host if args.sqlhost == "" else args.sqlhost
|
|
27
|
-
|
|
28
|
-
sql_protocol = "postgresql" if importlib.util.find_spec("psycopg2") is None else "postgresql+psycopg2"
|
|
29
|
-
|
|
30
|
-
config = {
|
|
31
|
-
"username": args.username,
|
|
32
|
-
"password": args.password,
|
|
33
|
-
"kdbhost": kdb_host,
|
|
34
|
-
"kdbport": args.kdbport,
|
|
35
|
-
"sqlconnstr": f"{sql_protocol}://{args.username}:{args.password}@{sql_host}:{args.sqlport}/{args.sqldb}",
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
yaml = YAML(typ="safe")
|
|
39
|
-
config_folder = Path("~/.tradedbmgr").expanduser()
|
|
40
|
-
config_folder.mkdir(exist_ok=True)
|
|
41
|
-
with open(config_folder.joinpath("config.yaml"), "w") as f:
|
|
42
|
-
yaml.dump(config, f)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|