HIDE_HEADER in bl_options over rides the panel location in the UI
Sub Panels

Blender now has subpanels. Recommend making "Holding Panels" and make your other panels children.

Example https://blender.stackexchange.com/a/155517/15543

On rearranging panels


  This is easily done by rearranging the order in which each panel is
  registered.


Maybe a poison chalice.  Remember the user can move panels around. The expected result is the register order.  Been my experience this is (generally but) not always the case.  Registering panels in startup scripts places them higher than via addon.

As mentioned above, would now use subpanels.

If the same issue is the case re a headerless panels panels moving to top, recommend using an appended or prepended draw method.

Here is example rejigged from link above

Panel 2 and 3 are children of panel 1. Both panel 1 and panel 3 are set to hide header.


2 results of running script below. Both preserve order. The render of the no header subpanel is strange (bg color).  Panel 1 is Ok, perhaps explains why no header panels are moved to top.

import bpy


class EXAMPLE_PT_panel_1(bpy.types.Panel):
    bl_label = "Panel 1"
    bl_category = "Example tab"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_options = {"HIDE_HEADER"}

    def draw(self, context):
        layout = self.layout
        layout.label(text="This is panel 1.")



class EXAMPLE_PT_panel_2(bpy.types.Panel):
    bl_label = "Panel 2"
    bl_category = "Example tab"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_options = {"DEFAULT_CLOSED"}
    bl_parent_id = "EXAMPLE_PT_panel_1"
    def draw(self, context):
        layout = self.layout
        layout.label(text="This is panel 2.")


class EXAMPLE_PT_panel_3(bpy.types.Panel):
    bl_label = "Panel 3"
    bl_category = "Example tab"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_options = {'HIDE_HEADER'}
    bl_parent_id = "EXAMPLE_PT_panel_1"
    def draw(self, context):
        layout = self.layout
        layout.label(text="This is panel 3.")


classes = [ EXAMPLE_PT_panel_2, EXAMPLE_PT_panel_3]

from random import shuffle
shuffle(classes)
classes.insert(0, EXAMPLE_PT_panel_1)
print(classes)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)


def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()


Here is another example that dynamically generates n panels.  Test script uses driver namespace to hold panels to unregister while running from __main__ 

Gives some options to play around with. There is a bl_order flag that the base PropertyPanel (Custom Props) uses always be at the bottom.

import bpy
from  bpy.app import driver_namespace as dns

for p in dns.get("panels", []):
    if getattr(p, "is_registered"):
        bpy.utils.unregister_class(p)
else:
    dns.get("panels", []).clear()

from random import shuffle

use_parent_zero = True
hide_second_headers = False
n = 5 # number of panels   
shuffle_order = True
random_shuffle = False

order = list(range(n))
if shuffle_order:
    shuffle(order)

class TestPanel:
    bl_category = 'TestPanel'
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "object"
    bl_order = 0

    def draw(self, context):
        layout = self.layout
        cls = self.__class__
        layout.label(text=f"{cls.bl_label} Order {cls.bl_order}")


panels = [type(f"Panel{i}",
                (TestPanel, bpy.types.Panel),
                {"bl_label" : f"Panel {i}",
                 "bl_idname" : f"TEST_PT_Panel{i}",
                 "bl_order" :  TestPanel.bl_order + order[i],
                 }) for i in range(n)
                 ]

panels.sort(key=lambda p : p.bl_order) 
if random_shuffle: 
    shuffle(panels)  
print([(p.bl_label, p.bl_order) for p in panels])           
for i, p in enumerate(panels):
    if i and use_parent_zero:
        p.bl_parent_id = panels[0].bl_idname
    if i % 2 and hide_second_headers :
        p.bl_options = {'HIDE_HEADER'}

    bpy.utils.register_class(p)

dns["panels"] = panels


About the only conclusion is sub panels are displayed in registered order.