Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdf9d074c8 | |||
| 2c53efb4f3 | |||
| 553edab0db | |||
| 675412c125 | |||
| be9ef2b54f |
+10
-4
@@ -1,9 +1,11 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
||||||
import requests
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from qsforex.event.event import TickEvent
|
from qsforex.event.event import TickEvent
|
||||||
from qsforex.data.price import PriceHandler
|
from qsforex.data.price import PriceHandler
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ class StreamingForexPrices(PriceHandler):
|
|||||||
self.events_queue = events_queue
|
self.events_queue = events_queue
|
||||||
self.pairs = pairs
|
self.pairs = pairs
|
||||||
self.prices = self._set_up_prices_dict()
|
self.prices = self._set_up_prices_dict()
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def invert_prices(self, pair, bid, ask):
|
def invert_prices(self, pair, bid, ask):
|
||||||
"""
|
"""
|
||||||
@@ -38,12 +41,13 @@ class StreamingForexPrices(PriceHandler):
|
|||||||
|
|
||||||
def connect_to_stream(self):
|
def connect_to_stream(self):
|
||||||
pairs_oanda = ["%s_%s" % (p[:3], p[3:]) for p in self.pairs]
|
pairs_oanda = ["%s_%s" % (p[:3], p[3:]) for p in self.pairs]
|
||||||
|
pair_list = ",".join(pairs_oanda)
|
||||||
try:
|
try:
|
||||||
requests.packages.urllib3.disable_warnings()
|
requests.packages.urllib3.disable_warnings()
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
url = "https://" + self.domain + "/v1/prices"
|
url = "https://" + self.domain + "/v1/prices"
|
||||||
headers = {'Authorization' : 'Bearer ' + self.access_token}
|
headers = {'Authorization' : 'Bearer ' + self.access_token}
|
||||||
params = {'instruments' : pairs_oanda, 'accountId' : self.account_id}
|
params = {'instruments' : pair_list, 'accountId' : self.account_id}
|
||||||
req = requests.Request('GET', url, headers=headers, params=params)
|
req = requests.Request('GET', url, headers=headers, params=params)
|
||||||
pre = req.prepare()
|
pre = req.prepare()
|
||||||
resp = s.send(pre, stream=True, verify=False)
|
resp = s.send(pre, stream=True, verify=False)
|
||||||
@@ -62,10 +66,12 @@ class StreamingForexPrices(PriceHandler):
|
|||||||
dline = line.decode('utf-8')
|
dline = line.decode('utf-8')
|
||||||
msg = json.loads(dline)
|
msg = json.loads(dline)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Caught exception when converting message into json\n" + str(e))
|
self.logger.error(
|
||||||
|
"Caught exception when converting message into json: %s" % str(e)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if "instrument" in msg or "tick" in msg:
|
if "instrument" in msg or "tick" in msg:
|
||||||
print(msg)
|
self.logger.debug(msg)
|
||||||
getcontext().rounding = ROUND_HALF_DOWN
|
getcontext().rounding = ROUND_HALF_DOWN
|
||||||
instrument = msg["tick"]["instrument"].replace("_", "")
|
instrument = msg["tick"]["instrument"].replace("_", "")
|
||||||
time = msg["tick"]["time"]
|
time = msg["tick"]["time"]
|
||||||
|
|||||||
+29
-2
@@ -10,15 +10,33 @@ class TickEvent(Event):
|
|||||||
self.bid = bid
|
self.bid = bid
|
||||||
self.ask = ask
|
self.ask = ask
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Type: %s, Instrument: %s, Time: %s, Bid: %s, Ask: %s" % (
|
||||||
|
str(self.type), str(self.instrument),
|
||||||
|
str(self.time), str(self.bid), str(self.ask)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
class SignalEvent(Event):
|
class SignalEvent(Event):
|
||||||
def __init__(self, instrument, order_type, side, time):
|
def __init__(self, instrument, order_type, side, time):
|
||||||
self.type = 'SIGNAL'
|
self.type = 'SIGNAL'
|
||||||
self.instrument = instrument
|
self.instrument = instrument
|
||||||
self.order_type = order_type
|
self.order_type = order_type
|
||||||
self.side = side
|
self.side = side
|
||||||
self.time = time # Time of the last tick that generated the signal
|
self.time = time # Time of the last tick that generated the signal
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Type: %s, Instrument: %s, Order Type: %s, Side: %s" % (
|
||||||
|
str(self.type), str(self.instrument),
|
||||||
|
str(self.order_type), str(self.side)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
class OrderEvent(Event):
|
class OrderEvent(Event):
|
||||||
def __init__(self, instrument, units, order_type, side):
|
def __init__(self, instrument, units, order_type, side):
|
||||||
@@ -26,4 +44,13 @@ class OrderEvent(Event):
|
|||||||
self.instrument = instrument
|
self.instrument = instrument
|
||||||
self.units = units
|
self.units = units
|
||||||
self.order_type = order_type
|
self.order_type = order_type
|
||||||
self.side = side
|
self.side = side
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Type: %s, Instrument: %s, Units: %s, Order Type: %s, Side: %s" % (
|
||||||
|
str(self.type), str(self.instrument), str(self.units),
|
||||||
|
str(self.order_type), str(self.side)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
@@ -5,6 +5,7 @@ try:
|
|||||||
import httplib
|
import httplib
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import http.client as httplib
|
import http.client as httplib
|
||||||
|
import logging
|
||||||
try:
|
try:
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -47,6 +48,7 @@ class OANDAExecutionHandler(ExecutionHandler):
|
|||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
self.account_id = account_id
|
self.account_id = account_id
|
||||||
self.conn = self.obtain_connection()
|
self.conn = self.obtain_connection()
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def obtain_connection(self):
|
def obtain_connection(self):
|
||||||
return httplib.HTTPSConnection(self.domain)
|
return httplib.HTTPSConnection(self.domain)
|
||||||
@@ -68,6 +70,6 @@ class OANDAExecutionHandler(ExecutionHandler):
|
|||||||
"/v1/accounts/%s/orders" % str(self.account_id),
|
"/v1/accounts/%s/orders" % str(self.account_id),
|
||||||
params, headers
|
params, headers
|
||||||
)
|
)
|
||||||
response = self.conn.getresponse().read()
|
response = self.conn.getresponse().read().decode("utf-8").replace("\n","").replace("\t","")
|
||||||
print(response)
|
self.logger.debug(response)
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[loggers]
|
||||||
|
keys=root,qsforex.trading.trading
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys=consoleHandler
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys=simpleFormatter
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level=DEBUG
|
||||||
|
handlers=consoleHandler
|
||||||
|
|
||||||
|
[logger_qsforex.trading.trading]
|
||||||
|
level=DEBUG
|
||||||
|
handlers=consoleHandler
|
||||||
|
qualname=qsforex.trading.trading
|
||||||
|
propagate=0
|
||||||
|
|
||||||
|
[handler_consoleHandler]
|
||||||
|
class=StreamHandler
|
||||||
|
level=DEBUG
|
||||||
|
formatter=simpleFormatter
|
||||||
|
args=(sys.stdout,)
|
||||||
|
|
||||||
|
[formatter_simpleFormatter]
|
||||||
|
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||||
|
datefmt=
|
||||||
+61
-44
@@ -2,6 +2,7 @@ from __future__ import print_function
|
|||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -30,6 +31,7 @@ class Portfolio(object):
|
|||||||
self.positions = {}
|
self.positions = {}
|
||||||
if self.backtest:
|
if self.backtest:
|
||||||
self.backtest_file = self.create_equity_file()
|
self.backtest_file = self.create_equity_file()
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def calc_risk_position_size(self):
|
def calc_risk_position_size(self):
|
||||||
return self.equity * self.risk_per_trade
|
return self.equity * self.risk_per_trade
|
||||||
@@ -126,51 +128,66 @@ class Portfolio(object):
|
|||||||
print(out_line[:-2])
|
print(out_line[:-2])
|
||||||
self.backtest_file.write(out_line)
|
self.backtest_file.write(out_line)
|
||||||
|
|
||||||
def execute_signal(self, signal_event):
|
def execute_signal(self, signal_event):
|
||||||
side = signal_event.side
|
# Check that the prices ticker contains all necessary
|
||||||
currency_pair = signal_event.instrument
|
# currency pairs prior to executing an order
|
||||||
units = int(self.trade_units)
|
execute = True
|
||||||
time = signal_event.time
|
tp = self.ticker.prices
|
||||||
|
for pair in tp:
|
||||||
# If there is no position, create one
|
if tp[pair]["ask"] is None or tp[pair]["bid"] is None:
|
||||||
if currency_pair not in self.positions:
|
execute = False
|
||||||
if side == "buy":
|
|
||||||
position_type = "long"
|
# All necessary pricing data is available,
|
||||||
|
# we can execute
|
||||||
|
if execute:
|
||||||
|
side = signal_event.side
|
||||||
|
currency_pair = signal_event.instrument
|
||||||
|
units = int(self.trade_units)
|
||||||
|
time = signal_event.time
|
||||||
|
|
||||||
|
# If there is no position, create one
|
||||||
|
if currency_pair not in self.positions:
|
||||||
|
if side == "buy":
|
||||||
|
position_type = "long"
|
||||||
|
else:
|
||||||
|
position_type = "short"
|
||||||
|
self.add_new_position(
|
||||||
|
position_type, currency_pair,
|
||||||
|
units, self.ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
# If a position exists add or remove units
|
||||||
else:
|
else:
|
||||||
position_type = "short"
|
ps = self.positions[currency_pair]
|
||||||
self.add_new_position(
|
|
||||||
position_type, currency_pair,
|
|
||||||
units, self.ticker
|
|
||||||
)
|
|
||||||
|
|
||||||
# If a position exists add or remove units
|
if side == "buy" and ps.position_type == "long":
|
||||||
|
add_position_units(currency_pair, units)
|
||||||
|
|
||||||
|
elif side == "sell" and ps.position_type == "long":
|
||||||
|
if units == ps.units:
|
||||||
|
self.close_position(currency_pair)
|
||||||
|
# TODO: Allow units to be added/removed
|
||||||
|
elif units < ps.units:
|
||||||
|
return
|
||||||
|
elif units > ps.units:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif side == "buy" and ps.position_type == "short":
|
||||||
|
if units == ps.units:
|
||||||
|
self.close_position(currency_pair)
|
||||||
|
# TODO: Allow units to be added/removed
|
||||||
|
elif units < ps.units:
|
||||||
|
return
|
||||||
|
elif units > ps.units:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif side == "sell" and ps.position_type == "short":
|
||||||
|
add_position_units(currency_pair, units)
|
||||||
|
|
||||||
|
order = OrderEvent(currency_pair, units, "market", side)
|
||||||
|
self.events.put(order)
|
||||||
|
|
||||||
|
self.logger.info("Portfolio Balance: %s" % self.balance)
|
||||||
else:
|
else:
|
||||||
ps = self.positions[currency_pair]
|
self.logger.info("Unable to execute order as price data was insufficient.")
|
||||||
|
|
||||||
if side == "buy" and ps.position_type == "long":
|
|
||||||
add_position_units(currency_pair, units)
|
|
||||||
|
|
||||||
elif side == "sell" and ps.position_type == "long":
|
|
||||||
if units == ps.units:
|
|
||||||
self.close_position(currency_pair)
|
|
||||||
# TODO: Allow units to be added/removed
|
|
||||||
elif units < ps.units:
|
|
||||||
return
|
|
||||||
elif units > ps.units:
|
|
||||||
return
|
|
||||||
|
|
||||||
elif side == "buy" and ps.position_type == "short":
|
|
||||||
if units == ps.units:
|
|
||||||
self.close_position(currency_pair)
|
|
||||||
# TODO: Allow units to be added/removed
|
|
||||||
elif units < ps.units:
|
|
||||||
return
|
|
||||||
elif units > ps.units:
|
|
||||||
return
|
|
||||||
|
|
||||||
elif side == "sell" and ps.position_type == "short":
|
|
||||||
add_position_units(currency_pair, units)
|
|
||||||
|
|
||||||
order = OrderEvent(currency_pair, units, "market", side)
|
|
||||||
self.events.put(order)
|
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ class TestPortfolio(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(rpu)
|
self.assertTrue(rpu)
|
||||||
self.assertEqual(ps.units, Decimal("7000"))
|
self.assertEqual(ps.units, Decimal("7000"))
|
||||||
self.assertEqual(self.port.balance, Decimal("99988.84"))
|
self.assertEqual(self.port.balance, Decimal("99988.83"))
|
||||||
|
|
||||||
def test_close_position_long(self):
|
def test_close_position_long(self):
|
||||||
position_type = "long"
|
position_type = "long"
|
||||||
@@ -265,7 +265,7 @@ class TestPortfolio(unittest.TestCase):
|
|||||||
cp = self.port.close_position(currency_pair)
|
cp = self.port.close_position(currency_pair)
|
||||||
self.assertTrue(cp)
|
self.assertTrue(cp)
|
||||||
self.assertRaises(ps) # Key doesn't exist
|
self.assertRaises(ps) # Key doesn't exist
|
||||||
self.assertEqual(self.port.balance, Decimal("100026.64"))
|
self.assertEqual(self.port.balance, Decimal("100026.63"))
|
||||||
|
|
||||||
def test_close_position_short(self):
|
def test_close_position_short(self):
|
||||||
position_type = "short"
|
position_type = "short"
|
||||||
@@ -312,13 +312,13 @@ class TestPortfolio(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(rpu)
|
self.assertTrue(rpu)
|
||||||
self.assertEqual(ps.units, Decimal("7000"))
|
self.assertEqual(ps.units, Decimal("7000"))
|
||||||
self.assertEqual(self.port.balance, Decimal("99988.84"))
|
self.assertEqual(self.port.balance, Decimal("99988.83"))
|
||||||
|
|
||||||
# Close the position
|
# Close the position
|
||||||
cp = self.port.close_position(currency_pair)
|
cp = self.port.close_position(currency_pair)
|
||||||
self.assertTrue(cp)
|
self.assertTrue(cp)
|
||||||
self.assertRaises(ps) # Key doesn't exist
|
self.assertRaises(ps) # Key doesn't exist
|
||||||
self.assertEqual(self.port.balance, Decimal("99962.80"))
|
self.assertEqual(self.port.balance, Decimal("99962.77"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class Position(object):
|
|||||||
ticker_cur = self.ticker.prices[self.currency_pair]
|
ticker_cur = self.ticker.prices[self.currency_pair]
|
||||||
if self.position_type == "long":
|
if self.position_type == "long":
|
||||||
self.avg_price = Decimal(str(ticker_cur["ask"]))
|
self.avg_price = Decimal(str(ticker_cur["ask"]))
|
||||||
self.cur_price = Decimal(str(ticker_cur["bid"]))
|
self.cur_price = Decimal(str(ticker_cur["bid"]))
|
||||||
else:
|
else:
|
||||||
self.avg_price = Decimal(str(ticker_cur["bid"]))
|
self.avg_price = Decimal(str(ticker_cur["bid"]))
|
||||||
self.cur_price = Decimal(str(ticker_cur["ask"]))
|
self.cur_price = Decimal(str(ticker_cur["ask"]))
|
||||||
@@ -83,11 +83,11 @@ class Position(object):
|
|||||||
ticker_cp = self.ticker.prices[self.currency_pair]
|
ticker_cp = self.ticker.prices[self.currency_pair]
|
||||||
ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
|
ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
|
||||||
if self.position_type == "long":
|
if self.position_type == "long":
|
||||||
remove_price = ticker_cp["ask"]
|
|
||||||
qh_close = ticker_qh["bid"]
|
|
||||||
else:
|
|
||||||
remove_price = ticker_cp["bid"]
|
remove_price = ticker_cp["bid"]
|
||||||
qh_close = ticker_qh["ask"]
|
qh_close = ticker_qh["ask"]
|
||||||
|
else:
|
||||||
|
remove_price = ticker_cp["ask"]
|
||||||
|
qh_close = ticker_qh["bid"]
|
||||||
self.units -= dec_units
|
self.units -= dec_units
|
||||||
self.update_position_price()
|
self.update_position_price()
|
||||||
# Calculate PnL
|
# Calculate PnL
|
||||||
@@ -99,11 +99,9 @@ class Position(object):
|
|||||||
ticker_cp = self.ticker.prices[self.currency_pair]
|
ticker_cp = self.ticker.prices[self.currency_pair]
|
||||||
ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
|
ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
|
||||||
if self.position_type == "long":
|
if self.position_type == "long":
|
||||||
remove_price = ticker_cp["ask"]
|
|
||||||
qh_close = ticker_qh["bid"]
|
|
||||||
else:
|
|
||||||
remove_price = ticker_cp["bid"]
|
|
||||||
qh_close = ticker_qh["ask"]
|
qh_close = ticker_qh["ask"]
|
||||||
|
else:
|
||||||
|
qh_close = ticker_qh["bid"]
|
||||||
self.update_position_price()
|
self.update_position_price()
|
||||||
# Calculate PnL
|
# Calculate PnL
|
||||||
pnl = self.calculate_pips() * qh_close * self.units
|
pnl = self.calculate_pips() * qh_close * self.units
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
ipython==3.1.0
|
ipython==7.16.3
|
||||||
matplotlib==1.4.3
|
matplotlib==1.4.3
|
||||||
mock==1.0.1
|
mock==1.0.1
|
||||||
nose==1.3.6
|
nose==1.3.6
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class TestStrategy(object):
|
|||||||
self.invested = False
|
self.invested = False
|
||||||
|
|
||||||
def calculate_signals(self, event):
|
def calculate_signals(self, event):
|
||||||
if event.type == 'TICK':
|
if event.type == 'TICK' and event.instrument == self.pairs[0]:
|
||||||
if self.ticks % 5 == 0:
|
if self.ticks % 5 == 0:
|
||||||
if self.invested == False:
|
if self.invested == False:
|
||||||
signal = SignalEvent(self.pairs[0], "market", "buy", event.time)
|
signal = SignalEvent(self.pairs[0], "market", "buy", event.time)
|
||||||
|
|||||||
+13
-2
@@ -1,5 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
try:
|
try:
|
||||||
import Queue as queue
|
import Queue as queue
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -30,16 +32,23 @@ def trade(events, strategy, portfolio, execution, heartbeat):
|
|||||||
else:
|
else:
|
||||||
if event is not None:
|
if event is not None:
|
||||||
if event.type == 'TICK':
|
if event.type == 'TICK':
|
||||||
|
logger.info("Received new tick event: %s", event)
|
||||||
strategy.calculate_signals(event)
|
strategy.calculate_signals(event)
|
||||||
portfolio.update_portfolio(event)
|
portfolio.update_portfolio(event)
|
||||||
elif event.type == 'SIGNAL':
|
elif event.type == 'SIGNAL':
|
||||||
|
logger.info("Received new signal event: %s", event)
|
||||||
portfolio.execute_signal(event)
|
portfolio.execute_signal(event)
|
||||||
elif event.type == 'ORDER':
|
elif event.type == 'ORDER':
|
||||||
|
logger.info("Received new order event: %s", event)
|
||||||
execution.execute_order(event)
|
execution.execute_order(event)
|
||||||
time.sleep(heartbeat)
|
time.sleep(heartbeat)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# Set up logging
|
||||||
|
logging.config.fileConfig('../logging.conf')
|
||||||
|
logger = logging.getLogger('qsforex.trading.trading')
|
||||||
|
|
||||||
# Set the number of decimal places to 2
|
# Set the number of decimal places to 2
|
||||||
getcontext().prec = 2
|
getcontext().prec = 2
|
||||||
|
|
||||||
@@ -47,8 +56,8 @@ if __name__ == "__main__":
|
|||||||
events = queue.Queue()
|
events = queue.Queue()
|
||||||
equity = settings.EQUITY
|
equity = settings.EQUITY
|
||||||
|
|
||||||
# Trade "Cable"
|
# Pairs to include in streaming data set
|
||||||
pairs = ["GBPUSD"]
|
pairs = ["EURUSD", "GBPUSD"]
|
||||||
|
|
||||||
# Create the OANDA market price streaming class
|
# Create the OANDA market price streaming class
|
||||||
# making sure to provide authentication commands
|
# making sure to provide authentication commands
|
||||||
@@ -86,5 +95,7 @@ if __name__ == "__main__":
|
|||||||
price_thread = threading.Thread(target=prices.stream_to_queue, args=[])
|
price_thread = threading.Thread(target=prices.stream_to_queue, args=[])
|
||||||
|
|
||||||
# Start both threads
|
# Start both threads
|
||||||
|
logger.info("Starting trading thread")
|
||||||
trade_thread.start()
|
trade_thread.start()
|
||||||
|
logger.info("Starting price streaming thread")
|
||||||
price_thread.start()
|
price_thread.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user