diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-24 10:41:28 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-24 10:41:28 +0100 |
commit | 0c6eb1640c0d0c0e7b679d1702415a35319863f1 (patch) | |
tree | 758fa5203849f284bd8379d8928b22dfbbbc699e | |
parent | b53f483d54367875bed3769d2e4817866fbde224 (diff) | |
parent | 35667b31ddf1ce47a56ccbf4db9896dbc165ad0a (diff) | |
download | Trader-0c6eb1640c0d0c0e7b679d1702415a35319863f1.tar.gz Trader-0c6eb1640c0d0c0e7b679d1702415a35319863f1.tar.zst Trader-0c6eb1640c0d0c0e7b679d1702415a35319863f1.zip |
Merge branch 'store_reports' into dev
Add reports storing in database.
See https://git.immae.eu/mantisbt/view.php?id=56 for the expected schema
-rw-r--r-- | main.py | 30 | ||||
-rw-r--r-- | market.py | 51 | ||||
-rw-r--r-- | store.py | 20 | ||||
-rw-r--r-- | tasks/import_reports_to_database.py | 50 | ||||
-rw-r--r-- | test.py | 212 |
5 files changed, 286 insertions, 77 deletions
@@ -62,17 +62,20 @@ def make_order(market, value, currency, action="acquire", | |||
62 | 62 | ||
63 | def get_user_market(config_path, user_id, debug=False): | 63 | def get_user_market(config_path, user_id, debug=False): |
64 | pg_config, report_path = parse_config(config_path) | 64 | pg_config, report_path = parse_config(config_path) |
65 | market_config = list(fetch_markets(pg_config, str(user_id)))[0][0] | 65 | market_id, market_config, user_id = list(fetch_markets(pg_config, str(user_id)))[0] |
66 | return market.Market.from_config(market_config, debug=debug) | 66 | args = type('Args', (object,), { "debug": debug, "quiet": False })() |
67 | return market.Market.from_config(market_config, args, | ||
68 | pg_config=pg_config, market_id=market_id, | ||
69 | user_id=user_id, report_path=report_path) | ||
67 | 70 | ||
68 | def fetch_markets(pg_config, user): | 71 | def fetch_markets(pg_config, user): |
69 | connection = psycopg2.connect(**pg_config) | 72 | connection = psycopg2.connect(**pg_config) |
70 | cursor = connection.cursor() | 73 | cursor = connection.cursor() |
71 | 74 | ||
72 | if user is None: | 75 | if user is None: |
73 | cursor.execute("SELECT config,user_id FROM market_configs") | 76 | cursor.execute("SELECT id,config,user_id FROM market_configs") |
74 | else: | 77 | else: |
75 | cursor.execute("SELECT config,user_id FROM market_configs WHERE user_id = %s", user) | 78 | cursor.execute("SELECT id,config,user_id FROM market_configs WHERE user_id = %s", user) |
76 | 79 | ||
77 | for row in cursor: | 80 | for row in cursor: |
78 | yield row | 81 | yield row |
@@ -109,6 +112,9 @@ def parse_args(argv): | |||
109 | parser.add_argument("--after", | 112 | parser.add_argument("--after", |
110 | default=False, action='store_const', const=True, | 113 | default=False, action='store_const', const=True, |
111 | help="Run the steps after the cryptoportfolio update") | 114 | help="Run the steps after the cryptoportfolio update") |
115 | parser.add_argument("--quiet", | ||
116 | default=False, action='store_const', const=True, | ||
117 | help="Don't print messages") | ||
112 | parser.add_argument("--debug", | 118 | parser.add_argument("--debug", |
113 | default=False, action='store_const', const=True, | 119 | default=False, action='store_const', const=True, |
114 | help="Run in debug mode") | 120 | help="Run in debug mode") |
@@ -128,10 +134,12 @@ def parse_args(argv): | |||
128 | 134 | ||
129 | return args | 135 | return args |
130 | 136 | ||
131 | def process(market_config, user_id, report_path, args): | 137 | def process(market_config, market_id, user_id, args, report_path, pg_config): |
132 | try: | 138 | try: |
133 | market.Market\ | 139 | market.Market\ |
134 | .from_config(market_config, debug=args.debug, user_id=user_id, report_path=report_path)\ | 140 | .from_config(market_config, args, |
141 | pg_config=pg_config, market_id=market_id, | ||
142 | user_id=user_id, report_path=report_path)\ | ||
135 | .process(args.action, before=args.before, after=args.after) | 143 | .process(args.action, before=args.before, after=args.after) |
136 | except Exception as e: | 144 | except Exception as e: |
137 | print("{}: {}".format(e.__class__.__name__, e)) | 145 | print("{}: {}".format(e.__class__.__name__, e)) |
@@ -145,11 +153,13 @@ def main(argv): | |||
145 | import threading | 153 | import threading |
146 | market.Portfolio.start_worker() | 154 | market.Portfolio.start_worker() |
147 | 155 | ||
148 | for market_config, user_id in fetch_markets(pg_config, args.user): | 156 | def process_(*args): |
149 | threading.Thread(target=process, args=[market_config, user_id, report_path, args]).start() | 157 | threading.Thread(target=process, args=args).start() |
150 | else: | 158 | else: |
151 | for market_config, user_id in fetch_markets(pg_config, args.user): | 159 | process_ = process |
152 | process(market_config, user_id, report_path, args) | 160 | |
161 | for market_id, market_config, user_id in fetch_markets(pg_config, args.user): | ||
162 | process_(market_config, market_id, user_id, args, report_path, pg_config) | ||
153 | 163 | ||
154 | if __name__ == '__main__': # pragma: no cover | 164 | if __name__ == '__main__': # pragma: no cover |
155 | main(sys.argv[1:]) | 165 | main(sys.argv[1:]) |
@@ -1,6 +1,7 @@ | |||
1 | from ccxt import ExchangeError, NotSupported | 1 | from ccxt import ExchangeError, NotSupported |
2 | import ccxt_wrapper as ccxt | 2 | import ccxt_wrapper as ccxt |
3 | import time | 3 | import time |
4 | import psycopg2 | ||
4 | from store import * | 5 | from store import * |
5 | from cachetools.func import ttl_cache | 6 | from cachetools.func import ttl_cache |
6 | from datetime import datetime | 7 | from datetime import datetime |
@@ -13,20 +14,21 @@ class Market: | |||
13 | trades = None | 14 | trades = None |
14 | balances = None | 15 | balances = None |
15 | 16 | ||
16 | def __init__(self, ccxt_instance, debug=False, user_id=None, report_path=None): | 17 | def __init__(self, ccxt_instance, args, **kwargs): |
17 | self.debug = debug | 18 | self.args = args |
19 | self.debug = args.debug | ||
18 | self.ccxt = ccxt_instance | 20 | self.ccxt = ccxt_instance |
19 | self.ccxt._market = self | 21 | self.ccxt._market = self |
20 | self.report = ReportStore(self) | 22 | self.report = ReportStore(self, verbose_print=(not args.quiet)) |
21 | self.trades = TradeStore(self) | 23 | self.trades = TradeStore(self) |
22 | self.balances = BalanceStore(self) | 24 | self.balances = BalanceStore(self) |
23 | self.processor = Processor(self) | 25 | self.processor = Processor(self) |
24 | 26 | ||
25 | self.user_id = user_id | 27 | for key in ["user_id", "market_id", "report_path", "pg_config"]: |
26 | self.report_path = report_path | 28 | setattr(self, key, kwargs.get(key, None)) |
27 | 29 | ||
28 | @classmethod | 30 | @classmethod |
29 | def from_config(cls, config, debug=False, user_id=None, report_path=None): | 31 | def from_config(cls, config, args, **kwargs): |
30 | config["apiKey"] = config.pop("key", None) | 32 | config["apiKey"] = config.pop("key", None) |
31 | 33 | ||
32 | ccxt_instance = ccxt.poloniexE(config) | 34 | ccxt_instance = ccxt.poloniexE(config) |
@@ -43,20 +45,43 @@ class Market: | |||
43 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, | 45 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, |
44 | ccxt_instance.session.__class__) | 46 | ccxt_instance.session.__class__) |
45 | 47 | ||
46 | return cls(ccxt_instance, debug=debug, user_id=user_id, report_path=report_path) | 48 | return cls(ccxt_instance, args, **kwargs) |
47 | 49 | ||
48 | def store_report(self): | 50 | def store_report(self): |
49 | self.report.merge(Portfolio.report) | 51 | self.report.merge(Portfolio.report) |
52 | date = datetime.now() | ||
53 | if self.report_path is not None: | ||
54 | self.store_file_report(date) | ||
55 | if self.pg_config is not None: | ||
56 | self.store_database_report(date) | ||
57 | |||
58 | def store_file_report(self, date): | ||
50 | try: | 59 | try: |
51 | if self.report_path is not None: | 60 | report_file = "{}/{}_{}".format(self.report_path, date.isoformat(), self.user_id) |
52 | report_file = "{}/{}_{}".format(self.report_path, datetime.now().isoformat(), self.user_id) | 61 | with open(report_file + ".json", "w") as f: |
53 | with open(report_file + ".json", "w") as f: | 62 | f.write(self.report.to_json()) |
54 | f.write(self.report.to_json()) | 63 | with open(report_file + ".log", "w") as f: |
55 | with open(report_file + ".log", "w") as f: | 64 | f.write("\n".join(map(lambda x: x[1], self.report.print_logs))) |
56 | f.write("\n".join(map(lambda x: x[1], self.report.print_logs))) | ||
57 | except Exception as e: | 65 | except Exception as e: |
58 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) | 66 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) |
59 | 67 | ||
68 | def store_database_report(self, date): | ||
69 | try: | ||
70 | report_query = 'INSERT INTO reports("date", "market_config_id", "debug") VALUES (%s, %s, %s) RETURNING id;' | ||
71 | line_query = 'INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);' | ||
72 | connection = psycopg2.connect(**self.pg_config) | ||
73 | cursor = connection.cursor() | ||
74 | cursor.execute(report_query, (date, self.market_id, self.debug)) | ||
75 | report_id = cursor.fetchone()[0] | ||
76 | for date, type_, payload in self.report.to_json_array(): | ||
77 | cursor.execute(line_query, (date, report_id, type_, payload)) | ||
78 | |||
79 | connection.commit() | ||
80 | cursor.close() | ||
81 | connection.close() | ||
82 | except Exception as e: | ||
83 | print("impossible to store report to database: {}; {}".format(e.__class__.__name__, e)) | ||
84 | |||
60 | def process(self, actions, before=False, after=False): | 85 | def process(self, actions, before=False, after=False): |
61 | try: | 86 | try: |
62 | if len(actions or []) == 0: | 87 | if len(actions or []) == 0: |
@@ -36,12 +36,22 @@ class ReportStore: | |||
36 | hash_["date"] = datetime.now() | 36 | hash_["date"] = datetime.now() |
37 | self.logs.append(hash_) | 37 | self.logs.append(hash_) |
38 | 38 | ||
39 | @staticmethod | ||
40 | def default_json_serial(obj): | ||
41 | if isinstance(obj, (datetime, date)): | ||
42 | return obj.isoformat() | ||
43 | return str(obj) | ||
44 | |||
39 | def to_json(self): | 45 | def to_json(self): |
40 | def default_json_serial(obj): | 46 | return json.dumps(self.logs, default=self.default_json_serial, indent=" ") |
41 | if isinstance(obj, (datetime, date)): | 47 | |
42 | return obj.isoformat() | 48 | def to_json_array(self): |
43 | return str(obj) | 49 | for log in (x.copy() for x in self.logs): |
44 | return json.dumps(self.logs, default=default_json_serial, indent=" ") | 50 | yield ( |
51 | log.pop("date"), | ||
52 | log.pop("type"), | ||
53 | json.dumps(log, default=self.default_json_serial, indent=" ") | ||
54 | ) | ||
45 | 55 | ||
46 | def set_verbose(self, verbose_print): | 56 | def set_verbose(self, verbose_print): |
47 | self.verbose_print = verbose_print | 57 | self.verbose_print = verbose_print |
diff --git a/tasks/import_reports_to_database.py b/tasks/import_reports_to_database.py new file mode 100644 index 0000000..152c762 --- /dev/null +++ b/tasks/import_reports_to_database.py | |||
@@ -0,0 +1,50 @@ | |||
1 | import sys | ||
2 | import os | ||
3 | import simplejson as json | ||
4 | from datetime import datetime | ||
5 | from decimal import Decimal as D | ||
6 | import psycopg2 | ||
7 | |||
8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
9 | from main import parse_config | ||
10 | |||
11 | config = sys.argv[1] | ||
12 | reports = sys.argv[2:] | ||
13 | |||
14 | pg_config, report_path = parse_config(config) | ||
15 | |||
16 | connection = psycopg2.connect(**pg_config) | ||
17 | cursor = connection.cursor() | ||
18 | |||
19 | report_query = 'INSERT INTO reports("date", "market_config_id", "debug") VALUES (%s, %s, %s) RETURNING id;' | ||
20 | line_query = 'INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);' | ||
21 | |||
22 | user_id_to_market_id = { | ||
23 | 2: 1, | ||
24 | 1: 3, | ||
25 | } | ||
26 | |||
27 | for report in reports: | ||
28 | with open(report, "rb") as f: | ||
29 | json_content = json.load(f, parse_float=D) | ||
30 | basename = os.path.basename(report) | ||
31 | date, rest = basename.split("_", 1) | ||
32 | user_id, rest = rest.split(".", 1) | ||
33 | |||
34 | date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f") | ||
35 | market_id = user_id_to_market_id[int(user_id)] | ||
36 | debug = any("debug" in x and x["debug"] for x in json_content) | ||
37 | print(market_id, date, debug) | ||
38 | cursor.execute(report_query, (date, market_id, debug)) | ||
39 | report_id = cursor.fetchone()[0] | ||
40 | |||
41 | for line in json_content: | ||
42 | date = datetime.strptime(line["date"], "%Y-%m-%dT%H:%M:%S.%f") | ||
43 | type_ = line["type"] | ||
44 | del(line["date"]) | ||
45 | del(line["type"]) | ||
46 | |||
47 | cursor.execute(line_query, (date, report_id, type_, json.dumps(line))) | ||
48 | connection.commit() | ||
49 | cursor.close() | ||
50 | connection.close() | ||
@@ -23,6 +23,9 @@ for test_type in limits: | |||
23 | class WebMockTestCase(unittest.TestCase): | 23 | class WebMockTestCase(unittest.TestCase): |
24 | import time | 24 | import time |
25 | 25 | ||
26 | def market_args(self, debug=False, quiet=False): | ||
27 | return type('Args', (object,), { "debug": debug, "quiet": quiet })() | ||
28 | |||
26 | def setUp(self): | 29 | def setUp(self): |
27 | super(WebMockTestCase, self).setUp() | 30 | super(WebMockTestCase, self).setUp() |
28 | self.wm = requests_mock.Mocker() | 31 | self.wm = requests_mock.Mocker() |
@@ -1092,7 +1095,7 @@ class MarketTest(WebMockTestCase): | |||
1092 | self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 1095 | self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
1093 | 1096 | ||
1094 | def test_values(self): | 1097 | def test_values(self): |
1095 | m = market.Market(self.ccxt) | 1098 | m = market.Market(self.ccxt, self.market_args()) |
1096 | 1099 | ||
1097 | self.assertEqual(self.ccxt, m.ccxt) | 1100 | self.assertEqual(self.ccxt, m.ccxt) |
1098 | self.assertFalse(m.debug) | 1101 | self.assertFalse(m.debug) |
@@ -1104,19 +1107,27 @@ class MarketTest(WebMockTestCase): | |||
1104 | self.assertEqual(m, m.balances.market) | 1107 | self.assertEqual(m, m.balances.market) |
1105 | self.assertEqual(m, m.ccxt._market) | 1108 | self.assertEqual(m, m.ccxt._market) |
1106 | 1109 | ||
1107 | m = market.Market(self.ccxt, debug=True) | 1110 | m = market.Market(self.ccxt, self.market_args(debug=True)) |
1108 | self.assertTrue(m.debug) | 1111 | self.assertTrue(m.debug) |
1109 | 1112 | ||
1110 | m = market.Market(self.ccxt, debug=False) | 1113 | m = market.Market(self.ccxt, self.market_args(debug=False)) |
1111 | self.assertFalse(m.debug) | 1114 | self.assertFalse(m.debug) |
1112 | 1115 | ||
1116 | with mock.patch("market.ReportStore") as report_store: | ||
1117 | with self.subTest(quiet=False): | ||
1118 | m = market.Market(self.ccxt, self.market_args(quiet=False)) | ||
1119 | report_store.assert_called_with(m, verbose_print=True) | ||
1120 | with self.subTest(quiet=True): | ||
1121 | m = market.Market(self.ccxt, self.market_args(quiet=True)) | ||
1122 | report_store.assert_called_with(m, verbose_print=False) | ||
1123 | |||
1113 | @mock.patch("market.ccxt") | 1124 | @mock.patch("market.ccxt") |
1114 | def test_from_config(self, ccxt): | 1125 | def test_from_config(self, ccxt): |
1115 | with mock.patch("market.ReportStore"): | 1126 | with mock.patch("market.ReportStore"): |
1116 | ccxt.poloniexE.return_value = self.ccxt | 1127 | ccxt.poloniexE.return_value = self.ccxt |
1117 | self.ccxt.session.request.return_value = "response" | 1128 | self.ccxt.session.request.return_value = "response" |
1118 | 1129 | ||
1119 | m = market.Market.from_config({"key": "key", "secred": "secret"}) | 1130 | m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args()) |
1120 | 1131 | ||
1121 | self.assertEqual(self.ccxt, m.ccxt) | 1132 | self.assertEqual(self.ccxt, m.ccxt) |
1122 | 1133 | ||
@@ -1125,7 +1136,7 @@ class MarketTest(WebMockTestCase): | |||
1125 | m.report.log_http_request.assert_called_with('GET', 'URL', 'data', | 1136 | m.report.log_http_request.assert_called_with('GET', 'URL', 'data', |
1126 | 'headers', 'response') | 1137 | 'headers', 'response') |
1127 | 1138 | ||
1128 | m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True) | 1139 | m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args(debug=True)) |
1129 | self.assertEqual(True, m.debug) | 1140 | self.assertEqual(True, m.debug) |
1130 | 1141 | ||
1131 | def test_get_tickers(self): | 1142 | def test_get_tickers(self): |
@@ -1134,7 +1145,7 @@ class MarketTest(WebMockTestCase): | |||
1134 | market.NotSupported | 1145 | market.NotSupported |
1135 | ] | 1146 | ] |
1136 | 1147 | ||
1137 | m = market.Market(self.ccxt) | 1148 | m = market.Market(self.ccxt, self.market_args()) |
1138 | self.assertEqual("tickers", m.get_tickers()) | 1149 | self.assertEqual("tickers", m.get_tickers()) |
1139 | self.assertEqual("tickers", m.get_tickers()) | 1150 | self.assertEqual("tickers", m.get_tickers()) |
1140 | self.ccxt.fetch_tickers.assert_called_once() | 1151 | self.ccxt.fetch_tickers.assert_called_once() |
@@ -1147,7 +1158,7 @@ class MarketTest(WebMockTestCase): | |||
1147 | "ETH/ETC": { "bid": 1, "ask": 3 }, | 1158 | "ETH/ETC": { "bid": 1, "ask": 3 }, |
1148 | "XVG/ETH": { "bid": 10, "ask": 40 }, | 1159 | "XVG/ETH": { "bid": 10, "ask": 40 }, |
1149 | } | 1160 | } |
1150 | m = market.Market(self.ccxt) | 1161 | m = market.Market(self.ccxt, self.market_args()) |
1151 | 1162 | ||
1152 | ticker = m.get_ticker("ETH", "ETC") | 1163 | ticker = m.get_ticker("ETH", "ETC") |
1153 | self.assertEqual(1, ticker["bid"]) | 1164 | self.assertEqual(1, ticker["bid"]) |
@@ -1175,7 +1186,7 @@ class MarketTest(WebMockTestCase): | |||
1175 | market.ExchangeError("foo"), | 1186 | market.ExchangeError("foo"), |
1176 | ] | 1187 | ] |
1177 | 1188 | ||
1178 | m = market.Market(self.ccxt) | 1189 | m = market.Market(self.ccxt, self.market_args()) |
1179 | 1190 | ||
1180 | ticker = m.get_ticker("ETH", "ETC") | 1191 | ticker = m.get_ticker("ETH", "ETC") |
1181 | self.ccxt.fetch_ticker.assert_called_with("ETH/ETC") | 1192 | self.ccxt.fetch_ticker.assert_called_with("ETH/ETC") |
@@ -1195,7 +1206,7 @@ class MarketTest(WebMockTestCase): | |||
1195 | self.assertIsNone(ticker) | 1206 | self.assertIsNone(ticker) |
1196 | 1207 | ||
1197 | def test_fetch_fees(self): | 1208 | def test_fetch_fees(self): |
1198 | m = market.Market(self.ccxt) | 1209 | m = market.Market(self.ccxt, self.market_args()) |
1199 | self.ccxt.fetch_fees.return_value = "Foo" | 1210 | self.ccxt.fetch_fees.return_value = "Foo" |
1200 | self.assertEqual("Foo", m.fetch_fees()) | 1211 | self.assertEqual("Foo", m.fetch_fees()) |
1201 | self.ccxt.fetch_fees.assert_called_once() | 1212 | self.ccxt.fetch_fees.assert_called_once() |
@@ -1222,7 +1233,7 @@ class MarketTest(WebMockTestCase): | |||
1222 | get_ticker.side_effect = _get_ticker | 1233 | get_ticker.side_effect = _get_ticker |
1223 | 1234 | ||
1224 | with mock.patch("market.ReportStore"): | 1235 | with mock.patch("market.ReportStore"): |
1225 | m = market.Market(self.ccxt) | 1236 | m = market.Market(self.ccxt, self.market_args()) |
1226 | self.ccxt.fetch_all_balances.return_value = { | 1237 | self.ccxt.fetch_all_balances.return_value = { |
1227 | "USDT": { | 1238 | "USDT": { |
1228 | "exchange_free": D("10000.0"), | 1239 | "exchange_free": D("10000.0"), |
@@ -1262,7 +1273,7 @@ class MarketTest(WebMockTestCase): | |||
1262 | (False, 12), (True, 12)]: | 1273 | (False, 12), (True, 12)]: |
1263 | with self.subTest(sleep=sleep, debug=debug), \ | 1274 | with self.subTest(sleep=sleep, debug=debug), \ |
1264 | mock.patch("market.ReportStore"): | 1275 | mock.patch("market.ReportStore"): |
1265 | m = market.Market(self.ccxt, debug=debug) | 1276 | m = market.Market(self.ccxt, self.market_args(debug=debug)) |
1266 | 1277 | ||
1267 | order_mock1 = mock.Mock() | 1278 | order_mock1 = mock.Mock() |
1268 | order_mock2 = mock.Mock() | 1279 | order_mock2 = mock.Mock() |
@@ -1339,7 +1350,7 @@ class MarketTest(WebMockTestCase): | |||
1339 | for debug in [True, False]: | 1350 | for debug in [True, False]: |
1340 | with self.subTest(debug=debug),\ | 1351 | with self.subTest(debug=debug),\ |
1341 | mock.patch("market.ReportStore"): | 1352 | mock.patch("market.ReportStore"): |
1342 | m = market.Market(self.ccxt, debug=debug) | 1353 | m = market.Market(self.ccxt, self.market_args(debug=debug)) |
1343 | 1354 | ||
1344 | value_from = portfolio.Amount("BTC", "1.0") | 1355 | value_from = portfolio.Amount("BTC", "1.0") |
1345 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1356 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
@@ -1375,54 +1386,135 @@ class MarketTest(WebMockTestCase): | |||
1375 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") | 1386 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") |
1376 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") | 1387 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") |
1377 | 1388 | ||
1378 | def test_store_report(self): | 1389 | def test_store_file_report(self): |
1379 | |||
1380 | file_open = mock.mock_open() | ||
1381 | m = market.Market(self.ccxt, user_id=1) | ||
1382 | with self.subTest(file=None),\ | ||
1383 | mock.patch.object(m, "report") as report,\ | ||
1384 | mock.patch("market.open", file_open): | ||
1385 | m.store_report() | ||
1386 | report.merge.assert_called_with(store.Portfolio.report) | ||
1387 | file_open.assert_not_called() | ||
1388 | |||
1389 | report.reset_mock() | ||
1390 | file_open = mock.mock_open() | 1390 | file_open = mock.mock_open() |
1391 | m = market.Market(self.ccxt, report_path="present", user_id=1) | 1391 | m = market.Market(self.ccxt, self.market_args(), report_path="present", user_id=1) |
1392 | with self.subTest(file="present"),\ | 1392 | with self.subTest(file="present"),\ |
1393 | mock.patch("market.open", file_open),\ | 1393 | mock.patch("market.open", file_open),\ |
1394 | mock.patch.object(m, "report") as report,\ | 1394 | mock.patch.object(m, "report") as report,\ |
1395 | mock.patch.object(market, "datetime") as time_mock: | 1395 | mock.patch.object(market, "datetime") as time_mock: |
1396 | 1396 | ||
1397 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1398 | report.print_logs = [[time_mock.now(), "Foo"], [time_mock.now(), "Bar"]] | 1397 | report.print_logs = [[time_mock.now(), "Foo"], [time_mock.now(), "Bar"]] |
1399 | report.to_json.return_value = "json_content" | 1398 | report.to_json.return_value = "json_content" |
1400 | 1399 | ||
1401 | m.store_report() | 1400 | m.store_file_report(datetime.datetime(2018, 2, 25)) |
1402 | 1401 | ||
1403 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | 1402 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") |
1404 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.log", "w") | 1403 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.log", "w") |
1405 | file_open().write.assert_any_call("json_content") | 1404 | file_open().write.assert_any_call("json_content") |
1406 | file_open().write.assert_any_call("Foo\nBar") | 1405 | file_open().write.assert_any_call("Foo\nBar") |
1407 | m.report.to_json.assert_called_once_with() | 1406 | m.report.to_json.assert_called_once_with() |
1408 | report.merge.assert_called_with(store.Portfolio.report) | ||
1409 | 1407 | ||
1410 | report.reset_mock() | 1408 | m = market.Market(self.ccxt, self.market_args(), report_path="error", user_id=1) |
1411 | |||
1412 | m = market.Market(self.ccxt, report_path="error", user_id=1) | ||
1413 | with self.subTest(file="error"),\ | 1409 | with self.subTest(file="error"),\ |
1414 | mock.patch("market.open") as file_open,\ | 1410 | mock.patch("market.open") as file_open,\ |
1415 | mock.patch.object(m, "report") as report,\ | 1411 | mock.patch.object(m, "report") as report,\ |
1416 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 1412 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
1417 | file_open.side_effect = FileNotFoundError | 1413 | file_open.side_effect = FileNotFoundError |
1418 | 1414 | ||
1415 | m.store_file_report(datetime.datetime(2018, 2, 25)) | ||
1416 | |||
1417 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | ||
1418 | |||
1419 | @mock.patch.object(market, "psycopg2") | ||
1420 | def test_store_database_report(self, psycopg2): | ||
1421 | connect_mock = mock.Mock() | ||
1422 | cursor_mock = mock.MagicMock() | ||
1423 | |||
1424 | connect_mock.cursor.return_value = cursor_mock | ||
1425 | psycopg2.connect.return_value = connect_mock | ||
1426 | m = market.Market(self.ccxt, self.market_args(), | ||
1427 | pg_config={"config": "pg_config"}, user_id=1) | ||
1428 | cursor_mock.fetchone.return_value = [42] | ||
1429 | |||
1430 | with self.subTest(error=False),\ | ||
1431 | mock.patch.object(m, "report") as report: | ||
1432 | report.to_json_array.return_value = [ | ||
1433 | ("date1", "type1", "payload1"), | ||
1434 | ("date2", "type2", "payload2"), | ||
1435 | ] | ||
1436 | m.store_database_report(datetime.datetime(2018, 3, 24)) | ||
1437 | connect_mock.assert_has_calls([ | ||
1438 | mock.call.cursor(), | ||
1439 | mock.call.cursor().execute('INSERT INTO reports("date", "market_config_id", "debug") VALUES (%s, %s, %s) RETURNING id;', (datetime.datetime(2018, 3, 24), None, False)), | ||
1440 | mock.call.cursor().fetchone(), | ||
1441 | mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date1', 42, 'type1', 'payload1')), | ||
1442 | mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date2', 42, 'type2', 'payload2')), | ||
1443 | mock.call.commit(), | ||
1444 | mock.call.cursor().close(), | ||
1445 | mock.call.close() | ||
1446 | ]) | ||
1447 | |||
1448 | connect_mock.reset_mock() | ||
1449 | with self.subTest(error=True),\ | ||
1450 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
1451 | psycopg2.connect.side_effect = Exception("Bouh") | ||
1452 | m.store_database_report(datetime.datetime(2018, 3, 24)) | ||
1453 | self.assertEqual(stdout_mock.getvalue(), "impossible to store report to database: Exception; Bouh\n") | ||
1454 | |||
1455 | def test_store_report(self): | ||
1456 | m = market.Market(self.ccxt, self.market_args(), user_id=1) | ||
1457 | with self.subTest(file=None, pg_config=None),\ | ||
1458 | mock.patch.object(m, "report") as report,\ | ||
1459 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1460 | mock.patch.object(m, "store_file_report") as file_report: | ||
1461 | m.store_report() | ||
1462 | report.merge.assert_called_with(store.Portfolio.report) | ||
1463 | |||
1464 | file_report.assert_not_called() | ||
1465 | db_report.assert_not_called() | ||
1466 | |||
1467 | report.reset_mock() | ||
1468 | m = market.Market(self.ccxt, self.market_args(), report_path="present", user_id=1) | ||
1469 | with self.subTest(file="present", pg_config=None),\ | ||
1470 | mock.patch.object(m, "report") as report,\ | ||
1471 | mock.patch.object(m, "store_file_report") as file_report,\ | ||
1472 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1473 | mock.patch.object(market, "datetime") as time_mock: | ||
1474 | |||
1475 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1476 | |||
1419 | m.store_report() | 1477 | m.store_report() |
1420 | 1478 | ||
1421 | report.merge.assert_called_with(store.Portfolio.report) | 1479 | report.merge.assert_called_with(store.Portfolio.report) |
1422 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | 1480 | file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) |
1481 | db_report.assert_not_called() | ||
1482 | |||
1483 | report.reset_mock() | ||
1484 | m = market.Market(self.ccxt, self.market_args(), pg_config="present", user_id=1) | ||
1485 | with self.subTest(file=None, pg_config="present"),\ | ||
1486 | mock.patch.object(m, "report") as report,\ | ||
1487 | mock.patch.object(m, "store_file_report") as file_report,\ | ||
1488 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1489 | mock.patch.object(market, "datetime") as time_mock: | ||
1490 | |||
1491 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1492 | |||
1493 | m.store_report() | ||
1494 | |||
1495 | report.merge.assert_called_with(store.Portfolio.report) | ||
1496 | file_report.assert_not_called() | ||
1497 | db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | ||
1498 | |||
1499 | report.reset_mock() | ||
1500 | m = market.Market(self.ccxt, self.market_args(), | ||
1501 | pg_config="pg_config", report_path="present", user_id=1) | ||
1502 | with self.subTest(file="present", pg_config="present"),\ | ||
1503 | mock.patch.object(m, "report") as report,\ | ||
1504 | mock.patch.object(m, "store_file_report") as file_report,\ | ||
1505 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1506 | mock.patch.object(market, "datetime") as time_mock: | ||
1507 | |||
1508 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1509 | |||
1510 | m.store_report() | ||
1511 | |||
1512 | report.merge.assert_called_with(store.Portfolio.report) | ||
1513 | file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | ||
1514 | db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | ||
1423 | 1515 | ||
1424 | def test_print_orders(self): | 1516 | def test_print_orders(self): |
1425 | m = market.Market(self.ccxt) | 1517 | m = market.Market(self.ccxt, self.market_args()) |
1426 | with mock.patch.object(m.report, "log_stage") as log_stage,\ | 1518 | with mock.patch.object(m.report, "log_stage") as log_stage,\ |
1427 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | 1519 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ |
1428 | mock.patch.object(m, "prepare_trades") as prepare_trades,\ | 1520 | mock.patch.object(m, "prepare_trades") as prepare_trades,\ |
@@ -1436,7 +1528,7 @@ class MarketTest(WebMockTestCase): | |||
1436 | prepare_orders.assert_called_with(compute_value="average") | 1528 | prepare_orders.assert_called_with(compute_value="average") |
1437 | 1529 | ||
1438 | def test_print_balances(self): | 1530 | def test_print_balances(self): |
1439 | m = market.Market(self.ccxt) | 1531 | m = market.Market(self.ccxt, self.market_args()) |
1440 | 1532 | ||
1441 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ | 1533 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ |
1442 | mock.patch.object(m.report, "log_stage") as log_stage,\ | 1534 | mock.patch.object(m.report, "log_stage") as log_stage,\ |
@@ -1461,7 +1553,7 @@ class MarketTest(WebMockTestCase): | |||
1461 | @mock.patch("market.ReportStore.log_error") | 1553 | @mock.patch("market.ReportStore.log_error") |
1462 | @mock.patch("market.Market.store_report") | 1554 | @mock.patch("market.Market.store_report") |
1463 | def test_process(self, store_report, log_error, process): | 1555 | def test_process(self, store_report, log_error, process): |
1464 | m = market.Market(self.ccxt) | 1556 | m = market.Market(self.ccxt, self.market_args()) |
1465 | with self.subTest(before=False, after=False): | 1557 | with self.subTest(before=False, after=False): |
1466 | m.process(None) | 1558 | m.process(None) |
1467 | 1559 | ||
@@ -3039,6 +3131,14 @@ class ReportStoreTest(WebMockTestCase): | |||
3039 | report_store.print_log(portfolio.Amount("BTC", 1)) | 3131 | report_store.print_log(portfolio.Amount("BTC", 1)) |
3040 | self.assertEqual(stdout_mock.getvalue(), "") | 3132 | self.assertEqual(stdout_mock.getvalue(), "") |
3041 | 3133 | ||
3134 | def test_default_json_serial(self): | ||
3135 | report_store = market.ReportStore(self.m) | ||
3136 | |||
3137 | self.assertEqual("2018-02-24T00:00:00", | ||
3138 | report_store.default_json_serial(portfolio.datetime(2018, 2, 24))) | ||
3139 | self.assertEqual("1.00000000 BTC", | ||
3140 | report_store.default_json_serial(portfolio.Amount("BTC", 1))) | ||
3141 | |||
3042 | def test_to_json(self): | 3142 | def test_to_json(self): |
3043 | report_store = market.ReportStore(self.m) | 3143 | report_store = market.ReportStore(self.m) |
3044 | report_store.logs.append({"foo": "bar"}) | 3144 | report_store.logs.append({"foo": "bar"}) |
@@ -3048,6 +3148,20 @@ class ReportStoreTest(WebMockTestCase): | |||
3048 | report_store.logs.append({"amount": portfolio.Amount("BTC", 1)}) | 3148 | report_store.logs.append({"amount": portfolio.Amount("BTC", 1)}) |
3049 | self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n },\n {\n "amount": "1.00000000 BTC"\n }\n]', report_store.to_json()) | 3149 | self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n },\n {\n "amount": "1.00000000 BTC"\n }\n]', report_store.to_json()) |
3050 | 3150 | ||
3151 | def test_to_json_array(self): | ||
3152 | report_store = market.ReportStore(self.m) | ||
3153 | report_store.logs.append({ | ||
3154 | "date": "date1", "type": "type1", "foo": "bar", "bla": "bla" | ||
3155 | }) | ||
3156 | report_store.logs.append({ | ||
3157 | "date": "date2", "type": "type2", "foo": "bar", "bla": "bla" | ||
3158 | }) | ||
3159 | logs = list(report_store.to_json_array()) | ||
3160 | |||
3161 | self.assertEqual(2, len(logs)) | ||
3162 | self.assertEqual(("date1", "type1", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[0]) | ||
3163 | self.assertEqual(("date2", "type2", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[1]) | ||
3164 | |||
3051 | @mock.patch.object(market.ReportStore, "print_log") | 3165 | @mock.patch.object(market.ReportStore, "print_log") |
3052 | @mock.patch.object(market.ReportStore, "add_log") | 3166 | @mock.patch.object(market.ReportStore, "add_log") |
3053 | def test_log_stage(self, add_log, print_log): | 3167 | def test_log_stage(self, add_log, print_log): |
@@ -3541,7 +3655,7 @@ class MainTest(WebMockTestCase): | |||
3541 | mock.patch("main.parse_config") as main_parse_config: | 3655 | mock.patch("main.parse_config") as main_parse_config: |
3542 | with self.subTest(debug=False): | 3656 | with self.subTest(debug=False): |
3543 | main_parse_config.return_value = ["pg_config", "report_path"] | 3657 | main_parse_config.return_value = ["pg_config", "report_path"] |
3544 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3658 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] |
3545 | m = main.get_user_market("config_path.ini", 1) | 3659 | m = main.get_user_market("config_path.ini", 1) |
3546 | 3660 | ||
3547 | self.assertIsInstance(m, market.Market) | 3661 | self.assertIsInstance(m, market.Market) |
@@ -3549,7 +3663,7 @@ class MainTest(WebMockTestCase): | |||
3549 | 3663 | ||
3550 | with self.subTest(debug=True): | 3664 | with self.subTest(debug=True): |
3551 | main_parse_config.return_value = ["pg_config", "report_path"] | 3665 | main_parse_config.return_value = ["pg_config", "report_path"] |
3552 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3666 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] |
3553 | m = main.get_user_market("config_path.ini", 1, debug=True) | 3667 | m = main.get_user_market("config_path.ini", 1, debug=True) |
3554 | 3668 | ||
3555 | self.assertIsInstance(m, market.Market) | 3669 | self.assertIsInstance(m, market.Market) |
@@ -3568,16 +3682,16 @@ class MainTest(WebMockTestCase): | |||
3568 | args_mock.after = "after" | 3682 | args_mock.after = "after" |
3569 | self.assertEqual("", stdout_mock.getvalue()) | 3683 | self.assertEqual("", stdout_mock.getvalue()) |
3570 | 3684 | ||
3571 | main.process("config", 1, "report_path", args_mock) | 3685 | main.process("config", 3, 1, args_mock, "report_path", "pg_config") |
3572 | 3686 | ||
3573 | market_mock.from_config.assert_has_calls([ | 3687 | market_mock.from_config.assert_has_calls([ |
3574 | mock.call("config", debug="debug", user_id=1, report_path="report_path"), | 3688 | mock.call("config", args_mock, pg_config="pg_config", market_id=3, user_id=1, report_path="report_path"), |
3575 | mock.call().process("action", before="before", after="after"), | 3689 | mock.call().process("action", before="before", after="after"), |
3576 | ]) | 3690 | ]) |
3577 | 3691 | ||
3578 | with self.subTest(exception=True): | 3692 | with self.subTest(exception=True): |
3579 | market_mock.from_config.side_effect = Exception("boo") | 3693 | market_mock.from_config.side_effect = Exception("boo") |
3580 | main.process("config", 1, "report_path", args_mock) | 3694 | main.process(3, "config", 1, "report_path", args_mock, "pg_config") |
3581 | self.assertEqual("Exception: boo\n", stdout_mock.getvalue()) | 3695 | self.assertEqual("Exception: boo\n", stdout_mock.getvalue()) |
3582 | 3696 | ||
3583 | def test_main(self): | 3697 | def test_main(self): |
@@ -3595,7 +3709,7 @@ class MainTest(WebMockTestCase): | |||
3595 | 3709 | ||
3596 | parse_config.return_value = ["pg_config", "report_path"] | 3710 | parse_config.return_value = ["pg_config", "report_path"] |
3597 | 3711 | ||
3598 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] | 3712 | fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]] |
3599 | 3713 | ||
3600 | main.main(["Foo", "Bar"]) | 3714 | main.main(["Foo", "Bar"]) |
3601 | 3715 | ||
@@ -3605,8 +3719,8 @@ class MainTest(WebMockTestCase): | |||
3605 | 3719 | ||
3606 | self.assertEqual(2, process.call_count) | 3720 | self.assertEqual(2, process.call_count) |
3607 | process.assert_has_calls([ | 3721 | process.assert_has_calls([ |
3608 | mock.call("config1", 1, "report_path", args_mock), | 3722 | mock.call("config1", 3, 1, args_mock, "report_path", "pg_config"), |
3609 | mock.call("config2", 2, "report_path", args_mock), | 3723 | mock.call("config2", 1, 2, args_mock, "report_path", "pg_config"), |
3610 | ]) | 3724 | ]) |
3611 | with self.subTest(parallel=True): | 3725 | with self.subTest(parallel=True): |
3612 | with mock.patch("main.parse_args") as parse_args,\ | 3726 | with mock.patch("main.parse_args") as parse_args,\ |
@@ -3623,7 +3737,7 @@ class MainTest(WebMockTestCase): | |||
3623 | 3737 | ||
3624 | parse_config.return_value = ["pg_config", "report_path"] | 3738 | parse_config.return_value = ["pg_config", "report_path"] |
3625 | 3739 | ||
3626 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] | 3740 | fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]] |
3627 | 3741 | ||
3628 | main.main(["Foo", "Bar"]) | 3742 | main.main(["Foo", "Bar"]) |
3629 | 3743 | ||
@@ -3635,9 +3749,9 @@ class MainTest(WebMockTestCase): | |||
3635 | self.assertEqual(2, process.call_count) | 3749 | self.assertEqual(2, process.call_count) |
3636 | process.assert_has_calls([ | 3750 | process.assert_has_calls([ |
3637 | mock.call.__bool__(), | 3751 | mock.call.__bool__(), |
3638 | mock.call("config1", 1, "report_path", args_mock), | 3752 | mock.call("config1", 3, 1, args_mock, "report_path", "pg_config"), |
3639 | mock.call.__bool__(), | 3753 | mock.call.__bool__(), |
3640 | mock.call("config2", 2, "report_path", args_mock), | 3754 | mock.call("config2", 1, 2, args_mock, "report_path", "pg_config"), |
3641 | ]) | 3755 | ]) |
3642 | 3756 | ||
3643 | @mock.patch.object(main.sys, "exit") | 3757 | @mock.patch.object(main.sys, "exit") |
@@ -3723,7 +3837,7 @@ class MainTest(WebMockTestCase): | |||
3723 | rows = list(main.fetch_markets({"foo": "bar"}, None)) | 3837 | rows = list(main.fetch_markets({"foo": "bar"}, None)) |
3724 | 3838 | ||
3725 | psycopg2.connect.assert_called_once_with(foo="bar") | 3839 | psycopg2.connect.assert_called_once_with(foo="bar") |
3726 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | 3840 | cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs") |
3727 | 3841 | ||
3728 | self.assertEqual(["row_1", "row_2"], rows) | 3842 | self.assertEqual(["row_1", "row_2"], rows) |
3729 | 3843 | ||
@@ -3733,7 +3847,7 @@ class MainTest(WebMockTestCase): | |||
3733 | rows = list(main.fetch_markets({"foo": "bar"}, 1)) | 3847 | rows = list(main.fetch_markets({"foo": "bar"}, 1)) |
3734 | 3848 | ||
3735 | psycopg2.connect.assert_called_once_with(foo="bar") | 3849 | psycopg2.connect.assert_called_once_with(foo="bar") |
3736 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | 3850 | cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs WHERE user_id = %s", 1) |
3737 | 3851 | ||
3738 | self.assertEqual(["row_1", "row_2"], rows) | 3852 | self.assertEqual(["row_1", "row_2"], rows) |
3739 | 3853 | ||
@@ -3797,7 +3911,7 @@ class ProcessorTest(WebMockTestCase): | |||
3797 | 3911 | ||
3798 | def test_method_arguments(self): | 3912 | def test_method_arguments(self): |
3799 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 3913 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
3800 | m = market.Market(ccxt) | 3914 | m = market.Market(ccxt, self.market_args()) |
3801 | 3915 | ||
3802 | processor = market.Processor(m) | 3916 | processor = market.Processor(m) |
3803 | 3917 | ||