diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..0dd6ddc --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [py3-port] # or - py3-port + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install test deps + run: | + python -m pip install --upgrade pip + pip install -e '.[dev]' # pulls pytest + tox (from setup.py extras) + + - name: Run pytest + run: python -m pytest -q diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 89262ba..7effdac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Changelog ========= +:Version: 1.0.3 of 2025-08-02 +Support for Python 3.8+ +Added calibrate_tim2_day function to batch.py :Version: 1.0.2 of 2017-09-29 diff --git a/README.rst b/README.rst index ec8b06f..33d3ba4 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,7 @@ Installation Dependencies (automatically installed) -------------------------------------- - - Python 2.7 + - Python 3.8+ - NumPy - SciPy - Pandas diff --git a/priceprop/__init__.py b/priceprop/__init__.py index d6e952f..d12eabf 100644 --- a/priceprop/__init__.py +++ b/priceprop/__init__.py @@ -1,4 +1,7 @@ -from propagator import * +from .propagator import * + +__version__ = "1.0.3" + def __reload_submodules__(): reload(propagator) \ No newline at end of file diff --git a/priceprop/batch.py b/priceprop/batch.py index a3845eb..5950816 100644 --- a/priceprop/batch.py +++ b/priceprop/batch.py @@ -3,7 +3,7 @@ import numpy as np import matplotlib.pyplot as plt import scorr -import propagator as prop +from . import propagator as prop # Helpers # ============================================================================ @@ -12,7 +12,15 @@ def complete_data_columns( tt, split_dates=True ): - """Fill in columns necessary for models""" + """ + Fill in columns necessary for models + + Args: + tt: pandas.DataFrame + The trades DataFrame + split_dates: bool + Whether to split trades into two samples based on date + """ if not 'sc' in tt: tt['sc'] = tt['sign'] * tt['change'] @@ -31,7 +39,18 @@ def complete_data_columns( def get_trade_split(tt, split_by): - "Get dict of sample key-mask pairs for splitting trades into groups." + """ + Get dict of sample key-mask pairs for splitting trades into groups. + + Args: + tt: pandas.DataFrame + The trades DataFrame + split_by: str + The column to split trades by + Returns: + dict + A dictionary with sample keys and mask values + """ if split_by: samples = tt[split_by].unique() masks = {m: (tt[split_by] == m) for m in samples} @@ -41,10 +60,19 @@ def get_trade_split(tt, split_by): return masks def shift(x, n, val=np.nan): - """Shift array, pad with fixed value. + """ + Shift array, pad with fixed value. Example: Convert r_1 = p(t+1) - p(t) to causal return p(t) - p(t-1) without losing timesteps with pad(r_1, 1). + Args: + x: array-like + The array to shift + n: int + The number of steps to shift + val: float + The value to pad with + Returns: """ if n == 0: @@ -59,6 +87,48 @@ def shift(x, n, val=np.nan): res[:n] = x[n:] return res + +def calibrate_tim2_day(r, eps, maxlag=180, norm='corr'): + """ + High-level shortcut: given raw return series ``r`` and order-flow + series ``eps`` (same length), compute sparse PC/NPC streams, the six + full-lag correlations, and call :pyfunc:`propagator.calibrate_tim2`. + + Args: + r: array-like + The return series + eps: array-like + The order-flow series + maxlag: int + The maximum lag to calculate the response of + norm: str + The normalization to use + Returns: + tuple + A tuple with the results + """ + import numpy as np, scorr + # PC/NPC masks (price-changing = mid-price move with matching sign) + mask_pc = (r != 0) & (np.sign(r) == np.sign(eps)) + mask_npc = ~mask_pc + eps_pc_full = np.where(mask_pc, eps, 0.0) + eps_npc_full = np.where(mask_npc, eps, 0.0) + + nncorr = scorr.acorr(eps_npc_full, norm=norm) + cccorr = scorr.acorr(eps_pc_full, norm=norm) + cncorr = scorr.xcorr(eps_pc_full, eps_npc_full, norm=norm) + ncncorr = scorr.xcorr(eps_npc_full, eps_pc_full, norm=norm) + Sln = scorr.xcorr(r, eps_pc_full, norm=norm) + Slc = scorr.xcorr(r, eps_npc_full, norm=norm) + + g, meta = prop.calibrate_tim2( + nncorr, cccorr, cncorr, ncncorr, Sln, Slc, maxlag=maxlag + ) + G_pc = g[:maxlag] + G_npc = g[maxlag:] + return G_pc, G_npc, meta + + # Analyse Trades # ============================================================================ @@ -68,9 +138,23 @@ def calibrate_models( group=False, models = ['cim','tim1','tim2','hdim2','hdim2_x2'] ): - """Return dict with correlations, kernels, and responses. + """ + Return dict with correlations, kernels, and responses. Calculate sign & price change correlations, response functions, and fitted propagator kernels for trades in DataFrame. + + Args: + tt: pandas.DataFrame + The trades DataFrame + nfft: str + The nfft to calculate the response of + group: bool + Whether to group trades by date + models: list + The models to calibrate + Returns: + dict + A dictionary with the results """ # store results @@ -245,11 +329,33 @@ def calc_models( models = ['cim','tim1','tim2','hdim2','hdim2_x2'], smooth_kernel = True ): - """Add propagator(-like) models to trades. + """ + Add propagator(-like) models to trades. Pass a dict: calibration is added to dict, not returned. Pass trades DataFrame directly: calibration is returned as DataFrame(s) + Args: + dbc: dict + A dictionary with the trades DataFrame + nfft: str + The nfft to calculate the response of + group: bool + Whether to group trades by date + calibrate: bool + Whether to calibrate the models + split_by: str + The column to split trades by + rshift: int + The number of steps to shift the return + models: list + The models to run + smooth_kernel: bool + Whether to smooth the kernel + Returns: + dict + A dictionary with the results + See also: calibrate_models, aggregate_impact.add_models_to_trades """ # normalise inputs (dict / df) diff --git a/priceprop/propagator.py b/priceprop/propagator.py index 23165b9..dfe0464 100644 --- a/priceprop/propagator.py +++ b/priceprop/propagator.py @@ -9,12 +9,35 @@ # ===================================================================== def integrate(x): - "Return lag 1 sum, i.e. price from return, or an integrated kernel." + """ + Return lag 1 sum, i.e. price from return, or an integrated kernel. + + Args: + x: array-like + The signal to integrate + Returns: + array-like + The integrated signal + + """ return np.concatenate([[0], np.cumsum(x[:-1])]) def smooth_tail_rbf(k, l0=3, tau=5, smooth=1, epsilon=1): - """Smooth tail of array k with radial basis functions""" + """ + Smooth tail of array k with radial basis functions + + Args: + k: array-like + The array to smooth + l0: int + The lag at which to start the interpolation + tau: int + The time constant for the exponential decay + Returns: + array-like + The smoothed signal + """ # interpolate in log-lags l = np.log(np.arange(l0,len(k))) # estimate functions @@ -33,6 +56,17 @@ def smooth_tail_rbf(k, l0=3, tau=5, smooth=1, epsilon=1): def propagate(s, G, sfunc=np.sign): """Simulate propagator model from signs and one kernel. Equivalent to tim1, one of the kernels in tim2 or hdim2. + + Args: + s: array-like + The signs to propagate + G: array-like + The kernel to propagate with + sfunc: function + The function to apply to the signs. Default: np.sign + Returns: + array-like + The propagated signal """ steps = len(s) s = sfunc(s[:len(s)]) @@ -43,7 +77,21 @@ def propagate(s, G, sfunc=np.sign): # ===================================================================== def _return_response(ret, x, maxlag): - """Helper for response and response_grouped_df.""" + """ + Helper for response and response_grouped_df. + + Args: + ret: str + 'l' for lags, 's' for differential response, 'r' for return response + x: array-like + The signal to calculate the response of + maxlag: int + The maximum lag to calculate the response of + Returns: + tuple + The lag, differential response, and response + """ + maxlag = int(maxlag) # return what? ret = ret.lower() res = [] @@ -76,20 +124,21 @@ def response(r, s, maxlag=10**4, ret='lsr', subtract_mean=False): Note that this commonly used price response is a simple cross correlation and NOT equivalent to the linear response in systems analysis. - Parameters: - =========== - - r: array-like - Returns - s: array-like - Order signs - maxlag: int - Longest lag to calculate - ret: str - can include 'l' to return lags, 'r' to return response, and - 's' to return differential response (in specified order). - subtract_mean: bool - Subtract means first. Default: False (signal means already zero) + Args: + r: array-like + Returns + s: array-like + Order signs + maxlag: int + Longest lag to calculate + ret: str + can include 'l' to return lags, 'r' to return response, and + 's' to return differential response (in specified order). + subtract_mean: bool + Subtract means first. Default: False (signal means already zero) + Returns: + tuple + The lag, differential response, and response """ maxlag = min(maxlag, len(r) - 2) s = s[:len(r)] @@ -106,19 +155,20 @@ def response_grouped_df( Note that this commonly used price response is a simple cross correlation and NOT equivalent to the linear response in systems analysis. - Parameters - ========== - - df: pandas.DataFrame - Dataframe containing order signs and returns - cols: tuple - The columns of interest - nfft: - Length of the fft segments - ret: str - What to return ('l': lags, 'r': response, 's': incremental response). - subtract_mean: bool - Subtract means first. Default: False (signal means already zero) + Args: + df: pandas.DataFrame + The dataframe to calculate the response of + cols: list + The columns to calculate the response of + nfft: str + The nfft to calculate the response of + ret: str + What to return ('l': lags, 'r': response, 's': incremental response). + subtract_mean: bool + Subtract means first. Default: False (signal means already zero) + Returns: + tuple + The lag, differential response, and response See also response, spectral.xcorr_grouped_df for more explanations """ @@ -143,21 +193,51 @@ def response_grouped_df( # ===================================================================== def beta_from_gamma(gamma): - """Return exponent beta for the (integrated) propagator decay + """ + Return exponent beta for the (integrated) propagator decay G(lag) = lag**-beta that compensates a sign-autocorrelation C(lag) = lag**-gamma. + + Args: + gamma: float + The exponent of the sign-autocorrelation + Returns: + float + The exponent beta """ return (1-gamma)/2. def G_pow(steps, beta): - """Return power-law Propagator kernel G(l). l=0...steps""" + """ + Return power-law Propagator kernel G(l). l=0...steps + + Args: + steps: int + The number of steps + beta: float + The exponent of the propagator decay + Returns: + array-like + The propagator kernel + """ G = np.arange(1,steps)**-beta#+1 G = np.r_[0, G] return G def k_pow(steps, beta): - """Return increment of power-law propagator kernel g. l=0...steps""" + """ + Return increment of power-law propagator kernel g. l=0...steps + + Args: + steps: int + The number of steps + beta: float + The exponent of the propagator decay + Returns: + array-like + The propagator kernel + """ return np.diff(G_pow(steps, beta)) # TIM1 specific @@ -166,10 +246,8 @@ def k_pow(steps, beta): def calibrate_tim1(c, Sl, maxlag=10**4): """Return empirical estimate TIM1 kernel - Parameters: - =========== - - c: array-like + Args: + c: array-like Cross-correlation (covariance). Sl: array-like Price-response. If the response is differential, so is the returned @@ -190,10 +268,8 @@ def tim1(s, G, sfunc=np.sign): and the 1 step ahead return p(t+1)-p(t) for the differential kernel g, where G == numpy.cumsum(g). - Parameters: - =========== - - s: array-like + Args: + s: array-like Order signs G: array-like Kernel @@ -212,24 +288,25 @@ def calibrate_tim2( Return empirical estimate for both kernels of the TIM2. (Transient Impact Model with two propagators) - Parameters: - =========== - - nncorr: array-like - Cross-covariance between non-price-changing (n-) orders. - cccorr: array-like - Cross-covariance between price-changing (c-) orders. - cncorr: array-like - Cross-covariance between c- and n-orders - nccorr: array-like - Cross-covariance between n- and c-orders. - Sln: array-like - (incremental) price response for n-orders - Slc: array-like - (incremental) price response for c-orders - maxlag: int - Length of the kernels. - + Args: + nncorr: array-like + Cross-correlation matrix for non-price-changing trades + cccorr: array-like + Cross-correlation matrix for price-changing trades + cncorr: array-like + Cross-correlation matrix for non-price-changing trades + nccorr: array-like + Cross-correlation matrix for non-price-changing trades + Sln: array-like + (incremental) price response for non-price-changing trades + Slc: array-like + (incremental) price response for price-changing trades + maxlag: int + Length of the kernels. + Returns: + tuple + The non-price-changing and price-changing kernels + See also: calibrate_tim1, calibrate_hdim2 """ # incremental response @@ -257,10 +334,9 @@ def tim2(s, c, G_n, G_c, sfunc=np.sign): Returns prices when integrated kernels are passed as arguments or returns for differential kernels. - Parameters: - =========== - s: array - Trade signs + Args: + s: array + Trade signs c: array Trade labels (1 = change; 0 = no change) G_n: array @@ -269,7 +345,9 @@ def tim2(s, c, G_n, G_c, sfunc=np.sign): Kernel for price-changing trades sfunc: function [optional] Function to apply to signs. Default: np.sign. - + Returns: + array-like + The propagated signal See also: calibrate_tim2, tim1, hdim2. """ assert c.dtype == bool, "c must be a boolean indicator!" @@ -292,8 +370,7 @@ def calibrate_hdim2( orders. The argument names corresponds to the argument order in spectral.x3corr. - Parameters: - =========== + Args: Cnnc: 2d-array-like Cross-covariance matrix for n-, n-, c- orders. Cccc: 2d-array-like @@ -306,7 +383,9 @@ def calibrate_hdim2( (incremental) lagged price response for c-orders maxlag: int Length of the kernels. - + Returns: + tuple + The non-price-changing and price-changing kernels See also: hdim2, """ maxlag = maxlag or min(len(Cccc), len(Sln))/2 @@ -338,11 +417,11 @@ def calibrate_hdim2( return gn, gc def hdim2(s, c, k_n, k_c, sfunc=np.sign): - """Simulate History Dependent Impact Model 2, return return. + """ + Simulate History Dependent Impact Model 2, return return. - Parameters: - =========== - s: array + Args: + s: array Trade signs c: array Trade labels (1 = change; 0 = no change) @@ -352,7 +431,9 @@ def hdim2(s, c, k_n, k_c, sfunc=np.sign): Differential kernel for price-changing trades sfunc: function [optional] Function to apply to signs. Default: np.sign. - + Returns: + array-like + The propagated signal See also: calibrate_hdim2, tim2, tim1. """ assert c.dtype == bool, "c must be a boolean indicator!" diff --git a/setup.py b/setup.py index f22235a..be412ff 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='priceprop', - version='1.0.2', + version='1.0.3', description=( 'Calibrate and simulate linear propagator models for the price ' 'impact of an extrinsic order flow.' @@ -15,19 +15,22 @@ Journal of Statistical Mechanics (2017, in print) Preprint at arXiv:1706.04163 """, + python_requires='>=3.8', classifiers=[ - 'Development Status :: 5 - Production/Stable', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: Education', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2 :: Only', - 'Programming Language :: Python :: 2.7', - #'Programming Language :: Python :: 3', - #'Programming Language :: Python :: 3.6', 'Topic :: Scientific/Engineering', 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules' + 'Topic :: Software Development :: Libraries :: Python Modules', + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3 :: 8", + "Programming Language :: Python :: 3 :: 9", + "Programming Language :: Python :: 3 :: 10", + "Programming Language :: Python :: 3 :: 11" ], keywords=[ 'propagator', 'price impact', 'intra day', 'order flow', @@ -36,19 +39,20 @@ ], url='http://github.com/felixpatzelt/priceprop', download_url=( - 'https://github.com/felixpatzelt/priceprop/archive/1.0.2.tar.gz' + 'https://github.com/felixpatzelt/priceprop/archive/1.0.3.tar.gz' ), author='Felix Patzelt', author_email='felix@neuro.uni-bremen.de', license='MIT', packages=find_packages(), install_requires=[ - 'numpy', - 'scipy', - 'pandas', - 'scorr' + "numpy>=1.24", + "scipy>=1.10", + "pandas>=2.0", + "scorr @ git+https://github.com/robatobalzac/scorr.git" ], include_package_data=True, zip_safe=False, test_suite='tests', + extras_require={'dev': ['pytest>=8', 'tox>=4']} ) \ No newline at end of file diff --git a/tests/test_propagator.py b/tests/test_propagator.py index f1f807c..af54608 100644 --- a/tests/test_propagator.py +++ b/tests/test_propagator.py @@ -107,7 +107,7 @@ def test_calibrate_hdim2(self): lpars = { k: np.array(v) if hasattr(v, '__len__') else v - for k,v in json.load(f).iteritems() + for k,v in json.load(f).items() } kn_est, kc_est = prop.calibrate_hdim2( lpars['Cnnc'], diff --git a/tests/test_tim2.py b/tests/test_tim2.py new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4bc297d --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py311, py310, py39, py38 + +[testenv] +deps = pytest +usedevelop = true # install package in editable mode instead of sdist +changedir = {toxinidir} # run tests in the repo root +commands = pytest -q \ No newline at end of file