2

I have a loop that cycles days and discretised time slots of a financial time series, and some code for my strategy lies inside it. I extracted this inner code to a standalone function, to be able to use it in specific situations for testing, to only find out a bit later that this operation heavily increased the total computational time of this loop. It makes sense, the long function signature, the call, (stack memory?) all this has to introduce some overhead.

# Fast, but cannot use my strategy for specific instances:
for d in data:
    # my strategy code

# Flexible but really slow
def my_strategy(**params):
    # my strategy code
f = partial(my_strategy, **params)
for d in data:
    f(d)

Now, my question: is there a way to keep both setups, the standalone function and the one with the loops and the actual code inside without explicit redundancy? I would need to have both.

I've been experiment with inspect.getsource(my_function) and wrap a copy of the outer function to then use eval but after 5 min I felt like an idiot, it does not feel right at all.

What's the best practice to go about this? If I duplicate the code, I have to make sure that if I modify one version, the other one is always in sync. I don't like this. I would like to mirror it efficiently.

As you might have thought, one option could be: leave the code where it is, inside the loop, and wrap your specific case with an array and feed it to the loop, whenever you need it. Well, I can't do that, mainly because I need to have a timed control over the inner code execution (am porting the back-tested strategy to real-time). Here I am just looking for some quick trick to mirror the code elegantly, if there is any.

--- EDIT ---

Ok so, to test this further, I implemented that horrible hack I mentioned and I made it work with exec. Thanks to this, I can now guarantee that the code used is exactly the same. The time difference observed is the same though. The loop that contains the code takes roughly HALF of the time taken by the loop with function call.

# horrible hack, please forgive me
def extract_strategy_function():
    # Start
    source_code = inspect.getsource(fastest_compute_trades)
    # Cutting the code snippet
    source_code = source_code.split('[safe_start - 1:]:', 1)[1].rsplit("if s['pos_open']:", 1)[0]
    # Needs re-indentation so the parser doesn't complain
    source_code = reindent(source_code, -2)
    function_name = 'single_apply_strategy'  # internal, no-one will see this anyway
    function_string = f"""
    def {function_name}(
            # INDICATORS (non mutable)
            i_time, close_price, r2, ma1, ma2, rsi, upper, lower,
            # STATE VARIABLES, TX HISTORY (mutable, 2 dictionaries and 1 list of dicts)
            s, gs, transactions,
            # CONSTANT PARAMETERS (non mutable)
            min_r2, macd_threshold, rsi_bound, waiting_slots, stop_loss,
            take_profit, trail_profit, double_down, dd_qty, commission_fee,
            # Day ID, useful when back-testing/grid-searching, left at the end so it can default to 0
            i_day=0
    ):
    {source_code}
    """.strip()
    # Evaluate it
    exec(function_string)  # Now in locals
    return locals()[function_name]
    
def slow(...):
    
    ...
    
    apply_strategy = partial(extract_strategy_function(), 
        **params, commission_fee=commission_fee, rsi_bound=rsi_bound)
   
    for i_day, day in enumerate(data):
        for i_time, close_price, r2, ma1, ma2, rsi, upper, lower, in list(enumerate(day))[safe_start - 1:]:
            apply_strategy(i_time, close_price, r2, ma1, ma2, rsi, upper, lower, 
                day_state, global_state, transactions, i_day=i_day)

        if day_state['pos_open']:
            ...
    
    ...
    
def fast(..., 
    #some extra parameters are now unpacked here, 
    #since all code is here they are now needed here
):
    ...
    
    for i_day, day in enumerate(data):
        for i_time, close_price, r2, ma1, ma2, rsi, upper, lower, in list(enumerate(day))[safe_start - 1:]:
            # actual code contained in single_apply_strategy written here, the whole strategy (one timestep)

        if day_state['pos_open']:
            ...
            
    ...

Any suggestions?

p.s. Needless to say, the output of the two setups is exactly the same

quan2m
  • 43
  • 1
  • 7

1 Answers1

2

Any overhead caused by function call is most probably negligible. Use your function and in case of need, measure the possible improvement by "manually inlining" your function.

In the case that you would need such performance, Python is probably not the best choice, in such case I would recommend C++. There is a language feature you ask for and it is called inline function: https://www.geeksforgeeks.org/inline-functions-cpp/

More information on Python and inlining: Python equivalence to inline functions or macros

In any case, I don't think your effort in this direction would bear any improvement.

Roman Pavelka
  • 3,736
  • 2
  • 11
  • 28
  • 1
    Ok so, to test this further, I implemented that horrible hack I mentioned and I made it work with `exec`. Thanks to this, I can now guarantee that the code used is exactly the same. The time difference observed is the same though. The loop that contains the code takes roughly HALF of the time taken by the loop with function call. How can this be even possible? It might have to do with the long signature. A lot of mutable and non mutable parameters. Thanks for the answer anyway, I'll look it up now – quan2m Feb 08 '21 at 15:44
  • @quan2m This is interesting, may you share the signature? – Roman Pavelka Feb 08 '21 at 15:50
  • https://shareablecode.com/snippets/war-against-redundancy-ydRS-9GDp Does this work? Alternatively, please tell me a better way to share it. If you need anything else just ask – quan2m Feb 08 '21 at 16:19
  • I edited this post and added it here, with some corrections.. @Roman – quan2m Feb 08 '21 at 16:28
  • 1
    @quan2m Maybe `import dis`, then `dis.dis(slow)`and `dis.dis(fast)` gives some insight. – AcK Feb 09 '21 at 22:45